Hystrix 入门教程 - 基础篇之二
大纲
Hystrix 配置说明
Hystrix 的配置比较多,具体可以参考:官方英文文档,第三方中文文档
Hystrix 命令使用
Hystrix 提供了两种命令,包括 HystrixCommand(@HystrixCommand)和 HystrixObservableCommand,两者都支持故障与延迟容错、断路器、指标统计等功能。
特别注意
Hystrix 为 HystrixCommand 提供了 @HystrixCommand 注解(底层基于自定义注解 + AOP 实现),但没有为 HystrixObservableCommand 提供相应的注解。这是因为 HystrixObservableCommand 是基于 RxJava 的 Observable 异步流,可以发射多个结果,逻辑更复杂。注解方式不适合封装这种流式、多次发射的异步执行,因为 AOP 拦截的是方法的一次调用,难以覆盖 Observable 的整个生命周期,也无法直接处理其订阅、错误回调和完成回调。
两种命令的区别
- HystrixCommand 默认是阻塞式的,既支持同步调用,也支持异步调用;而 HystrixObservableCommand 默认是非阻塞式的,支持异步调用,也支持转换为同步调用。
- HystrixCommand 的核心执行方法是
run(),而 HystrixObservableCommand 的核心执行方法是construct()。 - HystrixCommand 每次执行只能返回一个结果,而 HystrixObservableCommand 可以返回多个结果流。
| 特性 | HystrixCommand | HystrixObservableCommand |
|---|---|---|
| 返回结果 | 单个结果(同步 / 异步) | 多个结果流(响应式,支持流式返回) |
| 执行方式 | run() 返回单一值 | construct() 返回 Observable<T>(可发射多个元素或 0 个元素) |
| 支持场景 | 一次调用只需要一个结果(比如:查询用户信息、下单操作) | 需要返回数据流的场景(比如:获取一个批量数据流、事件推送) |
| 编程模型 | 命令式 + Future | 响应式 + RxJava |
| 使用难度 | 较简单 | 相对复杂,需要 RxJava 思维 |
两种命令的核心方法
HystrixCommand 的核心方法:
run():核心方法,执行一次只会返回一个结果execute():同步调用(阻塞式),调用后直接阻塞住,直到依赖服务返回单个结果或者抛出异常queue():异步调用(非阻塞式),返回一个 Future,后面可以通过 Future 获取单个结果observe():异步调用(非阻塞式),订阅一个 Observable 对象,Observable 代表的是依赖服务返回的结果,获取到一个代表结果的 Observable 对象的拷贝对象
HystrixObservableCommand 的核心方法:
construct():核心方法,返回Observable<T>(可发射多个元素或 0 个元素)- 如果确定只会返回一条数据,可以使用
toBlocking().single()、toBlocking().toFuture().get()等方式阻塞等待执行结果(即同步调用) observe()- 作用:返回一个热
Observable(Hot Observable) - 特点:
- 调用
observe()的时候就会立刻触发执行(不会等待订阅) - 多个订阅者会共享同一次执行结果(即多次订阅 → 复用同一次执行)
- 调用
- 适用场景:希望立即触发命令,并把同一个结果分发给多个订阅者
- 作用:返回一个热
toObservable()- 作用:直接返回一个冷
Observable(Cold Observable) - 特点:
- 只有当订阅(
subscribe())发生时,命令才会真正执行 - 每次有新的订阅者时,都会触发一次新的执行(即多次订阅 → 多次执行)
- 只有当订阅(
- 适用场景:需要完全控制订阅时机,或者希望每个订阅者独立执行一次命令
- 作用:直接返回一个冷
HystrixCommand 的底层实现
- HystrixCommand 的
execute()底层调用的是queue().get(),接着会调用toObservable().toBlocking().toFuture();也就是说,无论是哪种执行 Command 的方式,最终都是依赖toObservable()去执行的。 - 换言之,
toObservable()才是 HystrixCommand 执行逻辑的统一入口,它负责触发construct()或者run()方法,执行命令逻辑,并把结果流式化为 Observable,其他方法只是包装了不同的同步 / 异步调用语义。
Hystrix 的超时处理机制
在 Hystrix 中,如果 HystrixCommand.run() 或 HystrixObservableCommand.construct() 的执行时间超过了配置的超时时长,那么命令(Command)所在的主调用线程会抛出一个 TimeoutException,并触发 fallback 降级逻辑,此时不会再关心 run() 或 construct() 的返回值。需要特别注意的是,Hystrix 并不能真正中断一个正在执行且已经超时的依赖调用线程,该线程仍然可能因为长时间阻塞而占用线程池资源,从而导致线程池被耗尽,即使此时新请求已经被限流或拒绝;这是因为 Java 线程的中断机制是协作式的,只能发出中断信号,而不能强制终止线程。相反,如果执行没有超时,那么依赖调用的结果会被正常返回,同时 Hystrix 会进行相关的日志记录和指标统计。
两种命令的最佳实践
从 Hystrix 的设计理念和最佳实践来看,HystrixCommand(@HystrixCommand)和 HystrixObservableCommand 应该尽量用在 Service 层,而不是 Controller 层。原因如下:
- (1) 业务逻辑和远程调用集中在 Service 层
- Hystrix 的核心作用是熔断、降级、隔离远程调用。
- Controller 层只负责接收请求、组装参数和返回响应,不直接处理远程调用。
- 将 Hystrix 放在 Service 层,能保护实际的远程服务调用,而不是整个 HTTP 接口。
- (2) 便于代码复用
- Service 层的方法可能会被多个 Controller 调用,如果 Hystrix 用在 Service 层,熔断、降级和隔离策略可以被多个调用共享。
- (3) 避免 Controller 层阻塞过多
- Controller 层本身应该轻量化,尽量不直接处理熔断逻辑,否则会增加 Controller 复杂度。
- (4) 测试和维护更方便
- 在 Service 层使用 HystrixCommand,可以单独测试远程调用逻辑和降级策略,而不依赖 Controller 层。
两种命令的使用示例
HystrixCommand 的使用示例:
1 | public class GetProductInfoCommand extends HystrixCommand<String> { |
HystrixCommand 的调用方式:
1 | String result = new GetProductInfoCommand(1L).execute(); // 同步调用 |
HystrixObservableCommand 的使用示例:
1 | import rx.Observable; |
HystrixObservableCommand 的调用方式:
1 | Observable<String> observable = new GetProductInfoObservableCommand(1L).observe(); |
Hystrix 隔离策略
Hystrix 会对请求进行封装,然后管理请求的调用,从而实现断路器等多种功能。Hystrix 提供了两种隔离策略来处理请求,一种是线程池隔离(默认),一种是信号量隔离。
线程池隔离
Hystrix 采用 Bulkhead(舱壁隔离)技术,将外部依赖进行资源隔离,避免某个外部依赖的故障拖垮整个服务。在线程池隔离策略下,每个外部依赖的调用都在其独立的线程池中执行,从而与业务调用线程分离。这样即使外部依赖发生超时或延迟过长,也不会阻塞业务调用线程。此外,Hystrix 会为不同的外部依赖分配独立的线程池。如果某个依赖延迟严重,最多只会耗尽该依赖所属的线程池资源,而不会影响其他依赖的正常调用。
线程池隔离的介绍
- 概述
- Hystrix 会为每个服务调用(Command)分配一个独立的线程池(可以自定义大小),调用服务时,会在这个线程池中执行。
- 这样即使某个服务调用非常慢或阻塞,也不会影响调用它的主调用线程或者其他服务调用。
- 线程池内部有一个等待队列(Queue),默认可配置
maxQueueSize,当一个请求到来时:- 如果线程池有空闲线程,直接处理请求;
- 如果线程池满但队列未满,请求进入等待队列,等待有空闲线程可用;
- 如果线程池满且队列也满,会立即拒绝该请求,拒绝后的处理方式:
- 如果配置了
fallback,则执行fallback返回降级结果; - 如果未配置
fallback,则对外会抛出异常HystrixRuntimeException。
- 如果配置了
- 每个请求有独立的执行超时时间(默认 1 秒),即使请求进入线程池或队列,也可能因为执行时间过长而触发超时,Hystrix 会中断执行线程并调用
fallback(如果配置了)。 - 默认隔离策略就是线程池隔离。
- 作用:
- 不仅实现了限流控制,还实现了故障隔离:
- 即便服务调用慢或挂掉,线程池线程阻塞,也不会影响主调用线程。
- 线程池提供队列和超时控制,可以防止调用链雪崩。
- 限流是通过线程池的大小来控制的。
- 不仅实现了限流控制,还实现了故障隔离:
- 特点:
- 性能开销比信号量隔离大(线程上下文切换、线程池管理)。
- 自带超时机制,当线程池调用超时时自动中断线程,避免线程被长期占用。
- Hystrix 线程池里的线程不是 Tomcat 的线程,而是 Hystrix 自己维护的线程池中的工作线程。
- 如果某个依赖(如远程服务调用)卡住了,最多会占用 Hystrix 自己维护的线程池中的工作线程,不会拖垮 Tomcat 的所有请求线程。
- 优点:
- 资源隔离:每个外部依赖都运行在自己的线程池中,即使某个线程池被耗尽,也不会影响其他外部依赖的调用。
- 超时控制:每个线程池内部的请求都可以配置执行超时时间,当调用超时时自动中断线程,避免线程被长期占用。
- 引入新依赖的安全性:即使新引入的外部依赖服务存在问题,也只会影响自身的线程池,不会拖累整个系统。
- 快速恢复能力:当故障依赖恢复时,只需清理或重建其线程池即可快速恢复调用,而不像 Tomcat 主线程池那样复杂。
- 可观测性与动态调整:线程池会统计成功、失败、拒绝、超时等指标,运维人员可以据此近实时地热修改外部依赖服务的配置,无需停机。
- 适应服务变化:当服务本身发生变更或调用特征变化时,线程池的健康状况能及时反映问题,方便动态调整配置。
- 支持异步调用:基于线程池的异步特性,可以在同步调用的基础上封装出一层异步调用能力。
- 缺点:
- 额外 CPU 开销:除了 Tomcat 的请求线程,还需要维护 Hystrix 自己的线程池,增加了额外的 CPU 开销。
- 线程上下文切换成本:每个命令(Command)都依赖独立线程执行,会带来排队、调度和线程上下文切换的开销。
- 调用延迟增加:多线程异步执行会引入一定的延时。
- Netflix API 每天通过 Hystrix 执行 10 亿次调用,每个服务实例有 40 个以上的线程池,每个线程池有 10 个左右的线程;
- 最后发现每次调用平均增加约 3ms 左右的延时,最高延时不超过 10ms;但相较于系统可用性和稳定性的提升,这个代价是可接受的。
- 适用场景:
- 适用于可能出现网络延迟、阻塞的远程调用(如 HTTP / RPC / 数据库访问等)。
线程池隔离的使用
1 | public class GetBrandNameCommand extends HystrixCommand<String> { |
1 |
|
信号量隔离
Hystrix 提供了 Semaphore(信号量)隔离技术,用于限制某个依赖服务的并发访问量,而不是依赖线程池和队列大小来限制流量。信号量机制主要用于限流与削峰,适合低延迟、非网络调用或对性能要求较高的场景,但它无法提供超时控制,也不能对高延迟的依赖进行隔离,如果底层调用发生严重延迟,业务调动线程会一直阻塞住。通过将 execution.isolation.strategy=SEMAPHORE 设置隔离策略为信号量隔离,Hystrix 就会使用信号量来代替线程池进行并发控制,一旦并发请求数超过设定阈值,新的请求会立即被拒绝,从而实现限流。
信号量隔离的介绍
- 概述:
- 不创建线程池,而是在调用的线程中执行,通过信号量限制并发请求数量。
- 当信号量不足(即超过并发请求数量限制)时,Hystrix 不会阻塞等待获取信号量,而是立即拒绝该请求,拒绝后的处理方式:
- 如果配置了
fallback,则执行fallback返回降级结果; - 如果未配置
fallback,则对外会抛出异常HystrixRuntimeException。
- 如果配置了
- 不是默认隔离策略,需要在配置里显式设置
execution.isolation.strategy=SEMAPHORE。
- 作用:
- 本质上是一种轻量级的限流机制,可以防止某个服务被过度并发调用导致资源耗尽。
- 特点:
- 不切换线程,开销小,延迟低。
- 只控制并发数量(即限流),没有线程隔离的保护作用,也没有超时机制。
- 如果服务调用阻塞(比如网络慢),主调用线程(如 Tomcat 线程)也会被阻塞。
- 信号量隔离本身没有等待队列,也没有阻塞等待获取信号量的逻辑。
- 优点:
- 性能开销小:不需要额外的线程池和线程切换,避免了排队、调度和上下文切换带来的成本。
- 实现简单:直接在调用线程内执行,依靠信号量计数来限制并发。
- 适合低延迟场景:对于本地方法调用或快速返回的依赖(如本地内存缓存访问),信号量隔离更高效。
- 即时拒绝:一旦并发请求超过信号量阈值,新的请求会立即被拒绝,从而快速保护系统。
- 缺点:
- 无法超时控制:调用仍运行在业务调用线程中,如果底层依赖延迟严重,业务调用线程会被一直阻塞,不能像线程池隔离一样强制超时。
- 缺乏真正的隔离:没有单独线程池,若依赖调用阻塞,会直接占用 Tomcat 线程,可能会拖慢整个系统。
- 适用场景:
- 适合非常轻量的服务调用,线程开销大于服务调用本身的场景。
- 仅适用于延迟可控、调用快速的依赖,不适合高延迟或不稳定的外部服务。
- 比如,内部调用 / 本地方法调用(例如本地内存缓存访问),因为这种调用一般快而可靠,不涉及任何网络请求,不需要超时机制,只需要并发保护。
信号量隔离的使用
1 | public class GetCityNameCommand extends HystrixCommand<String> { |
1 |
|
Hystrix 熔断机制
熔断机制的介绍
Hystrix 熔断机制的工作原理如下:
(1) 请求量阈值判断
- 断路器首先会统计一段时间内的请求情况,这个时间窗口由
metrics.rollingStats.timeInMilliseconds(默认 10 秒)控制。 - 在这个时间窗口内,若通过断路器的请求数未达到
circuitBreaker.requestVolumeThreshold阀值(默认 20),则即使存在异常请求,也不会触发熔断判断。 - 换句话说,必须先有足够的请求量,断路器才会进行健康性评估。
- 举例:10 秒内经过断路器的请求总数只有 10 个,而阈值是 20,即使 10 个请求全部失败(100% 错误率),断路器也不会考虑熔断。
- 断路器首先会统计一段时间内的请求情况,这个时间窗口由
(2) 错误比例阈值判断
- 如果请求总量达到了阈值,断路器会计算失败请求的比例。
- 当失败比例 ≥
circuitBreaker.errorThresholdPercentage阀值(默认 50%)时,断路器会从 Closed(关闭)状态转为 Open(打开)。 - 失败的请求包括:抛出异常、超时、线程池 / 信号量拒绝等情况。
- 举例:10 秒内共有 30 个请求,而阈值是 20,其中有 20 请求个失败,失败率 66% ≥ 50%,则断路器会被打开。
(3) 断路器打开(Open)
- 一旦断路器进入 Open(打开)状态,所有经过该断路器的请求都会被直接拒绝,不再调用后端逻辑,拒绝后的处理方式:
- 如果配置了
fallback,则执行fallback返回降级结果; - 如果未配置
fallback,则对外会抛出异常HystrixRuntimeException。
- 如果配置了
- 一旦断路器进入 Open(打开)状态,所有经过该断路器的请求都会被直接拒绝,不再调用后端逻辑,拒绝后的处理方式:
(4) 断路器半开(Half-Open)
- 断路器保持打开状态的时间由
circuitBreaker.sleepWindowInMilliseconds(默认 5 秒)决定。 - 在这个休眠时间结束后,断路器会进入 Half-Open(半开)状态,允许少量请求通过,作为探测。
- 断路器保持打开状态的时间由
(5) 断路器关闭(Closed)
- 如果探测请求执行成功,说明后端服务已恢复可用,断路器会从 Half-Open 状态转为 Closed 状态,恢复正常调用。
- 如果探测请求依旧失败,断路器会重新进入 Open 状态,继续熔断一段时间。
熔断机制的配置
Hystrix 的默认熔断策略
在 10 秒内,如果请求数 ≥ 20 且错误率 ≥ 50%,断路器会打开;熔断 5 秒 后,断路器进入半开状态,尝试让少量请求通过,请求成功则断路器关闭(恢复正常),请求失败则断路器继续打开(再休眠 5 秒),如此往复。
circuitBreaker.enabled- 作用:
- 是否启用断路器:
true:启用。false:禁用(所有请求都会直接执行,不会触发熔断逻辑)。
- 是否启用断路器:
- 默认值:
true。
- 设置方法:
1
HystrixCommandProperties.Setter().withCircuitBreakerEnabled(boolean value);
- 作用:
metrics.rollingStats.timeInMilliseconds- 作用:
- 设置统计滚动时间窗口的长度
- 举例:如果将它设置为 10000 毫秒,那么 Hystrix 会基于过去 10 秒的请求统计数据来决定是否熔断。
- 默认值:
- 10000 毫秒(10 秒)。
- 设置方法:
1
HystrixCommandProperties.Setter().withMetricsRollingStatisticalWindowInMilliseconds(20000);
- 作用:
circuitBreaker.requestVolumeThreshold- 作用:
- 设置在滚动时间窗口(默认 10 秒)内,触发熔断所需的最小请求数。
- 默认值:
- 默认的请求数阀值是 20。
- 说明:
- 必须达到该请求数阈值,断路器才会开始计算错误比例。
- 如果请求数不足,即使全部请求失败,也不会触发熔断。
- 设置方法:
1
HystrixCommandProperties.Setter().withCircuitBreakerRequestVolumeThreshold(int value);
- 作用:
circuitBreaker.errorThresholdPercentage- 作用:
- 错误百分比阈值。
- 默认值:
- 50(即 50%)。
- 说明:
- 在滚动时间窗口内,若请求总数 ≥
requestVolumeThreshold, - 且错误率 ≥
errorThresholdPercentage, - 则断路器会从 Closed 状态转为 Open 状态。
- 在滚动时间窗口内,若请求总数 ≥
- 设置方法:
1
HystrixCommandProperties.Setter().withCircuitBreakerErrorThresholdPercentage(int value);
- 作用:
circuitBreaker.sleepWindowInMilliseconds- 作用:
- 断路器打开(Open)后,休眠的时间窗口。
- 默认值:
- 默认休眠 5000 毫秒(5 秒)。
- 说明:
- 在该时间段内,请求会被快速失败(直接走 Fallback 逻辑)。
- 时间过后,断路器进入 Half-Open 状态,允许少量请求尝试调用后端服务。
- 设置方法:
1
HystrixCommandProperties.Setter().withCircuitBreakerSleepWindowInMilliseconds(int value);
- 作用:
circuitBreaker.forceOpen- 作用:
- 强制打开断路器。
- 默认值:
false。
- 说明:
- 设置为
true后,断路器会一直处于 Open 状态,所有请求直接走 Fallback 逻辑,相当于手动熔断。
- 设置为
- 设置方法:
1
HystrixCommandProperties.Setter().withCircuitBreakerForceOpen(boolean value);
- 作用:
circuitBreaker.forceClosed- 作用:
- 强制关闭断路器。
- 默认值:
false。
- 说明:
- 设置为
true后,断路器会忽略所有错误统计,始终允许请求通过,相当于手动关闭熔断。
- 设置为
- 设置方法:
1
HystrixCommandProperties.Setter().withCircuitBreakerForceClosed(boolean value);
- 作用:
熔断机制的使用
Hystrix 熔断机制的使用(基于继承类):
1 | public class GetBrandNameCommand extends HystrixCommand<String> { |
1 |
|
Hystrix 熔断机制的使用(基于注解):
1 |
|
熔断机制的注意事项
为什么没有 fallback 仍可能出现 “服务雪崩”
即使断路器打开,如果没有 fallback,快速失败的请求就会直接抛异常给调用方:
- 如果调用方是用户请求(如 Web 请求):
- 每个请求直接失败,会让用户感知到服务不可用。
- 如果调用方是另一个服务(服务间调用):
- 异常被抛回去,服务调用方也可能因为未处理异常或线程被占用而失败。
- 当大量调用同时失败时,就可能形成连锁失败,类似 “雪崩效应”。
所以,断路器 + fallback 才能有效防止 “服务雪崩”:
- 断路器快速失败 → 保护资源
fallback提供兜底处理逻辑 → 系统仍然可用
举例说明
假设服务 A 调用服务 B,但服务 B 出现故障,服务 A 配置了 Hystrix:
- A 有
fallback→ B 不可用时,A 调用fallback返回默认值,A 仍然可用。 - A 无
fallback→ B 不可用时,A 抛出异常,A 的线程被占用,更多请求堆积 → A 也可能挂掉 → 连锁失败 → 服务雪崩。
总结
熔断机制可以防止对下游服务(服务提供方)的直接压力,但没有 fallback 时,上游服务(服务调用方)仍可能因异常堆积而失败,形成级联故障,所以单靠熔断机制不能完全避免雪崩,必须配合 fallback 才可以。
Hystrix 降级机制
降级机制的介绍
在以下几种情况下,Hystrix 会触发 fallback 降级机制:HystrixCommand 的 run() 方法或 HystrixObservableCommand 的 construct() 方法抛出异常、断路器打开、线程池 / 队列 / 信号量已满、命令(Command)执行超时。一般来说,fallback 建议返回一些默认值,比如默认结果或本地内存缓存数据,避免再次发起网络调用;如果确实需要网络调用,最好再封装到一个单独的 HystrixCommand 中,以保证资源隔离。在 HystrixCommand 中,可以通过重写 getFallback() 方法来提供降级逻辑;在 HystrixObservableCommand 中,可以通过实现 resumeWithFallback() 方法并返回一个 Observable 来提供降级结果。如果 fallback 成功返回结果,那么 Hystrix 会将该结果返回给调用方。对于 HystrixCommand,最终会包装成一个 Observable 发射结果;对于 HystrixObservableCommand,则直接返回原始的 Observable。
如果没有实现 fallback,或者 fallback 本身抛出了异常,Hystrix 也会返回一个 Observable,但不会有任何数据发射,不同的执行方式在调用时的表现不同:
- 对于
execute(),会直接抛出异常; - 对于
queue(),会返回一个Future,在调用get()时抛出异常; - 对于
observe(),会返回一个Observable,但在订阅时会触发调用者的onError(); - 对于
toObservable(),同样会返回一个Observable,在订阅时触发调用者的onError()。
降级机制的配置
Hystrix 为降级机制提供了配置参数 fallback.isolation.semaphore.maxConcurrentRequests,为什么需要这个参数呢?因为 Hystrix 的降级逻辑(fallback)本身可能也会访问本地资源(比如访问缓存、查询数据库、读文件等),如果没有访问限制,可能在高并发场景下将这些资源也压垮。该参数的核心作用如下:
- 用于限制 fallback 方法(降级逻辑)同时能被多少个线程并发执行,默认值是 10。
- fallback 方法默认是和调用线程在同一个线程里执行的,通过信号量隔离来控制并发数,而不是运行在独立的线程池。
- 当同时执行 fallback 方法的请求数(线程数)超过上限,新的请求不会进入 fallback 方法,而是会被直接拒绝并抛出异常
HystrixRuntimeException。 - 避免主逻辑挂了后,所有流量瞬间打到 fallback 方法,导致 fallback 方法自身也被压垮。
- 目的是避免因为大量请求失败时,fallback 方法被并发挤爆。
降级机制的使用
Hystrix 降级机制的使用(基于继承类):
1 | public class GetBrandNameCommand extends HystrixCommand<String> { |
1 |
|
Hystrix 降级机制的使用(基于注解):
1 |
|
Hystrix 超时机制
超时机制的介绍
在 Hystrix 中,如果 HystrixCommand.run() 或 HystrixObservableCommand.construct() 的执行时间超过了配置的超时时长,那么命令(Command)所在的主调用线程会抛出一个 TimeoutException,并触发 fallback 降级逻辑,此时不会再关心 run() 或 construct() 的返回值。需要特别注意的是,Hystrix 并不能真正中断一个正在执行且已经超时的依赖调用线程,该线程仍然可能因为长时间阻塞而占用线程池资源,从而导致线程池被耗尽,即使此时新请求已经被限流或拒绝;这是因为 Java 线程的中断机制是协作式的,只能发出中断信号,而不能强制终止线程。相反,如果执行没有超时,那么依赖调用的结果会被正常返回,同时 Hystrix 会进行相关的日志记录和指标统计。
作用对象:
- 针对每个
HystrixCommand的run()方法执行设置超时时间。 HystrixObservableCommand也支持超时机制,但不直接作用于construct()方法本身:- Observable 本身返回一个 RxJava Observable;
- 超时需要依赖
timeout操作符或者 Hystrix 提供的 Observable 封装的超时机制; - Hystrix 会在 Observable 执行过程中监控超时,并在超时后触发
fallback。
- 针对每个
配置方式:
- 编码方式:
1
HystrixCommandProperties.Setter().withExecutionIsolationThreadTimeoutInMilliseconds(2000);
- 注解方式:
1
2
3
4
5
- 编码方式:
超时行为:
- 当命令(Command)执行时间超过配置的超时时间(如 2000ms):
- 如果使用线程池隔离,执行线程会被标记为超时,但线程仍然可能继续运行,Hystrix 会执行 Fallback 逻辑(如果配置了)或抛出异常。
- 如果使用信号量隔离,超时机制不会生效,因为信号量隔离本身没有超时机制,超时需要由调用方自己控制。
- 当命令(Command)执行时间超过配置的超时时间(如 2000ms):
作用与意义:
- 避免单个请求长时间占用线程池资源。
- 配合线程池隔离使用,可防止故障蔓延。
特别注意
Hystrix 的超时机制通常是配合线程池隔离策略一起使用的,而信号量隔离策略是没有超时机制的。
超时机制的使用
Hystrix 超时机制的使用(基于继承类):
1 | public class GetBrandNameCommand extends HystrixCommand<String> { |
1 |
|
Hystrix 超时机制的使用(基于注解):
1 |
|
超时机制的工作原理
Hystrix 的超时机制是通过 HystrixTimer 定时器 + Future.cancel(true) 实现的。
- 整体思路
- Hystrix 会把
HystrixCommand.run()或者HystrixObservableCommand.construct()的逻辑封装到一个FutureTask,并交给线程池或信号量执行; - 同时,Hystrix 内部有一个
HystrixTimer(基于ScheduledThreadPoolExecutor),它会在超时时间到达时触发检查; - 如果命令还没执行完,就会触发超时逻辑:抛出
TimeoutException,标记命令执行超时,并尝试调用Thread.interrupt()来中断执行线程; - 最终,调用方得到的就是一个超时异常,Hystrix 会进入 fallback 逻辑(如果有配置)。
- Hystrix 会把
- 执行流程
- 提交任务:
HystrixCommand.execute()→queue()→toObservable()→FutureTask提交到线程池中执行。
- 提交任务:
- 启动定时器:同时在
HystrixTimer中注册一个定时任务,延迟时间 = 配置的超时时长。
- 启动定时器:同时在
- 定时器检查:如果到时间任务还未完成,定时器会:
- 标记命令为超时;
- 调用
Future.cancel(true)来触发执行线程的interrupt(); - 向调用方返回
TimeoutException。
- 降级处理:Hystrix 检测到超时异常后,会尝试调用
getFallback()或resumeWithFallback()来提供降级结果。
- 降级处理:Hystrix 检测到超时异常后,会尝试调用
- 注意事项
- 不能强制杀线程:
- Hystrix 的
interrupt()只是对执行线程打个中断标记,无法强制中断正在执行的线程,如果业务代码没响应中断,线程依旧会继续执行,直到自然结束为止。
- Hystrix 的
- 线程池耗尽风险:
- 如果依赖服务卡死,虽然 Hystrix 提前超时返回,但底层的线程仍然阻塞,可能导致线程池被耗尽。
- 配置参数:
- 超时时间由
execution.isolation.thread.timeoutInMilliseconds控制,默认 1000ms。
- 超时时间由
- 不能强制杀线程:
Hystrix 异常机制
5 种会被 fallback 截获的情况
Hystrix 的异常处理中,有 5 种出错的情况会被 fallback 所截获,从而触发 fallback,这些情况分别是:

有一种异常是不会触发 fallback 的,且不会被计数进入熔断,它是 BAD_REQUEST,会抛出 HystrixBadRequestException,这种异常一般对应的是由非法参数或者一些非系统异常引起的,对于这种异常可以根据响应创建对应的异常进行异常封装或者直接处理。
1 | /** |
获取 fallback 里的异常信息
若想在 @HystrixCommand 里获取异常信息,只需要在方法内指定 Throwable 参数;
1 |
|
或者继承 @HystrixCommand 的命令,通过方法来获取异常:
1 |
|
在 Feign Client 中可以用 ErrorDecoder 实现对这类异常的包装,在实际的使用中,很多时候调用接口会抛出这些 400-500 之间的错误,此时可以通过它进行封装:
1 |
|
fallback 会抛出异常的情况

Hystrix 线程池配置
默认的线程池规则
(1) Hystrix 线程池的唯一标识是
threadPoolKey- 如果 HystrixCommand / HystrixObservableCommand 没有显式指定
threadPoolKey:- 同一个类的所有方法默认使用 HystrixCommand / HystrixObservableCommand 的
groupKey所对应的线程池。 groupKey默认是方法所在类名,即同一个类里的所有方法默认共享同一个线程池。
- 同一个类的所有方法默认使用 HystrixCommand / HystrixObservableCommand 的
- 如果 HystrixCommand / HystrixObservableCommand 没有显式指定
(2) 独立线程池的情况
- 如果希望同一个类里的每个方法都使用独立的线程池,需要显式指定不同的
threadPoolKey,比如:1
2
3
4
5
public String getUser() { ... }
public String getOrder() { ... }
- 如果希望同一个类里的每个方法都使用独立的线程池,需要显式指定不同的
线程池的核心参数
| 参数 | 默认值 | 说明 |
|---|---|---|
coreSize | 10 | 线程池核心线程数(最大并发线程数)。 |
maxQueueSize | -1 | 等待队列长度,-1 表示不使用队列,线程满直接拒绝。 |
queueSizeRejectionThreshold | 5 | 当使用队列时,超过该阈值直接拒绝。因为 maxQueueSize 不允许热修改,因此提供这个可以热修改的参数,用于动态控制队列的最大大小。 |
keepAliveTimeMinutes | 1 | 允许非核心线程空闲存活时间(分钟)。 |
allowMaximumSizeToDivergeFromCoreSize | false | 是否允许 maximumSize 超过 coreSize。 |
maximumSize | 10 | 最大线程数(当 allowMaximumSizeToDivergeFromCoreSize=true 时生效)。 |
executionTimeoutInMilliseconds | 1000 | 任务执行的超时时间(毫秒),可通过 withExecutionTimeoutInMilliseconds() 设置。建议设置大一些,否则任务在队列中等待过久可能会直接超时,无法进入线程池执行。 |
特别注意
在使用 Hystrix 的线程池隔离时,线程池的等待队列默认是关闭的,只有手动指定等待队列的长度(大小)后才会启用等待队列。
线程池隔离的实现细节
在 Hystrix 的线程池隔离中,HystrixCommand 的 execute() 的底层其实是把命令(Command)封装成一个 FutureTask 并提交到线程池(ThreadPoolExecutor)去执行。而 Java 的线程池执行规则(ThreadPoolExecutor.execute())决定了 Hystrix 的任务是直接交给空闲线程执行还是先放入队列,具体逻辑是这样的:
- (1) 如果线程池中正在运行的线程数 <
corePoolSize- 会直接创建一个新线程来执行任务(即使有空闲线程也不会复用)。
- (2) 如果线程数 ≥
corePoolSize- 任务会尝试进入队列(Hystrix 默认用的是
SynchronousQueue或BlockingQueue)。
- 任务会尝试进入队列(Hystrix 默认用的是
- (3) 如果队列已满且线程数 <
maximumPoolSize- 会再创建新线程来执行任务。
- (4) 如果队列已满且线程数 ≥
maximumPoolSize- 任务会被拒绝,抛出
HystrixRuntimeException,在 Hystrix 中就会触发 fallback。
- 任务会被拒绝,抛出
线程池 + 服务 + 接口划分
HystrixCommand 的
commandKey、groupKey、threadPoolKey的区别:
commandKey- 定义:
- 代表某一个具体的命令(Command),是命令的唯一标识,一般对应依赖服务的某个接口调用。
- 作用:
- 熔断、降级、限流的最小单位,比如:断路器的状态统计是基于
commandKey的。 - 在
groupKey下进一步细分,可以在 Dashboard 监控中,看到每个 Command 的请求数、错误率、超时、熔断状态等统计信息。
- 熔断、降级、限流的最小单位,比如:断路器的状态统计是基于
- 默认值:
- 如果不显式指定
commandKey,其默认值有两种情况:- 继承方式:如果在继承 HystrixCommand 类时,在构造函数里没有显式指定 HystrixCommandKey,那么
commandKey默认是继承类的类名。 - 注解方式:如果在使用
@HystrixCommand注解时,未显式指定commandKey,那么commandKey默认是方法名。
- 继承方式:如果在继承 HystrixCommand 类时,在构造函数里没有显式指定 HystrixCommandKey,那么
- 如果不显式指定
- 典型使用:
1
HystrixCommandKey.Factory.asKey("GetOrderDetail")
- 定义:
groupKey- 定义:
- 代表一类命令(Command)的逻辑分组,即将一组相关的命令归类,一般对应某个依赖服务。
- 作用:
- Dashboard、Metrics 中的监控图表、请求统计,都是按
groupKey进行聚合显示。
- Dashboard、Metrics 中的监控图表、请求统计,都是按
- 默认值:
- 如果不显式指定
threadPoolKey,则groupKey也会作为默认的threadPoolKey来使用。 - 如果不显式指定
groupKey,则groupKey默认是方法所在类名,即同一个类里的所有方法默认共享同一个线程池。
- 如果不显式指定
- 推荐用法:
- 按依赖服务的维度来划分 Group,比如
OrderServiceGroup。 - 一个依赖服务可能暴露多个接口,每个接口对应一个
commandKey,但默认会落在 Group 级别的同一个线程池里。
- 按依赖服务的维度来划分 Group,比如
- 典型使用:
1
HystrixCommandGroupKey.Factory.asKey("OrderServiceGroup")
- 定义:
threadPoolKey- 定义:
- 代表一个独立的 HystrixThreadPool(线程池)。
- 作用:
- 线程池隔离:指定命令(Command)使用哪个线程池。
- 资源隔离粒度:同一个
threadPoolKey下的命令共享同一个线程池,彼此之间可能会互相影响;不同的threadPoolKey对应不同的线程池,互不干扰。
- 默认值:
- 如果没有显式指定
threadPoolKey,则默认会使用groupKey作为threadPoolKey。
- 如果没有显式指定
- 推荐用法:
- 共享线程池:默认情况下,同一个依赖服务下的所有接口(同一个
groupKey下的多个commandKey)共享同一个线程池。 - 独立线程池:如果一个依赖服务下某些接口的流量或耗时差异很大,可以给它们单独定义
threadPoolKey,即使用单独的线程池,避免 “拖垮” 其他接口的调用。
- 共享线程池:默认情况下,同一个依赖服务下的所有接口(同一个
- 典型使用:
1
HystrixThreadPoolKey.Factory.asKey("OrderServiceThreadPool")
- 定义:
HystrixCommand 的
commandKey、groupKey、threadPoolKey的层级关系:
commandKey:一个命令的唯一标识 = 一个接口。groupKey:多个接口命令的逻辑集合 = 一个服务。threadPoolKey:为一个或一组命令提供资源隔离 = 一个线程池。
HystrixCommand 的
commandKey、groupKey、threadPoolKey的最佳实践:
- 一般情况下:
- 一个依赖服务对应一个
groupKey,并共享同一个线程池。
- 一个依赖服务对应一个
- 特殊情况下:
- 如果一个依赖服务下的不同接口的 QPS、耗时差异很大,建议拆分线程池,给部分
commandKey定义独立的threadPoolKey,即使用单独的线程池,避免资源互相争抢。
- 如果一个依赖服务下的不同接口的 QPS、耗时差异很大,建议拆分线程池,给部分
- 监控与报警:
- 可以按
groupKey聚合显示服务整体调用情况。 - 也可以按
commandKey查看具体接口的性能和错误率。
- 可以按
总结说明
commandKey→ 接口级别(熔断 / 降级 / 限流 / 监控最小单位)groupKey→ 服务级别(逻辑分组 + 默认线程池)threadPoolKey→ 线程池级别(资源隔离)
自定义线程池的参数
第一种方式:使用
@HystrixCommand注解配置线程池参数
1 |
|
第二种方式:使用全局默认配置 + 按需覆盖
通过 application.yml 配置全局默认值,仅在特殊参数上覆盖:
1 | hystrix: |
然后方法上只保留关键内容:
1 |
|
第三种方式:按服务 / 业务分组配置
为不同业务指定不同 commandKey、threadPoolKey,并在 application.yml 中集中管理:
1 | hystrix: |
然后方法上这样写:
1 |
|
第四种方式:封装通用注解 / 切面
使用自定义注解 + AOP,减少重复代码:
1 |
|
然后方法上这样写:
1 |
|
