本文较大篇幅引用https://www.mrhelloworld.com/hystrix-circuit-breaker/,版权归该文章作者所有
hystrix是什么?
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比 如超时、异常等,
Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分 布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,
这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
大型项目中会出现的一些问题
典型的一个案例就是服务血崩效应 我们来看一张图:
上图是一条微服务调用链, 正常的情况我们就不必在讨论了, 我们来说一下非正常情况, 假设现在 微服务H 响应 时间过长,或者微服务H直接down机了如图:
来看下上图, 我们联想一下上图, 如果发生这种情况, 也就是说所有发给微服务D的请求 都会被卡在微服务H 那, 就会导致线程一直累计在这里, 那么其他的微服务(比如A,B,C...) 就没有可用线程了, 导致整个服务器 崩溃,这就是服务血崩。
导致服务雪崩的情况我们来总结一下,再看看怎么解决:程序BUG,数据不匹配,响应时间过长,服务不可用等等.....
针对上面的问题,我们来看看有哪些解决方案 :
服务限流
超时监控
服务熔断
服务降级
环境准备
hystrix-demo
聚合工程。SpringBoot 2.2.4.RELEASE
、Spring Cloud Hoxton.SR1
。
eureka-server
:注册中心eureka(端口8761)eureka-server02
:注册中心eureka(端口8762,两个注册中心相互注册,搭建过程省略)product-service
:商品服务,提供了/product/{id}
接口,/product/list
接口,/product/listByIds
接口order-service-rest
:订单服务,基于Ribbon
通过RestTemplate
调用商品服务order-server-feign
:订单服务,基于Feign
通过声明式服务调用商品服务
商品服务 product-service
1.创建项目
2.添加依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>product-service</artifactId> <version>1.0-SNAPSHOT</version> <!-- 继承父依赖 --> <parent> <groupId>com.example</groupId> <artifactId>hystrix-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <!-- 项目依赖 --> <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> </project>
3.配置文件
server: port: 7070 # 端口 spring: application: name: product-service # 应用名称 # 配置 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/
4.实体类
package com.example.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; }
5.编写服务
package com.example.service; import com.example.pojo.Product; import java.util.List; /** * 商品服务 */ public interface ProductService { /** * 查询商品列表 * * @return */ List<Product> selectProductList(); /** * 根据多个主键查询商品 * * @param ids * @return */ List<Product> selectProductListByIds(List<Integer> ids); /** * 根据主键查询商品 * * @param id * @return */ Product selectProductById(Integer id); }
实现类
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.stereotype.Service; import java.util.ArrayList; 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, "华为手机", 1, 5800D), new Product(2, "联想笔记本", 1, 6888D), new Product(3, "小米平板", 5, 2020D) ); } /** * 根据多个主键查询商品 * * @param ids * @return */ @Override public List<Product> selectProductListByIds(List<Integer> ids) { List<Product> products = new ArrayList<>(); ids.forEach(id -> products.add(new Product(id, "电视机" + id, 1, 5800D))); return products; } /** * 根据主键查询商品 * * @param id * @return */ @Override public Product selectProductById(Integer id) { return new Product(id, "冰箱", 1, 2666D); } }
6.控制层
package com.example.controller; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; /** * 查询商品列表 * * @return */ @GetMapping("/list") public List<Product> selectProductList() { return productService.selectProductList(); } /** * 根据多个主键查询商品 * * @param ids * @return */ @GetMapping("/listByIds") public List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids) { return productService.selectProductListByIds(ids); } /** * 根据主键查询商品 * * @param id * @return */ @GetMapping("/{id}") public Product selectProductById(@PathVariable("id") Integer id) { return productService.selectProductById(id); } }
7.启动类
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication // 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解 //@EnableEurekaClient public class ProductServiceApplication { public static void main(String[] args) { SpringApplication.run(ProductServiceApplication.class, args); } }
订单服务 order-service-rest
创建项目
添加依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>order-service-rest</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承父依赖 -->
<parent>
<groupId>com.example</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 项目依赖 -->
<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>
</project>
配置文件
server: port: 8080 # 端口 spring: application: name: order-service-rest # 应用名称 # 配置 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 com.example.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; }
order
package com.example.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 com.example.service; import com.example.pojo.Product; import java.util.List; /** * 商品管理 */ public interface ProductService { /** * 查询商品列表 * * @return */ List<Product> selectProductList(); /** * 根据多个主键查询商品 * * @param ids * @return */ List<Product> selectProductListByIds(List<Integer> ids); /** * 根据主键查询商品 * * @param id * @return */ Product selectProductById(Integer id); }
我们先使用 Ribbon 并通过 RestTemplate 来实现远程服务的调用product服务。先讲解 RestTemplate 方式的服务容错处理。
ProductServiceImpl.java
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; /** * 商品管理 */ @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 查询商品列表 * * @return */ @Override public List<Product> selectProductList() { // ResponseEntity: 封装了返回数据 return restTemplate.exchange( "http://product-service/product/list", HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() { }).getBody(); } /** * 根据多个主键查询商品 * * @param ids * @return */ @Override public List<Product> selectProductListByIds(List<Integer> ids) { StringBuffer sb = new StringBuffer(); ids.forEach(id -> sb.append("id=" + id + "&")); return restTemplate.getForObject("http://product-service/product/listByIds?" + sb.toString(), List.class); } /** * 根据主键查询商品 * * @param id * @return */ @Override public Product selectProductById(Integer id) { return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } }
OrderService.java
package com.example.service; import com.example.pojo.Order; public interface OrderService { /** * 根据主键查询订单 * * @param id * @return */ Order selectOrderById(Integer id); /** * 根据主键查询订单 * * @param id * @return */ Order queryOrderById(Integer id); /** * 根据主键查询订单 * * @param id * @return */ Order searchOrderById(Integer id); }
OrderServiceImpl.java
package com.example.service.impl; import com.example.pojo.Order; import com.example.service.OrderService; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Arrays; @Service public class OrderServiceImpl implements OrderService { @Autowired private ProductService productService; /** * 根据主键查询订单 * * @param id * @return */ @Override public Order selectOrderById(Integer id) { return new Order(id, "order-001", "中国", 22788D, productService.selectProductList()); } /** * 根据主键查询订单 * * @param id * @return */ @Override public Order queryOrderById(Integer id) { return new Order(id, "order-002", "中国", 11600D, productService.selectProductListByIds(Arrays.asList(1, 2))); } /** * 根据主键查询订单 * * @param id * @return */ @Override public Order searchOrderById(Integer id) { return new Order(id, "order-003", "中国", 2666D, Arrays.asList(productService.selectProductById(5))); } }
控制层
package com.example.controller; import com.example.pojo.Order; import com.example.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; /** * 根据主键查询订单-调用商品服务 /product/list * * @param id * @return */ @GetMapping("/{id}/product/list") public Order selectOrderById(@PathVariable("id") Integer id) { return orderService.selectOrderById(id); } /** * 根据主键查询订单-调用商品服务 /product/listByIds * * @param id * @return */ @GetMapping("/{id}/product/listByIds") public Order queryOrderById(@PathVariable("id") Integer id) { return orderService.queryOrderById(id); } /** * 根据主键查询订单-调用商品服务 /product/{id} * * @param id * @return */ @GetMapping("/{id}/product") public Order searchOrderById(@PathVariable("id") Integer id) { return orderService.searchOrderById(id); } }
启动类
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class OrderServiceRestApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(OrderServiceRestApplication.class, args); } }
服务降级
我们先来解释一下降级,降级是当我们的某个微服务响应时间过长,或者不可用了,讲白了也就是那个微服务调用不了了,我们不能把错误信息返回出来,或者让他一直卡在那里,所以要在准备一个对应的策略(一个方法)当发生 这种问题的时候我们直接调用这个方法来快速返回这个请求,不让他一直卡在那 。
讲了这么多,我们来看看具体怎么操作: 我们刚刚说了某个微服务调用不了了要做降级,也就是说,要在调用方做降级(不然那个微服务都down掉了再做 降级也没什么意义了) 比如说我们 order调用product那么就在order做降级
添加依赖
服务消费者 pom.xml 添加 hystrix 依赖。
<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
业务层
服务消费者业务层代码添加服务降级规则。
然后在我们的需要做服务降级方法上面加入注解@HystrixCommand(fallbackMethod就是我们刚刚说的方法的名字)
import com.example.pojo.Product; import com.example.service.ProductService; 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; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * 商品管理 */ @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 根据主键查询商品 * * @param id * @return */ // 声明需要服务容错的方法 // 服务降级 @HystrixCommand(fallbackMethod = "selectProductByIdFallback") @Override public Product selectProductById(Integer id) { return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } // 托底数据 private Product selectProductByIdFallback(Integer id) { return new Product(id, "托底数据", 1, 2666D); } }
启动类
服务消费者启动类开启熔断器注解。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; // 开启熔断器注解 2 选 1,@EnableHystrix 封装了 @EnableCircuitBreaker // @EnableHystrix @EnableCircuitBreaker @SpringBootApplication public class OrderServiceRestApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(OrderServiceRestApplication.class, args); } }
@EnableHystrix 或者@EnableCircuitBreaker(他们之间是一个继承关系,2个注解所描述的内容是 完全一样的)
测试
访问:http://localhost:9090/order/3/product 结果如下:
关闭服务提供者,再次访问:http://localhost:9090/order/3/product 结果如下:
通过结果可以看到,服务降级已经启用。当 Provider 不可用时返回托底数据,直到服务可用快速恢复。
我们在服务提供者的该方法上添加2秒延时,
@GetMapping("/{id}") public Product selectProductById(@PathVariable("id") Integer id) throws InterruptedException { Thread.sleep(2000); return productService.selectProductById(id); }
再重启服务提供者product-service测试一次
可能有些同学有疑问, 我这里什么都没干, 就让他休眠了一下 , 怎么就知道我这里超时了呢? 因为hystrix他有默认的超时监听,当你这个请求默认超过了1秒钟就会超时
当然,这个可以配置的,至于怎么配 置,待会儿我会把一些配置统一列出来
讲了这么多, 这个降级到底有什么用呢?
第一, 他可以监听你的请求有没有超时,
第二,报错了他这里直接截断了没有让请求一直卡在这里
其实降级还有一个好处, 就是当你的系统马上迎来大量的并发(双十一秒杀这种 或者促销活动) 这时候如果发现系 统马上承载不了这么大的并发时, 可以考虑先关闭一些不重要的微服务(在降级方法里面返回一个比较友好的信 息),
把资源让给主微服务,总结一下就是 整体资源快不够了,忍痛将某些服务先关掉,待渡过难关,再开启回来。
服务熔断
服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。
其实熔断,就好像我们生活中的跳闸一样, 比如说你的电路出故障了,为了防止出现 大型事故 这里直接切断了你的电源以免意外继续发生, 把这个概念放在我们程序上也是如此, 当一个微服务调用多 次出现问题时(默认是10秒内20次当然 这个也能配置),hystrix就会采取熔断机制,不再继续调用你的方法(会 在默认5秒钟内和电器短路一样,5秒钟后会试探性的先关闭熔断机制,但是如果这时候再失败一次{之前是20次} 那么又会重新进行熔断) 而是直接调用降级方法,这样就一定程度上避免了服务雪崩的问题
业务层
服务消费者业务层代码添加服务熔断规则。
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * 商品管理 */ @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 根据主键查询商品 * * @param id * @return */ // 声明需要服务容错的方法 // 服务熔断 @HystrixCommand(commandProperties = { // 当请求符合熔断条件触发 fallbackMethod 默认 20 个 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"), // 请求错误率大于 50% 就启动熔断器,然后 for 循环发起重试请求,当请求符合熔断条件触发 fallbackMethod @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"), // 熔断多少秒后去重试请求,默认 5s @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000"), }, fallbackMethod = "selectProductByIdFallback") @Override public Product selectProductById(Integer id) { System.out.println("-----selectProductById-----" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)); // 模拟查询主键为 1 的商品信息会导致异常 if (1 == id) throw new RuntimeException("查询主键为 1 的商品信息导致异常"); return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } // 托底数据 private Product selectProductByIdFallback(Integer id) { return new Product(id, "托底数据", 1, 2666D); } }
启动类
服务消费者启动类开启熔断器注解。
这个东西光笔记不太好测试,只能你们自己去测试了
Feign 整合hystrix:
环境准备
我们在父工程下再创建一个 Consumer 项目这次是基于 Feign 实现声明式服务调用。
添加依赖
服务提供者添加 openfeign
依赖,openfeign 默认集成了 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>order-service-feign</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承父依赖 -->
<parent>
<groupId>com.example</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 项目依赖 -->
<dependencies>
<!-- netflix eureka client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring cloud openfeign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</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>
</project>
配置文件
服务提供者需要开启 Feign 对于 Hystrix 的支持。
server: port: 9091 # 端口 spring: application: name: order-service-feign # 应用名称 # 配置 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/ # Feign 开启 Hystrix 支持 feign: hystrix: enabled: true
实体类
package com.example.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; }
Order.java
package com.example.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 com.example.service; import com.example.fallback.ProductServiceFallback; import com.example.pojo.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; // 声明需要调用的服务和服务熔断处理类 @FeignClient(value = "product-service", fallback = ProductServiceFallback.class) public interface ProductService { /** * 查询商品列表 * * @return */ @GetMapping("/product/list") List<Product> selectProductList(); /** * 根据多个主键查询商品 * * @param ids * @return */ @GetMapping("/product/listByIds") List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids); /** * 根据主键查询商品 * * @param id * @return */ @GetMapping("/product/{id}") Product selectProductById(@PathVariable("id") Integer id); }
OrderService.java
package com.example.service; import com.example.pojo.Order; public interface OrderService { /** * 根据主键查询订单 * * @param id * @return */ Order selectOrderById(Integer id); /** * 根据主键查询订单 * * @param id * @return */ Order queryOrderById(Integer id); /** * 根据主键查询订单 * * @param id * @return */ Order searchOrderById(Integer id); }
OrderServiceImpl.java
package com.example.service.impl; import com.example.pojo.Order; import com.example.service.OrderService; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Arrays; @Service public class OrderServiceImpl implements OrderService { @Autowired private ProductService productService; /** * 根据主键查询订单 * * @param id * @return */ @Override public Order selectOrderById(Integer id) { return new Order(id, "order-001", "中国", 22788D, productService.selectProductList()); } /** * 根据主键查询订单 * * @param id * @return */ @Override public Order queryOrderById(Integer id) { return new Order(id, "order-002", "中国", 11600D, productService.selectProductListByIds(Arrays.asList(1, 2))); } /** * 根据主键查询订单 * * @param id * @return */ @Override public Order searchOrderById(Integer id) { return new Order(id, "order-003", "中国", 2666D, Arrays.asList(productService.selectProductById(5))); } }
熔断降级
ProductServiceFallback.java
package com.example.fallback; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 服务熔断降级处理 */ @Component public class ProductServiceFallback implements ProductService { // 查询商品列表接口的托底数据 @Override public List<Product> selectProductList() { return Arrays.asList( new Product(1, "托底数据-华为手机", 1, 5800D), new Product(2, "托底数据-联想笔记本", 1, 6888D), new Product(3, "托底数据-小米平板", 5, 2020D) ); } // 根据多个主键查询商品接口的托底数据 @Override public List<Product> selectProductListByIds(List<Integer> ids) { List<Product> products = new ArrayList<>(); ids.forEach(id -> products.add(new Product(id, "托底数据-电视机" + id, 1, 5800D))); return products; } // 根据主键查询商品接口的托底数据 @Override public Product selectProductById(Integer id) { return new Product(id, "托底数据", 1, 2666D); } }
控制层
package com.example.controller; import com.example.pojo.Order; import com.example.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; /** * 根据主键查询订单-调用商品服务 /product/list * * @param id * @return */ @GetMapping("/{id}/product/list") public Order selectOrderById(@PathVariable("id") Integer id) { return orderService.selectOrderById(id); } /** * 根据主键查询订单-调用商品服务 /product/listByIds * * @param id * @return */ @GetMapping("/{id}/product/listByIds") public Order queryOrderById(@PathVariable("id") Integer id) { return orderService.queryOrderById(id); } /** * 根据主键查询订单-调用商品服务 /product/{id} * * @param id * @return */ @GetMapping("/{id}/product") public Order searchOrderById(@PathVariable("id") Integer id) { return orderService.searchOrderById(id); } }
启动类
服务消费者启动类开启 @EnableFeignClients
注解。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication // 开启 FeignClients 注解 @EnableFeignClients public class OrderServiceFeignApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceFeignApplication.class, args); } }
测试
http://localhost:9091/order/5/product
正常显示页面
添加2秒睡眠时间,再次请求
我们已经可以通过 Feign 实现服务降级处理,
但是服务不可用时如果我们想要捕获异常信息该如何实现?接下来一起学习一下
捕获服务异常
消费服务
通过 fallbackFactory
属性声明服务熔断降级处理类。
package com.example.service; import com.example.fallback.ProductServiceFallbackFactory; import com.example.pojo.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; // 声明需要调用的服务和服务熔断处理类 @FeignClient(value = "product-service", fallbackFactory = ProductServiceFallbackFactory.class) public interface ProductService { /** * 查询商品列表 * * @return */ @GetMapping("/product/list") List<Product> selectProductList(); /** * 根据多个主键查询商品 * * @param ids * @return */ @GetMapping("/product/listByIds") List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids); /** * 根据主键查询商品 * * @param id * @return */ @GetMapping("/product/{id}") Product selectProductById(@PathVariable("id") Integer id); }
熔断降级
实现 FallbackFactory
接口。
package com.example.fallback; import com.example.pojo.Product; import com.example.service.ProductService; import feign.hystrix.FallbackFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 服务熔断降级处理可以捕获异常 */ @Component public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> { // 获取日志,在需要捕获异常的方法中进行处理 Logger logger = LoggerFactory.getLogger(ProductServiceFallbackFactory.class); @Override public ProductService create(Throwable throwable) { return new ProductService() { // 查询商品列表接口的托底数据 @Override public List<Product> selectProductList() { logger.error("product-service 服务的 selectProductList 方法出现异常,异常信息如下:" + throwable); return Arrays.asList( new Product(1, "托底数据-华为手机", 1, 5800D), new Product(2, "托底数据-联想笔记本", 1, 6888D), new Product(3, "托底数据-小米平板", 5, 2020D) ); } // 根据多个主键查询商品接口的托底数据 @Override public List<Product> selectProductListByIds(List<Integer> ids) { logger.error("product-service 服务的 selectProductListByIds 方法出现异常,异常信息如下:" + throwable); List<Product> products = new ArrayList<>(); ids.forEach(id -> products.add(new Product(id, "托底数据-电视机" + id, 1, 5800D))); return products; } // 根据主键查询商品接口的托底数据 @Override public Product selectProductById(Integer id) { logger.error("product-service 服务的 selectProductById 方法出现异常,异常信息如下:" + throwable); return new Product(id, "托底数据", 1, 2666D); } }; } }
测试
访问:http://localhost:9091/order/1/product/list 结果如下:
控制台打印结果:
hystrix相关配置:
hystrix.command.default和hystrix.threadpool.default中的default为默认CommandKey
Command Properties
Execution相关的属性的配置:
hystrix.command.default.execution.isolation.strategy 隔离策略,默认是Thread, 可选Thread|Semaphore
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令执行超时时间,默认1000ms
hystrix.command.default.execution.timeout.enabled 执行是否启用超时,默认启用true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout 发生超时是是否中断,默认true
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大并发请求数,默认10,该参数当使用ExecutionIsolationStrategy.SEMAPHORE策略时才有效。如果达到最大并发请求数,请求会被拒绝。理论上选择semaphore size的原则和选择thread size一致,但选用semaphore时每次执行的单元要比较小且执行速度快(ms级别),否则的话应该用thread。
semaphore应该占整个容器(tomcat)的线程池的一小部分。
Fallback相关的属性
这些参数可以应用于Hystrix的THREAD和SEMAPHORE策略
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 如果并发数达到该设置值,请求会被拒绝和抛出异常并且fallback不会被调用。默认10
hystrix.command.default.fallback.enabled 当执行失败或者请求被拒绝,是否会尝试调用hystrixCommand.getFallback() 。默认true
Circuit Breaker相关的属性
hystrix.command.default.circuitBreaker.enabled 用来跟踪circuit的健康性,如果未达标则让request短路。默认true
hystrix.command.default.circuitBreaker.requestVolumeThreshold 一个rolling window内最小的请求数。如果设为20,那么当一个rolling window的时间内(比如说1个rolling window是10秒)收到19个请求,即使19个请求都失败,也不会触发circuit break。默认20
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds 触发短路的时间值,当该值设为5000时,则当触发circuit break后的5000毫秒内都会拒绝request,也就是5000毫秒后才会关闭circuit。默认5000
hystrix.command.default.circuitBreaker.errorThresholdPercentage错误比率阀值,如果错误率>=该值,circuit会被打开,并短路所有请求触发fallback。默认50
hystrix.command.default.circuitBreaker.forceOpen 强制打开熔断器,如果打开这个开关,那么拒绝所有request,默认false
hystrix.command.default.circuitBreaker.forceClosed 强制关闭熔断器 如果这个开关打开,circuit将一直关闭且忽略circuitBreaker.errorThresholdPercentage
Metrics相关参数
hystrix.command.default.metrics.rollingStats.timeInMilliseconds 设置统计的时间窗口值的,毫秒值,circuit break 的打开会根据1个rolling window的统计来计算。若rolling window被设为10000毫秒,则rolling window会被分成n个buckets,每个bucket包含success,failure,timeout,rejection的次数的统计信息。默认10000
hystrix.command.default.metrics.rollingStats.numBuckets 设置一个rolling window被划分的数量,若numBuckets=10,rolling window=10000,那么一个bucket的时间即1秒。必须符合rolling window % numberBuckets == 0。默认10
hystrix.command.default.metrics.rollingPercentile.enabled 执行时是否enable指标的计算和跟踪,默认true
hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds 设置rolling percentile window的时间,默认60000
hystrix.command.default.metrics.rollingPercentile.numBuckets 设置rolling percentile window的numberBuckets。逻辑同上。默认6
hystrix.command.default.metrics.rollingPercentile.bucketSize 如果bucket size=100,window=10s,若这10s里有500次执行,只有最后100次执行会被统计到bucket里去。增加该值会增加内存开销以及排序的开销。默认100
hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds 记录health 快照(用来统计成功和错误绿)的间隔,默认500ms
Request Context 相关参数
hystrix.command.default.requestCache.enabled 默认true,需要重载getCacheKey(),返回null时不缓存
hystrix.command.default.requestLog.enabled 记录日志到HystrixRequestLog,默认true
Collapser Properties 相关参数
hystrix.collapser.default.maxRequestsInBatch 单次批处理的最大请求数,达到该数量触发批处理,默认Integer.MAX_VALUE
hystrix.collapser.default.timerDelayInMilliseconds 触发批处理的延迟,也可以为创建批处理的时间+该值,默认10
hystrix.collapser.default.requestCache.enabled 是否对HystrixCollapser.execute() and HystrixCollapser.queue()的cache,默认true
ThreadPool 相关参数
线程数默认值10适用于大部分情况(有时可以设置得更小),如果需要设置得更大,那有个基本得公式可以follow:
requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room
每秒最大支撑的请求数 (99%平均响应时间 + 缓存值)
比如:每秒能处理1000个请求,99%的请求响应时间是60ms,那么公式是:
(0.060+0.012)
基本得原则时保持线程池尽可能小,他主要是为了释放压力,防止资源被阻塞。
当一切都是正常的时候,线程池一般仅会有1到2个线程激活来提供服务
hystrix.threadpool.default.coreSize 并发执行的最大线程数,默认10
hystrix.threadpool.default.maxQueueSize BlockingQueue的最大队列数,当设为-1,会使用SynchronousQueue,值为正时使用LinkedBlcokingQueue。该设置只会在初始化时有效,之后不能修改threadpool的queue size,除非reinitialising thread executor。默认-1。
hystrix.threadpool.default.queueSizeRejectionThreshold 即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝。因为maxQueueSize不能被动态修改,这个参数将允许我们动态设置该值。if maxQueueSize == -1,该字段将不起作用
hystrix.threadpool.default.keepAliveTimeMinutes 如果corePoolSize和maxPoolSize设成一样(默认实现)该设置无效。如果通过plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定义实现,该设置才有用,默认1.
hystrix.threadpool.default.metrics.rollingStats.timeInMilliseconds 线程池统计指标的时间,默认10000
hystrix.threadpool.default.metrics.rollingStats.numBuckets 将rolling window划分为n个buckets,默认10
至此 Hystrix 服务容错知识点就讲解结束了。