(一)SpringCloud之Eureka註冊中心

Eureka註冊中心

1、什麼是註冊中心

打個比方,註冊中心就好比手機中的通訊錄,所有的聯繫人的聯繫方式就在這個通訊錄中儲存。當需要打電話的時候,只需要查詢通訊錄就可以獲取某個聯繫人的聯繫方式。

註冊中心類似於通信錄,只不過註冊中心儲存的不是聯繫人的聯繫方式,而是每個服務的信息,從註冊中心獲取服務就好比通訊錄的查詢聯繫人,向註冊中心註冊服務,就好比通訊錄的保存聯繫人,先有註冊,才能查找。

注:註冊中心只不過是用來註冊和獲取服務,並不會用來調取服務,具體的調取服務有獲取服務方實現。就好比通訊錄只負責儲存和查詢聯繫人,查到聯繫人後使用撥號軟件撥打查詢到的聯繫人電話。

2、CAP原則和BASE理論

2.1、CAP原則

在這裏插入圖片描述

名稱 描述
Consistency 一致性。也叫做原子性。系統在執行某些操作後數據仍然處於一致的狀態。在分佈式系統中,更新操作執行成功後所有的用戶都應該讀到最新的值,這樣的系統被認爲是具有強一致性的。等同於所有節點訪問同一份最新的數據副本。
Availability 可用性。每一個操作總是能夠在一定的時間內返回結果,這裏需要注意的是"一定時間內"和"返回結果”。一定時間內指的是在可以容忍的範圍內返回結果,結果可以是成功或者是失敗,且不保證獲取的數據爲最新數據。
Partition tolerance 分區容錯性。分佈式系統在遇到任何網絡分區故障的時候,仍然能夠對外提供滿足一致性和可用性的服務,除非整個網絡環境都發生了故障。

CAP原則又稱CAP定理。值得是在系統中,ConsistencyAvailabilityPartition tolerance三者不可兼得。最多隻能同時滿足其中兩者。

CAP 由 Eric Brewer 在 2000 年 PODC 會議上提出。該猜想在提出兩年後被證明成立,成爲我們熟知的 CAP定理。CAP三者不可兼得。

  • CA:一致性和可用性。如果同時要滿足一致性和可用性,那麼只有單體應用可以實現。因爲如果是分佈式應用的話,如果要保證多個系統的一致性,可定要消耗時間。那麼可用性肯定就達不到要求。只有單體應用,可以滿足CA
  • CP:一致性和分區容錯性。這種典型的案例就是涉及到金錢的系統。涉及到金錢的系統肯定要保證數據一致,在分佈式中多臺服務器要保證數據一致,肯定不能再一定的時間內返回數據。好比在進行金錢轉賬的時候,寧願犧牲可用性,也要保證數據一致性。
  • AP:可用性和分區容容錯性。這種的經典案例就是搶購性的系統。例如春運的火車票,有時候明明看到還有票,但是在下單的時候卻被告知沒票了,這就是因爲需要滿足可用性,在一定的時間內返回數據,從而放棄了數據的一致性。但是這種情況在最後會有一個最終一致性。好比購票,顯示的是還有車票,但是下單時卻顯示失敗,就是因爲最終一致性。在最後的時候會進行一致性判斷。

如今,對於大多數互聯網應用場景,主機衆多,部署分散,並且規模也越來越大,節點只會越來越多,所以節點故障、網絡故障時常態,分區容錯性也就成爲了一個分佈式系統必然要面對的問題。那麼就只能在C和A之前進行取捨。但是對於設計金錢的系統卻不同,涉及到金錢的是非常重要的,寧願犧牲A,也要保證C。如果出現機器故障的話,寧願停止服務。

沒有最好的策略,只有最符合當前系統的策略。

2.2、BASE理論

CAP 理論已經提出好多年了,難道真的沒有辦法解決這個問題嗎?也許可以做些改變。比如 C 不必使用那麼強的一致性,可以先將數據存起來,稍後再更新,實現所謂的 “最終一致性”。這個思路又是一個龐大的問題,同時也引出了第二個理論 BASE 理論。

BASE全稱爲 Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)三個短語句的縮寫,來自 ebay 的架構師提出。

BASE 理論是對 CAP 中一致性和可用性權衡的結果,其來源於對大型互聯網分佈式實踐的總結,是基於 CAP 定理逐步演化而來的。其核心思想是:既然無法做到強一致性(Strong consistency),但每個應用都可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性(Eventual consistency)。

2.2.1、Basically Available(基本可用)

基本可用是指分佈式系統在出現故障時,允許損失部分可用性(例如響應時間、功能上的可用性)。需要注意的是,基本可用絕不等價於系統不可用。

  • 響應時間上的丟失:例如原先查詢數據只需要0.5秒,但是現在由於機器故障可以運行返回響應時間變爲1~2秒。
  • 功能上的可用性:例如雙十一購物的時候,爲了滿足非常龐大的流量衝擊,爲了保證系統的大方向的穩定,允許對一些服務降級處理(縮減服務器集羣,給別的訪問量大的使用)或者停止一些非必要的服務。

2.2.2、Soft state(軟狀態)

相對於原子性而言,要求多個節點的數據副本都是一致的,這是一種 “硬狀態”。

軟狀態是指允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。分佈式存儲中一般一份數據會有多個副本,允許不同副本數據同步的延時就是軟狀態的體現。

2.2.3、Eventually consistent(最終一致性)

系統不可能一直是軟狀態,必須有個時間期限。在期限過後,應當保證所有副本保持數據一致性。從而達到數據的最終一致性。這個時間期限取決於網絡延時,系統負載,數據複製方案設計等等因素。

實際上,不只是分佈式系統使用最終一致性,關係型數據庫在某個功能上,也是使用最終一致性的,比如備份,數據庫的複製都是需要時間的,這個複製過程中,業務讀取到的值就是舊值。當然,最終還是達成了數據一致性。這也算是一個最終一致性的經典案例。

3、爲什麼需要註冊中心

在分佈式系統中,不僅僅是需要在註冊中心找到服務和服務地址的映射關係那麼簡單,還需要考慮很多的複雜問題:

  • 服務註冊後,如何被及時的發現
  • 服務宕機後,如何及時處理下線
  • 服務如何有效的水平擴展
  • 服務發現時,如何進行路由
  • 服務異常時,如何進行降級
  • 註冊溪紅心如果實現自身的高可用

這些問題的解決都依賴於註冊中心。簡單看,註冊中心的功能有點類似於 DNS 服務器或者負載均衡器,而實際上,註冊中心作爲微服務的基礎組件,可能要更加複雜,也需要更多的靈活性和時效性。當然上面的問題,單單使用註冊中心是無法完成的,還需要使用SpringCloud的其他組件共同完成。

註冊中心解決了一下問題:

  • 服務管理
  • 服務之間的自動發現
  • 服務的依賴關係管理

4、Eureka

4.1、Eureka介紹

Eureka是Netflix開發的服務發現框架,本身是一個基於REST的服務,主要用於定位運行在AWS域中的中間層服務,以達到負載均衡和中間層服務故障轉移的目的。SpringCloud將它集成在其子項目spring-cloud-netflix中,以實現SpringCloud的服務發現功能。

4.2、Eureka註冊中心的各個角色

在這裏插入圖片描述

  1. **Eureka Server:**通過 Register、Get、Renew 等接口提供服務的註冊和發現。
  2. **Service Provider(Eureka Client):**服務提供方,把自身的服務實例註冊到 Eureka Server 中。
  3. **Service Consumer(Eureka Client):**服務調用方,通過 Eureka Server 獲取服務列表,消費服務。

4.3、Eureka入門案例

4.3.1、創建Mave聚合項目

在這裏插入圖片描述

  • server01:註冊中心
  • server02:註冊中心(後期搭建集羣版準備)
  • provider:服務提供者
  • consumer:服務消費者

4.3.2、添加依賴,配置文件

4.3.2.1、父項目

pom.xml

<!-- 繼承 spring-boot-starter-parent 依賴 -->
<!-- 使用繼承方式,實現複用,符合繼承的都可以被使用 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.0.RELEASE</version>
</parent>


<!--
        集中定義依賴組件版本號,但不引入,
        在子工程中用到聲明的依賴時,可以不加依賴的版本號,
        這樣可以統一管理工程中用到的依賴版本
     -->
<properties>
    <!-- Spring Cloud Hoxton.SR1 依賴 -->
    <spring-cloud.version>Hoxton.SR5</spring-cloud.version>
</properties>

<!-- 項目依賴管理 父項目只是聲明依賴,子項目需要寫明需要的依賴(可以省略版本信息) -->
<dependencyManagement>
    <dependencies>
        <!-- spring cloud 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

4.3.2.2、server02

pom.xml

<!-- 項目依賴 -->
<dependencies>
    <!-- netflix eureka server 依賴 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <!-- spring boot web 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- spring boot test 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

application.yml

server:
  port: 8761 # 端口

spring:
  application:
    name: eureka-server # 應用名稱

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    hostname: localhost			  # 主機名,不配置的時候將根據操作系統的主機名來獲取
  client:
    register-with-eureka: false   # 是否將自己註冊到註冊中心,默認爲 true
    fetch-registry: false         # 是否從註冊中心獲取服務註冊信息,默認爲 true
    service-url:                  # 註冊中心對外暴露的註冊地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

如果是單節點需要把register-with-eurekafetch-registry設置爲false,否則會報錯

4.3.3、單節點啓動

package cn.yanghuisen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * @author admin
 * @version 1.0
 * @date 2020/6/15 15:16
 * @Description TODO
 */
@SpringBootApplication
@EnableEurekaServer     // 開啓EurekaServer註解
public class EurekaServer01Application {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServer01Application.class);
    }
}

訪問:http://localhost:8761/

4.3.4、高可用(集羣)

server02

pom.xml

<!-- 項目依賴 -->
<dependencies>
    <!-- netflix eureka server 依賴 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <!-- spring boot web 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- spring boot test 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>cn.yanghuisen</groupId>
        <artifactId>eureka-server01</artifactId>
        <version>1.0-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

server02:application.yml

server:
  port: 8762 # 端口

spring:
  application:
    name: eureka-server # 應用名稱(集羣下相同)

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    hostname: eureka02            # 主機名,不配置的時候將根據操作系統的主機名來獲取
  client:
    # 設置服務註冊中心地址,指向另一個註冊中心
    service-url:                  # 註冊中心對外暴露的註冊地址
      defaultZone: http://localhost:8761/eureka/

server01:application.yml

server:
  port: 8761 # 端口

spring:
  application:
    name: eureka-server # 應用名稱(集羣下相同)

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    hostname: eureka01            # 主機名,不配置的時候將根據操作系統的主機名來獲取
  client:
    # 設置服務註冊中心地址,指向另一個註冊中心
    service-url:                  # 註冊中心對外暴露的註冊地址
      defaultZone: http://localhost:8762/eureka/

server02:啓動類

package cn.yanghuisen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * @author admin
 * @version 1.0
 * @date 2020/6/15 15:16
 * @Description TODO
 */
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer02Application {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServer01Application.class);
    }
}

訪問:http://localhost:8761/ 或者 http://localhost:8762/

4.3.5、顯示IP+端口

(server01和server02):application.yml

eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port

4.3.6、provider

pom.xml

<!-- 項目依賴 -->
<dependencies>
    <!-- netflix eureka client 依賴 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- spring boot web 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- lombok 依賴 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- spring boot test 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

application.yml

server:
  port: 7070 # 端口

spring:
  application:
    name: service-provider # 應用名稱(集羣下相同)

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

實體類

package cn.yanghuisen.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private Integer id;
    private String productName;
    private Integer productNum;
    private Double productPrice;

}

控制層

package cn.yanghuisen.controller;

import cn.yanghuisen.pojo.Product;
import cn.yanghuisen.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 查詢商品列表
     *
     * @return
     */
    @GetMapping("/list")
    public List<Product> selectProductList() {
        return productService.selectProductList();
    }

}

服務類:接口

package cn.yanghuisen.service;


import cn.yanghuisen.pojo.Product;

import java.util.List;

/**
 * 商品服務
 */
public interface ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    List<Product> selectProductList();

}

服務類:實現類

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

/**
 * 商品服務
 */
@Service
public class ProductServiceImpl implements ProductService {

    /**
     * 查詢商品列表
     *
     * @return
     */
    @Override
    public List<Product> selectProductList() {
        return Arrays.asList(
                new Product(1, "華爲手機", 2, 5888D),
                new Product(2, "聯想筆記本", 1, 6888D),
                new Product(3, "小米平板", 5, 2666D)
        );
    }

}

啓動類

package cn.yanghuisen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// 開啓 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,默認會開啓該註解
//@EnableEurekaClient
public class ServiceProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceProviderApplication.class, args);
    }

}

訪問:http://localhost:8761/ 或者 http://localhost:8762/。查看服務提供者是否已經註冊到註冊中心

4.3.7、consumer

pom.xml

<!-- 項目依賴 -->
<dependencies>
    <!-- netflix eureka client 依賴 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.fasterxml.jackson.dataformat</groupId>
                <artifactId>jackson-dataformat-xml</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- spring boot web 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- lombok 依賴 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- spring boot test 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

application.yml

server:
  port: 9090 # 端口

spring:
  application:
    name: service-consumer # 應用名稱

# 配置 Eureka Server 註冊中心
eureka:
  client:
    register-with-eureka: false         # 是否將自己註冊到註冊中心,默認爲 true
    registry-fetch-interval-seconds: 10 # 表示 Eureka Client 間隔多久去服務器拉取註冊信息,默認爲 30 秒
    service-url:                        # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

實體類

package cn.yanghuisen.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private Integer id;
    private String productName;
    private Integer productNum;
    private Double productPrice;

}

package cn.yanghuisen.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {

    private Integer id;
    private String orderNo;
    private String orderAddress;
    private Double totalPrice;
    private List<Product> productList;

}

消費服務:接口

package cn.yanghuisen.service;


import cn.yanghuisen.pojo.Order;

public interface OrderService {

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    Order selectOrderById(Integer id);

}

控制層

package cn.yanghuisen.controller;

import cn.yanghuisen.pojo.Order;
import cn.yanghuisen.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Order selectOrderById(@PathVariable("id") Integer id) {
        return orderService.selectOrderById(id);
    }

}

消費服務:方式一

package cn.yanghuisen.service.impl;

import cn.yanghuisen.pojo.Order;
import cn.yanghuisen.pojo.Product;
import cn.yanghuisen.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order selectOrderById(Integer id) {
        return new Order(id, "order-001", "中國", 31994D,
                selectProductListByDiscoveryClient());
    }

    private List<Product> selectProductListByDiscoveryClient() {
        StringBuffer sb = null;

        // 獲取服務列表
        List<String> serviceIds = discoveryClient.getServices();
        if (CollectionUtils.isEmpty(serviceIds))
            return null;

        // 根據服務名稱獲取服務
        List<ServiceInstance> serviceInstances = discoveryClient.getInstances("service-provider");
        if (CollectionUtils.isEmpty(serviceInstances))
            return null;

        ServiceInstance si = serviceInstances.get(0);
        sb = new StringBuffer();
        sb.append("http://" + si.getHost() + ":" + si.getPort() + "/product/list");

        // ResponseEntity: 封裝了返回數據
        ResponseEntity<List<Product>> response = restTemplate.exchange(
                sb.toString(),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {});
        return response.getBody();
    }
}

消費服務:方式二

package cn.yanghuisen.service.impl;

import cn.yanghuisen.pojo.Order;
import cn.yanghuisen.pojo.Product;
import cn.yanghuisen.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient; // Ribbon 負載均衡器

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order selectOrderById(Integer id) {
        return new Order(id, "order-001", "中國", 31994D,
                selectProductListByLoadBalancerClient());
    }

    private List<Product> selectProductListByLoadBalancerClient() {
        StringBuffer sb = null;

        // 根據服務名稱獲取服務
        ServiceInstance si = loadBalancerClient.choose("service-provider");
        if (null == si)
            return null;

        sb = new StringBuffer();
        sb.append("http://" + si.getHost() + ":" + si.getPort() + "/product/list");

        // ResponseEntity: 封裝了返回數據
        ResponseEntity<List<Product>> response = restTemplate.exchange(
                sb.toString(),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {});
        return response.getBody();
    }
}

消費服務:方式三

啓動類

@Bean
@LoadBalanced // 負載均衡註解
public RestTemplate restTemplate() {
    return new RestTemplate();
}
package cn.yanghuisen.service.impl;

import cn.yanghuisen.pojo.Order;
import cn.yanghuisen.pojo.Product;
import cn.yanghuisen.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 根據主鍵查詢訂單
     *
     * @param id
     * @return
     */
    @Override
    public Order selectOrderById(Integer id) {
        return new Order(id, "order-001", "中國", 31994D,
                selectProductListByLoadBalancerAnnotation());
    }

    private List<Product> selectProductListByLoadBalancerAnnotation() {
        // ResponseEntity: 封裝了返回數據
        ResponseEntity<List<Product>> response = restTemplate.exchange(
                "http://service-provider/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {});
        return response.getBody();
    }
}

訪問:http://localhost:9090/order/1

4.3.8、自我保護

一般情況下,服務在 Eureka 上註冊後,會每 30 秒發送心跳包,Eureka 通過心跳來判斷服務是否健康,同時會定期刪除超過 90 秒沒有發送心跳的服務。

有兩種情況會導致 Eureka Server 收不到微服務的心跳

  • 微服務自身的原因
  • 微服務與 Eureka 之間的網絡故障

自我保護模式

Eureka Server 在運行期間會去統計心跳失敗比例在 15 分鐘之內是否低於 85%,如果低於 85%,Eureka Server 會將這些實例保護起來,讓這些實例不會過期,同時提示一個警告。這種算法叫做 Eureka Server 的自我保護模式。

爲什麼要啓動自我保護

  • 因爲同時保留"好數據"與"壞數據"總比丟掉任何數據要更好,當網絡故障恢復後,這個 Eureka 節點會退出"自我保護模式”。
  • Eureka 還有客戶端緩存功能(也就是微服務的緩存功能)。即使 Eureka 集羣中所有節點都宕機失效,微服務的 Provider 和 Consumer 都能正常通信。
  • 微服務的負載均衡策略會自動剔除死亡的微服務節點。

關閉自我保護

server01和server02:application.yml

eureka:
  server:
    enable-self-preservation: false # true:開啓自我保護模式,false:關閉自我保護模式
    eviction-interval-timer-in-ms: 60000 # 清理間隔(單位:毫秒,默認是 60*1000)

4.3.9、優雅停服

配置了優雅停服以後,將不需要 Eureka Server 中配置關閉自我保護。本文使用 actuator 實現。

1、添加依賴

provider:pom.xml

<!-- spring boot actuator 依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2、配置文件

provider:application.yml

# 度量指標監控與健康檢查
management:
  endpoints:
    web:
      exposure:
        include: shutdown         # 開啓 shutdown 端點訪問
  endpoint:
    shutdown:
      enabled: true               # 開啓 shutdown 實現優雅停服

使用 POST 請求訪問:http://localhost:7070/actuator/shutdown

4.3.10、Eureka安全認證

1、添加依賴

server01和server02:pom.xml

<!-- spring boot security 依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、配置文件

server01和server02:application.yml

spring:
  # 安全認證
  security:
    user:
      name: root
      password: 123456

3、修改訪問集羣節點的 url

server01和server02:application.yml

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    hostname: eureka01            # 主機名,不配置的時候將根據操作系統的主機名來獲取
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    # 設置服務註冊中心地址,指向另一個註冊中心
    service-url:                  # 註冊中心對外暴露的註冊地址
      defaultZone: http://root:123456@localhost:8762/eureka/

provider:application.yml

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8762/eureka/

consumer:application.yml

# 配置 Eureka Server 註冊中心
eureka:
  client:
    register-with-eureka: false         # 是否將自己註冊到註冊中心,默認爲 true
    registry-fetch-interval-seconds: 10 # 表示 Eureka Client 間隔多久去服務器拉取註冊信息,默認爲 30 秒
    service-url:                        # 設置服務註冊中心地址
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8762/eureka/

4.3.11、過濾CSRF

Eureka 會自動化配置 CSRF 防禦機制,Spring Security 認爲 POST, PUT, and DELETE http methods 都是有風險的,如果這些 method 發送過程中沒有帶上 CSRF token 的話,會被直接攔截並返回 403 forbidden。

官方給出瞭解決的方法,具體可以參考 spring cloud issue 2754,裏面有大量的討論,這裏提供兩種解決方案。

首先註冊中心配置一個 @EnableWebSecurity 配置類,繼承 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,然後重寫 configure 方法。

方案一:忽略/eureka/的所有請求

server01和server02:配置類

package cn.yanghuisen.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * 安全認證配置類
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http); // 加這句是爲了訪問 eureka 控制檯和 /actuator 時能做安全控制
        http.csrf().ignoringAntMatchers("/eureka/**"); // 忽略 /eureka/** 的所有請求
    }

}

方案二:保持密碼驗證的同時禁用 CSRF 防禦機制

package cn.yanghuisen.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * 安全認證配置類
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 注意,如果直接 disable 的話會把安全驗證也禁用掉
        http.csrf().disable().authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .httpBasic();
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章