Spring Cloud 系列之 Netflix Hystrix 服务容错(三)

本篇文章为系列文章,未读前几集的同学请猛戳这里:

本篇文章讲解 Hystrix 的服务熔断和服务降级以及基于 Feign 的服务熔断处理。

  

1|0服务熔断

  

  点击链接观看:服务熔断视频(获取更多请关注公众号「哈喽沃德先生」)

  

  服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。

  

  

1|1添加依赖

  

  服务消费者 pom.xml 添加 hystrix 依赖。

<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>

  

1|2业务层

  

  服务消费者业务层代码添加服务熔断规则。

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))); } }

  

1|3启动类

  

  服务消费者启动类开启熔断器注解。

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); } }

  

1|4测试

  

  访问:http://localhost:9090/order/1/product 结果如下:

-----selectProductById-----22:47:12.463 -----selectProductById-----22:47:17.677 -----selectProductById-----22:47:22.894

  通过结果可以看到,服务熔断已经启用。每 5 秒会去重试一次 Provider 如果重试失败继续返回托底数据,如此反复直到服务可用,然后关闭熔断快速恢复。

  

2|0服务降级

  

  

  吃鸡游戏相信大家应该都有所耳闻,这个游戏落地的时候什么东西都没有,装备都是需要自己去主动搜索或者通过击杀其他队伍而获取。所以,在这个游戏中就涉及到一个背包的问题,背包的大小决定了能携带的物资数量,总共分为三级,在你没有拿到更高级的背包之前,你只能将最重要的装备留在身边。其实服务降级,就是这么回事,再看一个例子。

  大家都见过女生旅行吧,大号的旅行箱是必备物,平常走走近处绰绰有余,但一旦出个远门,再大的箱子都白搭了,怎么办呢?常见的情景就是把物品拿出来分分堆,比了又比,最后一些非必需品的就忍痛放下了,等到下次箱子够用了,再带上用一用。而服务降级,就是这么回事,整体资源快不够了,忍痛将某些服务先关掉,待渡过难关,再开启回来。

  

2|1触发条件

  

  • 方法抛出非 HystrixBadRequestException 异常;
  • 方法调用超时;
  • 熔断器开启拦截调用;
  • 线程池/队列/信号量跑满。

  

2|2添加依赖

  

  服务消费者 pom.xml 添加 hystrix 依赖。

<!-- spring-cloud netflix hystrix 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>

  

2|3业务层

  

  服务消费者业务层代码添加服务降级规则。

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); } }

  

2|4启动类

  

  服务消费者启动类开启熔断器注解。

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); } }

  

2|5测试

  

  访问:http://localhost:9090/order/3/product 结果如下:

  关闭服务提供者,再次访问:http://localhost:9090/order/3/product 结果如下:

  通过结果可以看到,服务降级已经启用。当 Provider 不可用时返回托底数据,直到服务可用快速恢复。

  

3|0Feign 雪崩处理

  

  点击链接观看:Feign 雪崩处理视频(获取更多请关注公众号「哈喽沃德先生」)

  

3|1环境准备

  

  我们在父工程下再创建一个 Consumer 项目这次是基于 Feign 实现声明式服务调用。

  

3|2添加依赖

  

  服务提供者添加 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>

  

3|3配置文件

  

  服务提供者需要开启 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

  

3|4实体类

  

  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; }

  

3|5消费服务

  

  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))); } }

  

3|6熔断降级

  

  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); } }

  

3|7控制层

  

  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); } }

  

3|8启动类

  

  服务消费者启动类开启 @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); } }

  

3|9捕获服务异常

  

  我们已经可以通过 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); } }; } }

  

3|10测试

  

  访问:http://localhost:9091/order/1/product/list 结果如下:

  

  控制台打印结果:

ERROR 17468 --- [ HystrixTimer-1] c.e.f.ProductServiceFallbackFactory : product-service 服务的 selectProductListByIds 方法出现异常,异常信息如下:com.netflix.hystrix.exception.HystrixTimeoutException

  至此 Hystrix 服务容错知识点就讲解结束了。

posted @ 2020-05-11 09:52  YoungDeng  阅读(133)  评论(0编辑  收藏  举报