Spring Cloud 系列之 Netflix Hystrix 服务容错(三)
本篇文章为系列文章,未读前几集的同学请猛戳这里:
本篇文章讲解 Hystrix 的服务熔断和服务降级以及基于 Feign 的服务熔断处理。
服务熔断
点击链接观看:服务熔断视频(获取更多请关注公众号「哈喽沃德先生」)
服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。
添加依赖
服务消费者 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 = {
// 当请求符合熔断条件触发 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 不可用时返回托底数据,直到服务可用快速恢复。
Feign 雪崩处理
点击链接观看:Feign 雪崩处理视频(获取更多请关注公众号「哈喽沃德先生」)
环境准备
我们在父工程下再创建一个 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
的文章。
🤗 您的点赞
和转发
是对我最大的支持。
📢 扫码关注 哈喽沃德先生
「文档 + 视频」每篇文章都配有专门视频讲解,学习更轻松噢 ~
https://mrhelloworld.com