五、服務降級和熔斷
本節將引入熔斷器Hystrix。首先來看一下什麼是雪崩效應:
服務雪崩效應是一種因“服務提供者的不可用”(原因)導致“服務調用者不可用”(結果),並將不可用逐漸放大的現象
舉個栗子:假設,order-server請求goods-server時,由於某些原因,goods-server返回時間變爲無限長,此時order-server也將一直等待響應,當order-server堆積了大量處於等待狀態的請求時,order-server服務器終將撒手人寰。
這時就需要Hystrix登場,在服務出現問題時它會及時的去切斷有問題的服務器,保證系統的基本秩序。
開刀:
訂單服務加入hystrix依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mujio</groupId>
<artifactId>mall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mall</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 熔斷所需依賴 start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<!-- 熔斷所需依賴 end -->
<!-- 添加web服務依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL數據庫依賴 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<!-- mybatis依賴 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- springboot項目需要的依賴,創建項目自動添加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
啓動類OrderServerApplication加上開啓hystrix的註解@EnableHystrix:
最後需要改造一下訂單服務中的GoodService:
package com.mujio.orderserver.service;
import com.mujio.orderserver.entity.Goods;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class GoodService {
//利用RestTemplate請求接口
@Autowired
private RestTemplate restTemplate;
//啓用負載均衡後,restTemplate自動選擇訪問哪個服務
@HystrixCommand(fallbackMethod = "getGoodsServiceOffline")
public Goods getGoods(int id){
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
return restTemplate.getForObject(url,Goods.class);
}
//請求失敗後,調用fallbackMethod指定的方法
public Goods getGoodsServiceOffline(int id){
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能獲取到商品信息");
goods.setPrice("");
return goods;
}
/*
// @Autowired
// private DiscoveryClient discoveryClient;
public Goods getGoods(int id) {
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
System.out.println(url);
return restTemplate.getForObject(url,Goods.class);
}
*/
/* public Goods getGoods(int id) {
String service = "goods-server";
List<ServiceInstance> instances = discoveryClient.getInstances(service);
if (instances.isEmpty()) {
return null;
}
//instances.get(0)使用獲取到的第一個服務
String url = "http://" + instances.get(0).getHost() + ":" + instances.get(0).getPort() + "/goods/" + id;
return restTemplate.getForObject(url, Goods.class);
}*/
/* public Goods getGoods(int id){
String url = "http://localhost:8000/goods/" + id;
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能獲取到商品信息");
goods.setPrice("");
Goods g;
try {
g = restTemplate.getForObject(url, Goods.class);
} finally {
g = goods;
return g;
}
}*/
}
重啓服務,訪問http://localhost:9000/order/4並多次刷新,信息正常;停止goods-server01,再次訪問,多次刷新:
可以看到,剛停止時goods-server01時,返回速度明顯慢了一些,並且在關閉後訪問會顯示“未能獲取到商品信息”,但是多次訪問刷新之後又會發現信息又正常了。這是因爲熔斷器發揮了作用,在服務出現故障的時候調用了fallbackMethod,並且及時切斷該服務,使得再刷新後數據恢復正常。
目前我們已經初步實現了分佈式的目的,但是仔細一想,這個系統還是有問題:eureka要是崩了咋辦?開啓兩個order-server有個錘子用?訪問的時候不還是得區分端口?還有這個服務調用藏在service中會不會有點太深了?
當然哈,只是覺得這一節篇幅有點短了,不如現在強行加點:
Feign:
feign是聲明式的web service客戶端,Spring Cloud集成了Ribbon和Eureka,可在使用Feign提供負載均衡的http客戶端。
瞧瞧這說法,“可使用Feign提供負載均衡的http客戶端”,怎麼提供呢?一步一步來:
加依賴(負載均衡在order-server中,當然也是給order-server的pom.xml加了):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!-- springboot版本改爲2.1.1.RELEASE -->
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mujio</groupId>
<artifactId>order-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RC2</spring-cloud.version>
</properties>
<dependencies>
<!-- Feign所需依賴 start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 根據spring cloud的版本選擇,早期的使用下面的依賴 -->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>-->
<!-- Feign所需依賴 end -->
<!-- 熔斷所需依賴 start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<!-- 熔斷所需依賴 end -->
<!-- 添加web服務依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL數據庫依賴 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<!-- mybatis依賴 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- springboot項目需要的依賴,創建項目自動添加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
<!-- 添加eureka依賴 start -->
<!-- eureka客戶端依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<exclusions>
<!-- eureka的數據轉換,自動將數據結果轉爲xml格式,我們不需要xml格式的結果所以需要排除 -->
<exclusion>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!-- 指定springcloud依賴版本 -->
<dependencyManagement>
<dependencies>
<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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!-- 添加eureka依賴 end -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
需要注意的是,本文中使用的spring cloud的版本是:Greenwich.RC2,加入feign時如出現衝突,比其早的版本,請考慮使用spring-cloud-starter-feign
繼續改造,首先是啓動類加註解@EnableFeignClients:
package com.mujio.orderserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServerApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
創建個使用feign的接口:
這裏有個@FeignClient(value = “goods-server”),它會根據value值自動轉換成對應的服務,所以要注意商品服務的應用名要與之一致。
它有點類似與controller根據請求路徑調用對應service方法,所以我是放在了controller包中。
再次改造GoodService(刀妹??):
package com.mujio.orderserver.service;
import com.mujio.orderserver.controller.GoodsFeignClient;
import com.mujio.orderserver.entity.Goods;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class GoodService {
// 引入feign
@Autowired
private GoodsFeignClient goodsFeignClient;
//啓用負載均衡後,有restTemplate自己去選擇訪問那個服務
@HystrixCommand(fallbackMethod = "getGoodsServiceOffline")
public Goods getGoods(int id){
return goodsFeignClient.getGoods(id);
}
public Goods getGoodsServiceOffline(int id){
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能獲取到商品信息");
goods.setPrice("");
return goods;
}
/*
@Autowired
private RestTemplate restTemplate;
//啓用負載均衡後,restTemplate自動選擇訪問哪個服務
@HystrixCommand(fallbackMethod = "getGoodsServiceOffline")
public Goods getGoods(int id){
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
return restTemplate.getForObject(url,Goods.class);
}
//請求失敗後,調用fallbackMethod指定的方法
public Goods getGoodsServiceOffline(int id){
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能獲取到商品信息");
goods.setPrice("");
return goods;
}
*/
/*
// @Autowired
// private DiscoveryClient discoveryClient;
public Goods getGoods(int id) {
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
System.out.println(url);
return restTemplate.getForObject(url,Goods.class);
}
*/
/* public Goods getGoods(int id) {
String service = "goods-server";
List<ServiceInstance> instances = discoveryClient.getInstances(service);
if (instances.isEmpty()) {
return null;
}
//instances.get(0)使用獲取到的第一個服務
String url = "http://" + instances.get(0).getHost() + ":" + instances.get(0).getPort() + "/goods/" + id;
return restTemplate.getForObject(url, Goods.class);
}*/
/* public Goods getGoods(int id){
String url = "http://localhost:8000/goods/" + id;
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能獲取到商品信息");
goods.setPrice("");
Goods g;
try {
g = restTemplate.getForObject(url, Goods.class);
} finally {
g = goods;
return g;
}
}*/
}
搞完重啓服務,測試啥的自然不用說了。下一節將引入zuul,順便解決另外兩個問題。(主要是因爲本節代碼已經上傳了,嘿嘿)