前言
上期我们介绍了Eureka,Ribbon以及Feign,相信大家都其有了一定的了解,接下来我将继续带领大家学习微服务。
前期回顾
SpringCloud微服务从零带你入门(一)(Eureka | Ribbon | Feign)
Hystrix服务容错处理
在微服务架构中,如果各个微服务相互之间进行调用,如果一个微服务出现故障,那么很可能造成连锁反应,为了解决这一问题,引入了Hystrix服务容错处理。Hystrix本质通过HystrixCommand对调用进行隔离,这样就能阻止故障的连锁反应。
使用Hystrix
1 POM文件加入Maven依赖
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.18</version>
</dependency>
2 编写HystrixCommand
public class MyHystrixCommand extends HystrixCommand<String> {
private final String name;
public MyHystrixCommand(String name) {
super(HystrixCommandGroupKey.Factory.asKey("MyGroup"));
this.name = name;
}
@Override
protected String run() {
return this.name + ":" + Thread.currentThread().getName();
}
}
HystrixCommand 是Netfix Hystrix框架中的核心抽象类,封装了需要容错处理的命令,提供了分布式系统中关键的容错能力(熔断、降级、隔离)。
首先我们需要继承实现HystrixCommand抽象类,通过构造函数设置一个GroupKey(用于将命令分组,同一组的命令会共享线程池或信号量,便于统计和配置)。同时将具体的逻辑写在run方法中。
3 main同步调用HystrixCommand
public static void main(String[] args)
throws InterruptedException, ExecutionException {
String result = new MyHystrixCommand("yinjihuan").execute();
System.out.println(result);
}
4 main函数异步调用HystrixCommand
public static void main(String[] args)
throws InterruptedException, ExecutionException {
Future<String> future = new MyHystrixCommand("yinjihuan").queue();
System.out.println(future.get());
}
5 回退支持
我们通过sleep方法模拟调用超时失败的情况,增加getFullback方法返回回退内容。
public class MyHystrixCommand extends HystrixCommand<String> {
private final String name;
public MyHystrixCommand(String name) {
super(HystrixCommandGroupKey.Factory.asKey("MyGroup"));
this.name = name;
}
@Override
protected String run() {
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.name + ":" + Thread.currentThread().getName();
}
@Override
protected String getFallback() {
return " 失败了 ";
}
}
执行代码,发现返回内容是“失败了”,证明程序触发了回退。
策略配置
Hystrix 支持两种执行隔离策略:
- 一种是线程池隔离(默认):为每个命令组分配独立线程池,通过线程池大小限制并发请求数,适用于远程调用(如 HTTP 请求)。
- 另一种就是信号量隔离:通过信号量(计数器)控制并发请求数,不创建独立线程,直接在调用线程中执行命令,适用于本地高并发调用或轻量级操作。
信号量隔离
信号量隔离核心执行流程:
当调用 execute() 或 queue() 时,Hystrix 首先检查信号量计数器:
- 若当前并发数 < 最大允许,获取信号量,在调用线程中执行 run() 方法。
- 若并发数已满,拒绝请求,触发 getFallback() 降级逻辑。我们看一下代码:
public class MyHystrixCommand extends HystrixCommand<String> {
private final String name;
public MyHystrixCommand(String name) {
super(
HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyGroup"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(
HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE
)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(20) // 最大并发 20
.withExecutionTimeoutInMilliseconds(2000) // 超时 2s
)
);
this.name = name;
}
@Override
protected String run() {
// 本地快速操作(如缓存查询),不会阻塞太久
return name + ":" + Thread.currentThread().getName(); // 线程名是调用线程(如 main 或 Tomcat 工作线程)
}
@Override
protected String getFallback() {
// 信号量拒绝或超时后的降级逻辑
return "Fallback:" + name;
}
}
线程池隔离策略
我们再看一下线程池隔离策略的核心流程:
1 请求到达:若线程池中有空闲核心线程(<10),直接分配核心线程执行 run()。若核心线程全忙,请求进入队列排队(队列最多容纳 100 个请求)。若队列已满,且当前线程数 <100,创建新线程(非核心线程)执行请求。若线程数达到 100 且队列已满,拒绝请求,触发降级逻辑 getFallback()。
2 线程回收:非核心线程(超过 coreSize 的线程)在空闲 keepAliveTimeMinutes 后销毁,直到线程数回到 coreSize。
super(
HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyGroup")) // 命令分组
.andCommandPropertiesDefaults( // 配置命令属性(隔离策略)
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(
HystrixCommandProperties.ExecutionIsolationStrategy.THREAD // 线程池隔离(默认)
)
)
.andThreadPoolPropertiesDefaults( // 配置线程池属性
HystrixThreadPoolProperties.Setter()
.withCoreSize(10) // 核心线程数
.withMaxQueueSize(100) // 最大队列长度
.withMaximumSize(100) // 最大线程数(当队列已满时创建的线程上限)
)
);
信号量和线程池隔离策略对比:信号量轻量但不隔离线程,适合低延迟操作;线程池隔离性强,适合远程调用。
结果缓存
Hystrix中也为我们提供了方法级别的缓存,Hystrix 的方法级缓存是请求作用域缓存(Request-Scoped Cache),同一请求上下文中,相同参数的命令只会执行一次,后续调用直接从缓存中获取结果。
核心流程:
- 缓存键生成:通过 getCacheKey() 方法返回的字符串作为缓存键。
- 缓存查找:执行命令前,Hystrix 会检查当前请求上下文中是否存在该缓存键对应的结果。
- 缓存存储:首次执行 run() 后,结果会存入当前请求上下文的缓存中。
- 作用域限制:缓存仅在当前 HystrixRequestContext 内有效,上下文关闭后缓存失效。
我们看一下代码实现:通过重写getCacheKey来判断是否返回缓存中的数据,getCacheKey可以根据参数来生成。我们把创建对象时传进来的name参数作为缓存的key。同时在run方法中,增加输出来判断多次运行run方法,只输出一次get data。同时注意缓存的处理取决于请求的上下文,我们必须初始化Hystrix-RequestContext。
@Override
protected String getCacheKey() {
return String.valueOf(this.name);
}
@Override
protected String run() {
System.err.println("get data");
return this.name + ":" + Thread.currentThread().getName();
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 1. 初始化请求上下文(必须显式调用,否则缓存不生效)
HystrixRequestContext context = HystrixRequestContext.initializeContext();
// 2. 第一次调用:触发 run(),结果存入缓存
String result1 = new MyHystrixCommand("yinjihuan").execute();
System.out.println(result1); // 输出:yinjihuan:hystrix-MyGroup-1
// 3. 第二次调用:相同 name,从缓存获取,不执行 run()
Future<String> future = new MyHystrixCommand("yinjihuan").queue();
String result2 = future.get();
System.out.println(result2); // 输出:yinjihuan:hystrix-MyGroup-1
// 4. 关闭上下文,释放资源(缓存随上下文销毁)
context.shutdown();
}
验证输出结果:
get data
yinjihuan:hystrix-MyGroup-1
yinjihuan:hystrix-MyGroup-1
我们可以看到只输出了一次get data,缓存生效。
缓存清除
有缓存必然就有清除缓存的动作,当数据发生变动时,必须将缓存中的数据也更新掉,不然就会出现脏数据的问题。同样地,Hystrix也有清除缓存的功能。增加一个支持缓存清除的类,代码如下:
public class ClearCacheHystrixCommand extends HystrixCommand<String> {
private final String name;
private static final HystrixCommandKey GETTER_KEY =
HystrixCommandKey.Factory.asKey("MyKey");
public ClearCacheHystrixCommand(String name) {
super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.
Factory.asKey("MyGroup")).andCommandKey(GETTER_KEY)
);
this.name = name;
}
public static void flushCache(String name) {
HystrixRequestCache.getInstance(GETTER_KEY,
HystrixConcurrencyStrategyDefault.getInstance()).clear(name);
}
@Override
protected String getCacheKey() {
return String.valueOf(this.name);
}
@Override
protected String run() {
System.err.println("get data");
return this.name + ":" + Thread.currentThread().getName();
}
@Override
protected String getFallback() {
return " 失败了 ";
}
}
flushCache方法就是清除缓存的方法,通过HystrixRequestCache来执行清除操作,根据getCacheKey返回的key来清除。
合并请求
Hystrix支持将多个请求自动合并为一个请求,利用这个功能可以节省网络开销,比如每个请求都要通过网络访问远程资源。如果把多个请求合并为一个一起执行,将多次网络交互变成一次,则会极大地节省开销。
public class MyHystrixCollapser extends HystrixCollapser<List<String>, String, String> {
// 泛型说明:
// - 第一个参数:批量处理的响应类型(List<String>)
// - 第二个参数:单个请求的响应类型(String)
// - 第三个参数:请求参数类型(String,即 name)
@Override
public String getRequestArgument() {
return name; // 返回当前请求的参数
}
@Override
protected HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, String>> requests) {
return new BatchCommand(requests); // 创建批量处理命令
}
@Override
protected void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, String>> requests) {
int count = 0;
for (CollapsedRequest<String, String> request : requests) {
request.setResponse(batchResponse.get(count++)); // 按顺序映射响应到每个请求
}
}
private static final class BatchCommand extends HystrixCommand<List<String>> {
private final Collection<CollapsedRequest<String, String>> requests;
public BatchCommand(Collection<CollapsedRequest<String, String>> requests) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetValueForKey")));
this.requests = requests;
}
}
@Override
protected List<String> run() {
System.out.println("真正执行请求......");
ArrayList<String> response = new ArrayList<>();
for (CollapsedRequest<String, String> request : requests) {
response.add("返回结果 : " + request.getArgument()); // 模拟批量处理,生成响应
}
return response;
}
}
我们对其进行一下测试:
HystrixRequestContext context = HystrixRequestContext.initializeContext();
Future <String> f1 = new MyHystrixCollapser("yinjihuan").queue();
Future<String> f2 = new MyHystrixCollapser("yinjihuan333").queue();
System.out.println(f1.get()+"="+f2.get());
context.shutdown();
验证输出结果:
真正执行请求 .......
返回结果 : yinjihuan= 返回结果 : yinjihuan333
Spring Cloud 中使用Hystrix
1 新建项目,增加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2 在启动类上添加@EnableHystrix或者@EnableCircuitBreaker。注意,@EnableHystrix中包含了@EnableCircuitBreaker。
@EnableHystrix激活 Hystrix 的所有功能,包括:
- 断路器(Circuit Breaker):监控服务调用成功率,自动熔断 / 恢复。
- 隔离策略(线程池 / 信号量):控制并发请求,防止级联故障。
- 请求合并(Request Collapsing):批量处理同类请求,减少网络开销。
- 方法级缓存:基于请求上下文的缓存优化(需配合 HystrixRequestContext)。
@EnableCircuitBreaker仅启用 Hystrix 的断路器功能,不包含隔离策略、请求合并等其他特性。
我们看一下HystrixCommand注解使用。
@SpringBootApplication
@EnableHystrix // 启用 Hystrix 核心功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@HystrixCommand(
fallbackMethod = "defaultCallHello", // 降级方法名
commandProperties = {
@HystrixProperty(
name = "execution.isolation.strategy",
value = "THREAD" // 隔离策略:线程池(默认值,可省略)
)
}
)
@GetMapping("/callHello")
public String callHello() {
// 远程调用逻辑(可能失败)
String result = restTemplate.getForObject("http://localhost:8088/house/hello", String.class);
return result;
}
public String defaultCallHello() {
return "降级响应:服务不可用"; // 主逻辑失败时返回此内容
}
当然commandProperties还有很多常用配置,如下:
commandProperties = {
// 超时时间(默认 1000ms)
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000"),
// 熔断触发条件:请求失败率 >= 50%,且 10s 内请求数 >= 20
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000") // 熔断后休眠 10s 尝试恢复
}
Feign整合Hystrix服务容错
1 在项目中加入EurekaClient,Feign,Hystrix的依赖,同时在属性文件中开启Feign对Hystrix的支持:
feign.hystrix.enabled=true
2 Fallback方式回退
在Feign的客户端类上的@FeignClient注解中指定fallback进行回退,创建一个Feign的客户端类UserRemoteClient,为其配置fallback。UserRemoteClientFallback类需要实现UserRemoteClient类中所有的方法,返回回退时的内容:
@FeignClient ( value = "eureka-client-user-service", fallback =
UserRemoteClientFallback.class)
public interface UserRemoteClient {
@GetMapping("/user/hello")
String hello();
}
@Component
public class UserRemoteClientFallback implements UserRemoteClient {
@Override
public String hello() {
return "fail";
}
}
3 FallbackFactory方式回退
@Component
public class UserRemoteClientFallbackFactory implements
FallbackFactory<UserRemoteClient> {
private Logger logger = LoggerFactory.getLogger(
UserRemoteClientFallbackFactory.class);
@Override
public UserRemoteClient create(final Throwable cause) {
logger.error(“UserRemoteClient回退:”, cause);
return new UserRemoteClient() {
@Override
public String hello() {
return "fail";
}
};
}
}
@FeignClient(value = " eureka-client-user-service ",
configuration = FeignConfiguration.class,
fallbackFactory = UserRemoteClientFallbackFactory.class)
FallbackFactory和Fallback唯一的区别在于异常信息的输出方式。
总结
本期讲述了SpringCloud的Hystrix篇,下期将介绍API网关Zuul,感兴趣的友友可以关注一下博主。
参考文献
Spring Cloud微服务:入门、实战与进阶 尹吉欢
写在文末
有疑问的友友,欢迎在评论区交流,笔者看到会及时回复。
请大家一定一定要关注!!!
请大家一定一定要关注!!!
请大家一定一定要关注!!!
友友们,你们的支持是我持续更新的动力~