Resilience4j熔断器下游恢复后无法从半开状态切回闭合状态
问题背景
我部署了order-service、inventory-service两个微服务,order-service会调用inventory-service校验订购商品的库存,仅当订单请求内所有商品均有库存时才会完成下单。
- 当
inventory-service宕机或响应过慢时,熔断器逻辑触发 - 因商品缺库存导致的下单失败不属于熔断器的失败判定场景
在熔断器从未从闭合状态切换到半开状态前,上述逻辑均符合预期。
异常现象
- 若连续5次以上请求均因商品缺库存下单失败,熔断器不会切换到开路状态,符合预期
- 将
inventory-service停服后,3次失败请求会触发熔断器切换到开路状态,随后自动切换到半开状态,也符合预期 - 但当
inventory-service恢复服务后,若发起的所有请求均包含一个或多个缺库存商品,熔断器会持续停留在half_open状态,不符合预期
缺库存属于业务正常场景,本应累加成功缓冲调用计数,但实际并未生效;从actuator监控信息来看,这类调用既没有被计为失败,也没有被计为成功。
补充说明:如果发起足够多的库存充足、可正常下单的请求,熔断器可以正常切回闭合状态。疑问点:即使所有请求都是缺库存的业务场景,被配置为忽略的异常难道不应该算作成功调用吗?
相关配置与代码
熔断器配置(application.properties)
resilience4j.circuitbreaker.instances.inventory.register-health-indicator=true resilience4j.circuitbreaker.instances.inventory.event-consumer-buffer-size=10 resilience4j.circuitbreaker.instances.inventory.sliding-window-type=COUNT_BASED resilience4j.circuitbreaker.instances.inventory.sliding-window-size=5 resilience4j.circuitbreaker.instances.inventory.failure-rate-threshold=50 resilience4j.circuitbreaker.instances.inventory.wait-duration-in-open-state=5s resilience4j.circuitbreaker.instances.inventory.permitted-number-of-calls-in-half-open-state=3 resilience4j.circuitbreaker.instances.inventory.automatic-transition-from-open-to-half-open-enabled=true resilience4j.circuitbreaker.instances.inventory.ignore-exceptions=com.mayrevision.orderservice.exception.OrderItemNotFoundException
配置修改记录:已将原配置项
resilience4j.circuitbreaker.instances.inventory.ignore-exceptions[0]=com.mayrevision.orderservice.exception.OrderItemNotFoundException修改为上述无索引的配置格式。
自定义异常处理器
@ControllerAdvice @ResponseStatus public class OrderItemNotFoundExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(OrderItemNotFoundException.class) public ResponseEntity<ErrorResponse> getErrorMessage(OrderItemNotFoundException exception) { ErrorResponse response = new ErrorResponse(HttpStatus.NOT_ACCEPTABLE, exception.getMessage()); return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(response); } }
控制器层代码
@PostMapping @ResponseStatus(HttpStatus.CREATED) @CircuitBreaker(name = "inventory", fallbackMethod = "fallBackMethod") public String placeOrder(@RequestBody OrderRequest orderRequest) throws OrderItemNotFoundException { orderService.placeOrder(orderRequest); return "Order placed successfully"; } public String fallBackMethod(OrderRequest orderRequest, RuntimeException runtimeException) { return "The order could not be placed. Please try back after some time."; }
问题原因与解决方案
根本原因
Resilience4j 熔断器的ignore-exceptions配置逻辑不是将标记的异常算作成功调用,而是直接将这类异常从统计维度完全排除:既不计入失败调用,也不计入成功调用,也不会占用半开状态下的许可调用计数配额。
半开状态下熔断器的状态切换逻辑是:统计permitted-number-of-calls-in-half-open-state配置的许可数量内的有效调用结果,当失败率低于阈值时切回闭合状态,高于阈值则切回开路状态。你配置的半开许可数是3,而所有抛出OrderItemNotFoundException的请求都被直接排除在统计之外,永远凑不够3个有效统计调用,自然会一直卡在半开状态。
闭合状态下滑动窗口统计是按计数滚动的,被忽略的异常不占窗口计数,连续抛出被忽略异常时窗口内没有失败记录,所以不会触发开路,这也是之前闭合状态下逻辑符合预期的原因。
另外当前的降级方法签名也存在问题:降级方法只声明了捕获RuntimeException类型参数,但OrderItemNotFoundException如果是受检异常(直接继承Exception而非RuntimeException)的话,抛出时根本不会进入降级逻辑,不过这不是当前卡半开的核心原因。
修复方案
- 不要用ignore-exceptions处理业务预期异常:业务上的库存不足属于正常业务返回,不应该用抛出异常的方式返回给切面层,建议调整业务逻辑:库存不足时直接返回对应的业务结果,而非抛出
OrderItemNotFoundException,让熔断器切面能捕获到正常返回,计入成功调用计数。 - 如果确实要保留异常抛出逻辑,不要把这类业务异常配置到
ignore-exceptions中,而是通过record-exceptions明确指定需要判定为失败的异常类型(比如服务调用超时异常、连接异常等),其余异常默认都会被判定为业务正常完成,计入成功计数,就能正常触发半开状态的统计判断。 - 修正降级方法签名:保证降级方法的异常参数可以覆盖所有可能从切面抛出的异常类型,避免异常穿透。
内容的提问来源于stack exchange,提问作者Chan

