Hystrix 源自 Netflix 团队于 2011 年开始研发。2012年 Hystrix 不断发展和成熟,Netflix 内部的许多团队都采用了它。如今,每天在 Netflix 上通过 Hystrix 执行数百亿个线程隔离和数千亿个信号量隔离的调用。极大地提高了系统的稳定性。
在分布式环境中,不可避免地会有许多服务依赖项中的某些服务失败而导致 雪崩效应 。Hystrix 是一个库,可通过添加等待时间容限和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的访问点,停止服务之间的级联故障并提供后备选项来实现此目的,所有这些都可以提高系统的整体稳定性。
在微服务架构中,一个请求需要调用多个服务是非常常见的。如客户端访问 A 服务,而 A 服务需要调用 B 服务,B 服务需要调用 C 服务,由于网络原因或者自身的原因,如果 B 服务或者 C 服务不能及时响应,A 服务将处于阻塞状态,直到 B 服务 C 服务响应。此时若有大量的请求涌入,容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。以下图示完美解释了什么是雪崩效应。
当一切服务正常时,请求看起来是这样的:
当其中一个服务有延迟时,它可能阻塞整个用户请求:
在高并发的情况下,一个服务的延迟可能导致所有服务器上的所有资源在数秒内饱和。比起服务故障,更糟糕的是这些应用程序还可能导致服务之间的延迟增加,从而备份队列,线程和其他系统资源,从而导致整个系统出现更多级联故障。
造成雪崩的原因可以归结为以下三点:
最终的结果就是:一个服务不可用,导致一系列服务的不可用。
雪崩是系统中的蝴蝶效应导致,其发生的原因多种多样,从源头我们无法完全杜绝雪崩的发生,但是雪崩的根本原因来源于服务之间的强依赖,所以我们可以提前评估做好服务容错。解决方案大概可以分为以下几种:
eureka-server
:注册中心 eureka-server02
:注册中心 product-service
:商品服务,提供了 /product/{id}
接口, /product/list
接口, /product/listByIds
接口 order-service-rest
:订单服务,基于 Ribbon
通过 RestTemplate
调用商品服务 order-server-feign
:订单服务,基于 Feign
通过声明式服务调用商品服务 服务提供者接口添加 Thread.sleep(2000)
,模拟服务处理时长。
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() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } 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); } }
服务消费者降低 Tomcat 最大线程数方便模拟高并发。
server: port: 8080 tomcat: max-threads: 10 # 降低最大线程数方便模拟高并发
Apache JMeter 应用程序是开源软件,100% 纯 Java 应用而设计的负载测试功能行为和测量性能。它最初是为测试 Web 应用程序而设计的,但此后已扩展到其他测试功能。
Apache JMeter 可用于测试静态和动态资源,Web 动态应用程序的性能。它可用于模拟服务器,服务器组,网络或对象上的繁重负载,以测试其强度或分析不同负载类型下的整体性能。
官网: https://jmeter.apache.org/ 本文安装 Windows 版本。
解压 apache-jmeter-5.2.1.zip
,进入 bin
目录运行 jmeter.bat
即可。不过运行之前我们先来修改一下配置文件,方便大家更友好的使用。
进入 bin
目录编辑 jmeter.properties
文件,修改 37 行和 1085 行两处代码(不同的电脑可能行数不一致,不过上下差距不大)。
language=zh_CN sampleresult.default.encoding=UTF-8
#language=en language=zh_CN #sampleresult.default.encoding=ISO-8859-1 sampleresult.default.encoding=UTF-8
运行 bin/jmeter.bat
文件,界面显示如下。
大家可以通过 选项
→ 外观
选择自己喜欢的界面风格。
HTTP 请求配置为服务消费者的 http://localhost:9090/order/1/product/list
我们可以添加结果数来查看请求响应的结果数据。
下图是执行请求以后所显示的效果。
如下图所示,点击启动按钮即可开始执行请求。STOP 按钮则为停止请求。
浏览器请求 http://localhost:9090/order/1/product 统计耗时如下。请求耗时: 235ms
通过 JMeter 开启 50 线程循环 50 次请求服务消费者 http://localhost:9090/order/1/product/list 然后浏览器再次请求 http://localhost:9090/order/1/product 统计耗时如下。请求耗时: 9.12s
通过以上测试我们可以发现, /product/list
服务如果出现故障或延迟整个系统的资源会被耗尽从而导致影响其他服务的正常使用,这种情况在微服务项目中是非常常见的,所以我们需要对服务做出容错处理。接下来我们就一个个学习服务容错的解决方案。
Hystrix 为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的 URL 没有变化,那么 Hystrix 不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。
Hystrix 自带缓存有两个缺点:
本文使用 Spring 的缓存集成方案,NoSql 使用 Redis 来实现,Redis 使用的是 5.0.7 版本。
服务消费者 pom.xml 添加 redis 和 commons-pool2 依赖。
<!-- spring boot data redis 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- commons-pool2 对象池依赖 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
服务消费者 application.yml 配置 Redis 缓存。
spring: # redis 缓存 redis: timeout: 10000 # 连接超时时间 host: 192.168.10.101 # Redis服务器地址 port: 6379 # Redis服务器端口 password: root # Redis服务器密码 database: 0 # 选择哪个库,默认0库 lettuce: pool: max-active: 1024 # 最大连接数,默认 8 max-wait: 10000 # 最大连接阻塞等待时间,单位毫秒,默认 -1 max-idle: 200 # 最大空闲连接,默认 8 min-idle: 5 # 最小空闲连接,默认 0
添加 Redis 配置类重写序列化规则。
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; /** * Redis 配置类 */ @Configuration public class RedisConfig { // 重写 RedisTemplate 序列化 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); // 为 String 类型 key 设置序列化器 template.setKeySerializer(new StringRedisSerializer()); // 为 String 类型 value 设置序列化器 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 为 Hash 类型 key 设置序列化器 template.setHashKeySerializer(new StringRedisSerializer()); // 为 Hash 类型 value 设置序列化器 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); return template; } // 重写 Cache 序列化 @Bean public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory()); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() // 设置默认过期时间 30 min .entryTtl(Duration.ofMinutes(30)) // 设置 key 和 value 的序列化 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getKeySerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } }
服务消费者启动类开启缓存注解
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; // 开启缓存注解 @EnableCaching @SpringBootApplication public class OrderServiceRestApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(OrderServiceRestApplication.class, args); } }
服务消费者业务层代码添加缓存规则。
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.cache.annotation.Cacheable; 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 */ @Cacheable(cacheNames = "orderService:product:list") @Override public List<Product> selectProductList() { // ResponseEntity: 封装了返回数据 return restTemplate.exchange( "http://product-service/product/list", HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() { }).getBody(); } /** * 根据主键查询商品 * * @param id * @return */ @Cacheable(cacheNames = "orderService:product:single", key = "#id") @Override public Product selectProductById(Integer id) { return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } }
为了方便查看效果我们在服务提供者对应接口中添加打印语句。
访问: http://localhost:9090/order/1/product/list 和 http://localhost:9090/order/1/product 效果如下。
当我们请求相同服务时,服务提供者也不再打印语句说明服务消费者的请求直接获取了缓存的数据。
JMeter 开启 50 线程循环 50 次请求 http://localhost:9090/order/1/product/list
浏览器请求 http://localhost:9090/order/1/product,结果如下:
从结果可以看出请求缓存已解决之前服务响应速度过慢的问题。
在微服务架构中,我们将一个项目拆分成很多个独立的模块,这些独立的模块通过远程调用来互相配合工作,但是, 在高并发情况下 ,通信次数的增加会导致总的通信时间增加,同时, 线程池的资源也是有限的 ,高并发环境会导致有 大量的线程处于等待状态 ,进而导致 响应延迟 ,为了解决这些问题,我们需要来了解 Hystrix 的请求合并。
设置请求合并之后,本来一个请求可能 5ms 就搞定了,但是现在必须再等 10ms 看看还有没有其他的请求一起,这样一个请求的耗时就从 5ms 增加到 15ms 了。
如果我们要发起的命令本身就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间消耗就显得微不足道了,另外高并发也是请求合并的一个非常重要的场景。
服务消费者 pom.xml 添加 hystrix 依赖。
<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
服务消费者业务层代码添加请求合并规则。
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; 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; import java.util.concurrent.Future; @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 根据多个主键查询商品 * * @param ids * @return */ // 声明需要服务容错的方法 @HystrixCommand @Override public List<Product> selectProductListByIds(List<Integer> ids) { System.out.println("-----orderService-----selectProductListByIds-----"); 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 */ // 处理请求合并的方法一定要支持异步,返回值必须是 Future<T> // 合并请求 @HystrixCollapser(batchMethod = "selectProductListByIds", // 合并请求方法 scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, // 请求方式 collapserProperties = { // 间隔多久的请求会进行合并,默认 10ms @HystrixProperty(name = "timerDelayInMilliseconds", value = "20"), // 批处理之前,批处理中允许的最大请求数 @HystrixProperty(name = "maxRequestsInBatch", value = "200") }) @Override public Future<Product> selectProductById(Integer id) { System.out.println("-----orderService-----selectProductById-----"); return null; } }
@HystrixCollapser
注解各项参数说明如下:
服务消费者模拟同一时间用户发起多个请求。
package com.example.service.impl; import com.example.pojo.Order; import com.example.pojo.Product; 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; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @Service public class OrderServiceImpl implements OrderService { @Autowired private ProductService productService; /** * 根据主键查询订单 * * @param id * @return */ @Override public Order searchOrderById(Integer id) { // 模拟同一时间用户发起多个请求。 Future<Product> p1 = productService.selectProductById(1); Future<Product> p2 = productService.selectProductById(2); Future<Product> p3 = productService.selectProductById(3); Future<Product> p4 = productService.selectProductById(4); Future<Product> p5 = productService.selectProductById(5); try { System.out.println(p1.get()); System.out.println(p2.get()); System.out.println(p3.get()); System.out.println(p4.get()); System.out.println(p5.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return new Order(id, "order-003", "中国", 29000D, null); } }
服务消费者启动类开启熔断器注解。
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); } }
访问: http://localhost:9090/order/1/product 控制台打印结果如下:
-----orderService-----selectProductListByIds----- {id=1, productName=电视机1, productNum=1, productPrice=5800.0} {id=2, productName=电视机2, productNum=1, productPrice=5800.0} {id=3, productName=电视机3, productNum=1, productPrice=5800.0} {id=4, productName=电视机4, productNum=1, productPrice=5800.0} {id=5, productName=电视机5, productNum=1, productPrice=5800.0}
根据结果得知,请求本来调用的是单个商品查询,请求合并以后只请求了一次批处理查询。
️ 以下为本人录制的其中一段,获取全部视频需联系本人。
没有线程池隔离的项目所有接口都运行在一个 ThreadPool
中,当某一个接口压力过大或者出现故障时,会导致资源耗尽从而影响到其他接口的调用而引发服务雪崩效应。我们在模拟高并发场景时也演示了该效果。
通过每次都开启一个单独线程运行。它的隔离是通过线程池,即每个隔离粒度都是个线程池,互相不干扰。线程池隔离方式,等于多了一层的保护措施,可以通过 hytrix 直接设置超时,超时后直接返回。
服务消费者 pom.xml 添加 hystrix 依赖。
<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
服务消费者业务层代码添加线程隔离规则。
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 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 */ // 声明需要服务容错的方法 // 线程池隔离 @HystrixCommand(groupKey = "order-productService-listPool",// 服务名称,相同名称使用同一个线程池 commandKey = "selectProductList",// 接口名称,默认为方法名 threadPoolKey = "order-productService-listPool",// 线程池名称,相同名称使用同一个线程池 commandProperties = { // 超时时间,默认 1000ms @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") }, threadPoolProperties = { // 线程池大小 @HystrixProperty(name = "coreSize", value = "6"), // 队列等待阈值(最大队列长度,默认 -1) @HystrixProperty(name = "maxQueueSize", value = "100"), // 线程存活时间,默认 1min @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"), // 超出队列等待阈值执行拒绝策略 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100") }, fallbackMethod = "selectProductListFallback") @Override public List<Product> selectProductList() { System.out.println(Thread.currentThread().getName() + "-----selectProductList-----"); // ResponseEntity: 封装了返回数据 return restTemplate.exchange( "http://product-service/product/list", HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() { }).getBody(); } // 托底数据 private List<Product> selectProductListFallback() { System.out.println("-----selectProductListFallback-----"); return Arrays.asList( new Product(1, "托底数据-华为手机", 1, 5800D), new Product(2, "托底数据-联想笔记本", 1, 6888D), new Product(3, "托底数据-小米平板", 5, 2020D) ); } /** * 根据主键查询商品 * * @param id * @return */ // 声明需要服务容错的方法 // 线程池隔离 @HystrixCommand(groupKey = "order-productService-singlePool",// 服务名称,相同名称使用同一个线程池 commandKey = "selectProductById",// 接口名称,默认为方法名 threadPoolKey = "order-productService-singlePool",// 线程池名称,相同名称使用同一个线程池 commandProperties = { // 超时时间,默认 1000ms @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") }, threadPoolProperties = { // 线程池大小 @HystrixProperty(name = "coreSize", value = "3"), // 队列等待阈值(最大队列长度,默认 -1) @HystrixProperty(name = "maxQueueSize", value = "100"), // 线程存活时间,默认 1min @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"), // 超出队列等待阈值执行拒绝策略 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100") }) @Override public Product selectProductById(Integer id) { System.out.println(Thread.currentThread().getName() + "-----selectProductById-----"); return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } }
@HystrixCommand
注解各项参数说明如下:
服务消费者启动类开启熔断器注解。
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); } }
服务提供者接口添加 Thread.sleep(2000)
,模拟服务处理时长。
JMeter 开启 20 线程循环 50 次访问: http://localhost:9090/order/1/product/list
浏览器访问: http://localhost:9090/order/1/product 控制台打印结果如下:
hystrix-order-productService-listPool-1-----selectProductList----- hystrix-order-productService-listPool-4-----selectProductList----- hystrix-order-productService-listPool-2-----selectProductList----- hystrix-order-productService-listPool-3-----selectProductList----- hystrix-order-productService-singlePool-1-----selectProductById----- hystrix-order-productService-listPool-5-----selectProductList----- hystrix-order-productService-listPool-6-----selectProductList-----
每次调用线程,当前请求通过计数信号量进行限制,当信号量大于了最大请求数 maxConcurrentRequests
时,进行限制,调用 fallback
接口快速返回。信号量的调用是同步的,也就是说,每次调用都得阻塞调用方的线程,直到结果返回。这样就导致了无法对访问做超时(只能依靠调用协议超时,无法主动释放)。
服务消费者 pom.xml 添加 hystrix 依赖。
<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
服务消费者业务层代码添加信号量隔离规则。
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.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.Arrays; import java.util.List; @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 查询商品列表 * * @return */ // 声明需要服务容错的方法 // 信号量隔离 @HystrixCommand(commandProperties = { // 超时时间,默认 1000ms @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000"), // 信号量隔离 @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"), // 信号量最大并发,调小一些方便模拟高并发 @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "6") }, fallbackMethod = "selectProductListFallback") @Override public List<Product> selectProductList() { // ResponseEntity: 封装了返回数据 return restTemplate.exchange( "http://product-service/product/list", HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() { }).getBody(); } // 托底数据 private List<Product> selectProductListFallback() { System.out.println("-----selectProductListFallback-----"); return Arrays.asList( new Product(1, "托底数据-华为手机", 1, 5800D), new Product(2, "托底数据-联想笔记本", 1, 6888D), new Product(3, "托底数据-小米平板", 5, 2020D) ); } }
@HystrixCommand
注解各项参数说明如下:
服务消费者启动类开启熔断器注解。
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); } }
服务提供者接口添加 Thread.sleep(2000)
,模拟服务处理时长。
服务消费者信号量最大并发设置为 6
,方便模拟高并发。
JMeter 开启 20 线程循环 50 次访问: http://localhost:9090/order/1/product/list
浏览器也访问: http://localhost:9090/order/1/product/list 结果如下:
隔离方式 | 是否支持超时 | 是否支持熔断 | 隔离原理 | 是否是异步调用 | 资源消耗 |
---|---|---|---|---|---|
线程池隔离 | 支持 | 支持 | 每个服务单独用线程池 | 支持同步或异步 | 大 |
信号量隔离 | 不支持 | 支持 | 通过信号量的计数器 | 同步调用,不支持异步 | 小 |
请求线程和调用 Provider 线程 不是同一条线程 ;
支持超时,可直接返回;
支持熔断,当线程池到达最大线程数后,再请求会触发 fallback
接口进行熔断;
隔离原理:每个服务单独用线程池;
支持同步和异步两种方式;
资源消耗大,大量线程的上下文切换、排队、调度等,容易造成机器负载高;
无法传递 Http Header。
maxConcurrentRequests
后。再请求会触发 fallback
接口进行熔断; 服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。
服务消费者 pom.xml 添加 hystrix 依赖。
<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
服务消费者业务层代码添加服务熔断规则。
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 = { // 10s 内请求数大于 10 个就启动熔断器,当请求符合熔断条件触发 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); } }
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 searchOrderById(Integer id) { return new Order(id, "order-003", "中国", 2666D, // 为了方便测试直接使用订单 ID 作为参数 Arrays.asList(productService.selectProductById(id))); } }
服务消费者启动类开启熔断器注解。
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); } }
访问: http://localhost:9090/order/1/product 结果如下:
-----selectProductById-----22:47:12.463 -----selectProductById-----22:47:17.677 -----selectProductById-----22:47:22.894
通过结果可以看到,服务熔断已经启用。每 5 秒会去重试一次 Provider 如果重试失败继续返回托底数据,如此反复直到服务可用,然后关闭熔断快速恢复。
吃鸡游戏相信大家应该都有所耳闻,这个游戏落地的时候什么东西都没有,装备都是需要自己去主动搜索或者通过击杀其他队伍而获取。所以,在这个游戏中就涉及到一个背包的问题,背包的大小决定了能携带的物资数量,总共分为三级,在你没有拿到更高级的背包之前,你只能将最重要的装备留在身边。其实服务降级,就是这么回事,再看一个例子。
大家都见过女生旅行吧,大号的旅行箱是必备物,平常走走近处绰绰有余,但一旦出个远门,再大的箱子都白搭了,怎么办呢?常见的情景就是把物品拿出来分分堆,比了又比,最后一些非必需品的就忍痛放下了,等到下次箱子够用了,再带上用一用。而服务降级,就是这么回事,整体资源快不够了,忍痛将某些服务先关掉,待渡过难关,再开启回来。
HystrixBadRequestException
服务消费者 pom.xml 添加 hystrix 依赖。
<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
服务消费者业务层代码添加服务降级规则。
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); } }
访问: http://localhost:9090/order/3/product 结果如下:
关闭服务提供者,再次访问: http://localhost:9090/order/3/product 结果如下:
通过结果可以看到,服务降级已经启用。当 Provider 不可用时返回托底数据,直到服务可用快速恢复。
我们在父工程下再创建一个 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
Product.java
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; }
ProductService.java
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); } }
OrderController.java
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); } }
我们已经可以通过 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<T>
接口。
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 结果如下:
控制台打印结果:
ERROR 17468 --- [ HystrixTimer-1] c.e.f.ProductServiceFallbackFactory : product-service 服务的 selectProductListByIds 方法出现异常,异常信息如下:com.netflix.hystrix.exception.HystrixTimeoutException
至此 Hystrix 服务容错知识点就讲解结束了。
本文采用 知识共享「署名-非商业性使用-禁止演绎 4.0 国际」许可协议
。
大家可以通过 分类
查看更多关于 Spring Cloud
的文章。
您的 点赞
和 转发
是对我最大的支持。
:loudspeaker: 扫码关注 哈喽沃德先生
「文档 + 视频」每篇文章都配有专门视频讲解,学习更轻松噢 ~