SpringCloud入門學習筆記(8-9初級部分,服務調用【Ribbon與OpenFeign】)

八、Ribbon負載均衡服務調用

恢復eureka集羣環境,以便接下來的練習。

簡介

在這裏插入圖片描述

負載均衡

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

Ribbon(負載均衡+RestTemplate調用)

Ribbon是客戶端(消費者)負載均衡的工具。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

pom

  <!--Ribbon的依賴-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
 </dependency>

新版的eureka依賴以及集成了Ribbon依賴,所以可以不引用。
在這裏插入圖片描述

RestTemplate

@LoadBalanced註解給RestTemplate開啓負載均衡的能力。

官方文檔:https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
在這裏插入圖片描述

getForObject/getForEntity方法

在這裏插入圖片描述
getForObject已經用過了,所以只測試getForEntity方法。

測試getForEntity方法

在消費者cloud-consumer-order80的OrderController方法中添加:

    @GetMapping("/consumer/payment/getEntity/{id}")
    public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
        log.info("********查詢的id:" + id);
        //getForObject兩個參數:請求地址,返回的對象類型
//        return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);

        //getStatusCode獲取狀態碼,is2xxSuccessful如果是狀態碼是2xx
        if(entity.getStatusCode().is2xxSuccessful()){
            //返回body
            return entity.getBody();
        }else{
            return new CommonResult<>(444, "操作失敗");
        }
    }

然後啓動eureka集羣,提供者集羣,消費者。(記得把eureka7001和提供者8001的yml文件改爲集羣環境(這個就這東西搞了我半小時,我就說怎麼負載均衡不了了,只使用了8002))
在瀏覽器輸入http://localhost/consumer/payment/getEntity/1
在這裏插入圖片描述

postForObject/postForEntity方法

    @GetMapping("/consumer/payment/createEntity")
    public CommonResult<Payment> create2(Payment payment){
        log.info("********插入的數據:" + payment);
        //postForObject分別有三個參數:請求地址,請求參數,返回的對象類型
//        return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
        ResponseEntity<CommonResult> entity = restTemplate.postForEntity(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
        if(entity.getStatusCode().is2xxSuccessful()){
            return entity.getBody();
        }else{
            return new CommonResult<>(444, "操作失敗");
        }
    }

輸入http://localhost//consumer/payment/createEntity?serial=田七,插入一條數據。
在這裏插入圖片描述
在這裏插入圖片描述

Ribbon核心組件IRUle

IRUle接口的實現類:
在這裏插入圖片描述
在這裏插入圖片描述
默認爲RoundRobinRule輪詢。

替換

在這裏插入圖片描述
Ribbon的自定義配置類不可以放在@ComponentScan所掃描的當前包下以及子包下,否則這個自定義配置類就會被所有的Ribbon客戶端共享,達不到爲指定的Ribbon定製配置,而@SpringBootApplication註解裏就有@ComponentScan註解,所以不可以放在主啓動類所在的包下。(因爲Ribbon是客戶端(消費者)這邊的,所以Ribbon的自定義配置類是在客戶端(消費者)添加,不需要在提供者或註冊中心添加)

  1. 所以Ribbon的自定義配置類不能放在springcloud包下,要在angenin包下再新建一個myrule包。
    在這裏插入圖片描述

  2. 在此包下新建MySelfRule自定義配置類

    @Configuration
    public class MySelfRule {
    
        @Bean
        public IRule myRule(){
            return new RandomRule();    //負載均衡機制改爲隨機
        }
    
    }
    
  3. 在主啓動類上添加@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
    name爲指定的服務名(服務名必須與註冊中心顯示的服務名大小寫一致)
    configuration爲指定服務使用自定義配置(自定義負載均衡機制)

  4. 啓動eurekaserver集羣,提供者集羣,消費者。

  5. 瀏覽器輸入http://localhost/consumer/payment/get/1,多次刷新實現負載均衡爲隨機。

Ribbon負載均衡——輪詢算法

RoundRobinRule原理

在這裏插入圖片描述
取餘

RoundRobinRule源碼

RoundRobinRule的核心爲choose方法:

public class RoundRobinRule extends AbstractLoadBalancerRule {
	//AtomicInteger原子整形類
    private AtomicInteger nextServerCyclicCounter;
	...
    public RoundRobinRule() {
    	//此時nextServerCyclicCounter是一個原子整形類,並且value爲0
        nextServerCyclicCounter = new AtomicInteger(0);
    }
	...
	//ILoadBalancer選擇的負載均衡機制,這裏lb爲輪詢
    public Server choose(ILoadBalancer lb, Object key) {
    	//如果傳入的lb沒有負載均衡,爲空
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        //還沒選到執行的server,並且選擇的次數沒超過10次,進行選擇server
        while (server == null && count++ < 10) {
        	//lb.getReachableServers獲取所有狀態是up的服務實例
            List<Server> reachableServers = lb.getReachableServers();
            //lb.getAllServers獲取所有服務實例
            List<Server> allServers = lb.getAllServers();
            //狀態爲up的服務實例的數量
            int upCount = reachableServers.size();
            //所有服務實例的數量
            int serverCount = allServers.size();
			
			//如果up的服務實例數量爲0或者服務實例爲0,打印日誌log.warn並返回server=null
            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
			
			//獲取到接下來server的下標
            int nextServerIndex = incrementAndGetModulo(serverCount);
            //獲取下一個server
            server = allServers.get(nextServerIndex);

			//如果
            if (server == null) {
                //線程讓步,線程會讓出CPU執行權,讓自己或者其它的線程運行。(讓步後,CPU的執行權也有可能又是當前線程)
                Thread.yield();
                //進入下次循環
                continue;
            }
			
			//獲取的server還活着並且還能工作,則返回該server
            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            //否則server改爲空
            server = null;
        }

		//選擇次數超過10次,打印日誌log.warn並返回server=null
        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }


    private int incrementAndGetModulo(int modulo) {
    	//CAS加自旋鎖
    	//CAS(Conmpare And Swap):是用於實現多線程同步的原子指令。CAS機制當中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改爲B。
    	//自旋鎖:是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖纔會退出循環。 
        for (;;) {
        	//獲取value,即0
            int current = nextServerCyclicCounter.get();
            //取餘,爲1
            int next = (current + 1) % modulo;
            //進行CAS判斷,如果此時在value的內存地址中,如果value和current相同,則爲true,返回next的值,否則就一直循環,直到結果爲true
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }
    ...
}

AtomicInteger的compareAndSet方法:

public class AtomicInteger extends Number implements java.io.Serializable {
	...
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    ...
}

手寫一個輪詢自定義配置類

8001和8002微服務改造

在8001和8002的PaymentController中加上這個方法,用於測試我們的自定義輪詢:

    @GetMapping("/payment/lb")
    public String getPaymentLB(){
        return serverPort;
    }
80訂單微服務改造
  1. 去掉ApplicationContextConfig裏restTemplate方法上的@LoadBalanced註解。

  2. 在springcloud包下新建lb.ILoadBalancer接口(自定義負載均衡機制(面向接口))

    public interface ILoadBalancer {
    
        //傳入具體實例的集合,返回選中的實例
        ServiceInstance instances(List<ServiceInstance> serviceInstance);
    
    }
    
  3. 在lb包下新建自定義ILoadBalancer接口的實現類

    @Component  //加入容器
    public class MyLB implements ILoadBalancer {
    
        //新建一個原子整形類
        private AtomicInteger atomicInteger = new AtomicInteger(0);
    
        //
        public final int getAndIncrement(){
            int current;
            int next;
            do{
                current = this.atomicInteger.get();
                //如果current是最大值,重新計算,否則加1(防止越界)
                next = current >= Integer.MAX_VALUE ? 0 : current + 1;
    
            //進行CAS判斷,如果不爲true,進行自旋
            }while (!this.atomicInteger.compareAndSet(current, next));
            System.out.println("****第幾次訪問,次數next:" + next);
    
            return next;
        }
    
        @Override
        public ServiceInstance instances(List<ServiceInstance> serviceInstance) {
            //非空判斷
            if(serviceInstance.size() <= 0){
                return null;
            }
            //進行取餘
            int index = getAndIncrement() % serviceInstance.size();
            //返回選中的服務實例
            return serviceInstance.get(index);
        }
    }
    
  4. 在OrderController添加:

        @Resource
        private ILoadBalancer iLoadBalancer;
        @Resource
        private DiscoveryClient discoveryClient;
    	
        @GetMapping("/consumer/payment/lb")
        public String getPaymentLB(){
            //獲取CLOUD-PAYMENT-SERVICE服務的所有具體實例
            List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
            if(instances == null || instances.size() <= 0){
                return null;
            }
    
            ServiceInstance serviceInstance = iLoadBalancer.instances(instances);
            URI uri = serviceInstance.getUri();
            System.out.println(uri);
    
            return restTemplate.getForObject(uri + "/payment/lb", String.class);
        }
    
  5. 啓動server集羣,提供者集羣,80消費者,然後在瀏覽器中輸入http://localhost/consumer/payment/lb,多次刷新,實現自定義輪詢。
    在這裏插入圖片描述

九、OpenFeign服務接口調用

簡介

官網文檔:https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
在這裏插入圖片描述
Feign是一個聲明式的web服務客戶端,讓編寫web服務客戶端變得非常容易,只需創建一個接口並在接口上添加註解即可

在這裏插入圖片描述
Feign與OpenFeign的區別
在這裏插入圖片描述

OpenFeign的使用(也是在消費者端)

在這裏插入圖片描述

  1. 新建模塊cloud-consumer-feign-order80

  2. pom

    <dependencies>
        <!-- openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- eureka-client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引用自己定義的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.angenin.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--熱部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
  3. yml

    server:
      port: 80
    
    
    eureka:
      client:
        register-with-eureka: false
        service-url:
          defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
    
  4. 主啓動類

    @EnableFeignClients //激活feign
    @SpringBootApplication
    public class OrderFeignMain80 {
    
        public static void main(String[] args) {
            SpringApplication.run(OrderFeignMain80.class, args);
        }
    
    }
    
  5. 在springcloud包下新建service.PaymentFeignService接口
    (業務邏輯接口+@FeignClient配置調用provider服務。)

    新建PaymentFeignService接口並新增註解@FeignClient

    //Feign封裝了Ribbon和RestTemplate,實現負載均衡和發送請求
    @Component
    @FeignClient(value = "CLOUD-PAYMENT-SERVICE")    //作爲feign的接口,找CLOUD-PAYMENT-SERVICE服務
    public interface PaymentFeignService {
    
        //直接複製8001的方法
        @GetMapping("/payment/get/{id}")
        public CommonResult getPaymentById(@PathVariable("id") Long id);
    
    }
    
  6. 在springcloud包下新建controller.OrderFeignController

    @Slf4j
    @RestController
    public class OrderFeignController {
    
        @Resource
        private PaymentFeignService paymentFeignService;
    
        @GetMapping("/consumer/payment/get/{id}")
        public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
            return paymentFeignService.getPaymentById(id);
        }
    
    }
    
  7. 按照順序啓動項目(server集羣,提供者集羣,消費者),然後在瀏覽器輸入http://localhost/consumer/payment/get/1,成功查詢到數據,並且有負載均衡(輪詢)。 在這裏插入圖片描述

總結:
在這裏插入圖片描述

OpenFeign超時控制

提供者在處理服務時用了3秒,提供者認爲花3秒是正常,而消費者只願意等1秒,1秒後,提供者會沒返回數據,消費者就會造成超時調用報錯。
所以需要雙方約定好時間,不使用默認的。

模擬超時出錯的情況

在這裏插入圖片描述

  1. 在8001的PaymentController裏添加:(模擬服務處理時間長)

        @GetMapping("/payment/feign/timeout")
        public String paymentFeignTimeout(){
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return serverPort;
        }
    
  2. 在80的PaymentFeignService中添加:

    @GetMapping("/payment/feign/timeout")
    public String paymentFeignTimeout();
    
  3. 然後在80的OrderFeignController中添加:

        @GetMapping("/consumer/payment/feign/timeout")
        public String paymentFeignTimeout(){
            //openFeign-ribbon,客戶端一般默認等待1秒
            return paymentFeignService.paymentFeignTimeout();
        }
    
  4. 啓動server集羣,提供者8001,消費者80
    http://localhost/consumer/payment/feign/timeout
    三秒後報錯。
    在這裏插入圖片描述

在這裏插入圖片描述

  1. 在80的yml中添加:

    #沒提示不管它,可以設置
    ribbon:
      #指的是建立連接後從服務器讀取到可用資源所用的時間
      ReadTimeout: 5000
      #指的是建立連接使用的時間,適用於網絡狀況正常的情況下,兩端連接所用的時間
      ConnectTimeout: 5000
    
  2. 重新訪問http://localhost/consumer/payment/feign/timeout,3秒後顯示。
    在這裏插入圖片描述

OpenFeign日誌打印功能

在這裏插入圖片描述

日誌級別

在這裏插入圖片描述

步驟

  1. 配置日誌bean
    在80的springcloud包下新建config.FeignConfig
import feign.Logger;	//不要導錯包

@Configuration
public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel(){
        //打印最詳細的日誌
        return Logger.Level.FULL;
    }

}
  1. 在80的yml文件中添加:

    #開啓日誌的feign客戶端
    logging:
      level:
        #feign日誌以什麼級別監控哪個接口
        com.angenin.springcloud.service.PaymentFeignService: debug	#寫你們自己的包名
    
  2. 啓動項目,http://localhost/consumer/payment/feign/timeout
    在這裏插入圖片描述

下一篇筆記:SpringCloud入門學習筆記(10初級部分,斷路器【Hystrix】)

學習視頻(p36-p46):https://www.bilibili.com/video/BV18E411x7eT?p=36

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