Java 服务重试机制方案详解
一、引言
1.1 为什么需要重试机制
在分布式系统和微服务架构中,服务调用失败是不可避免的。失败原因可能包括:
- 网络不稳定:网络抖动、连接超时、服务暂时不可达
- 资源竞争:数据库乐观锁冲突、并发限流触发
- 外部依赖故障:第三方服务宕机、API 调用失败
- 瞬时故障:数据库连接池耗尽、服务重启期间
重试机制的核心价值在于:将暂时性故障转化为最终成功,提升系统鲁棒性。
1.2 常见应用场景
| 场景类型 | 典型示例 | 重试必要性 |
|---|---|---|
| 数据库操作 | 乐观锁冲突、死锁检测 | 高并发场景必然发生 |
| 外部服务调用 | HTTP API、RPC 调用 | 网络不可靠性 |
| 消息处理 | MQ 消费失败 | 消息不能丢失 |
| 分布式事务 | 跨服务协调失败 | 最终一致性保障 |
| 资源获取 | 分布式锁获取失败 | 竞争场景常见 |
二、同步重试方案
方案一:Spring Retry
概述
Spring Retry 是 Spring 官方提供的重试框架,基于 AOP 实现,通过注解声明式配置重试策略。它与 Spring 事务管理无缝集成,能够捕获事务提交时抛出的异常。
核心原理:通过代理拦截目标方法,捕获指定异常后按策略重试。
优缺点
| 优点 | 缺点 |
|---|---|
| 注解驱动,配置简洁 | 同步阻塞,占用线程资源 |
| 与 Spring 事务无缝集成 | 不支持复杂重试条件判断 |
| 支持多种退避策略 | 重试期间无法处理其他请求 |
| Spring Boot(需显式引入) | 仅适用于短暂性故障 |
适用场景
- 数据库乐观锁冲突重试
- 短暂网络故障恢复
- 数据库连接池瞬时耗尽
- 与
@Transactional配合的事务内重试
使用示例:乐观锁冲突重试
@SpringBootApplication
@EnableRetry // 启用重试功能
public class Application { }
@Service
public class OrderService {
/**
* 乐观锁重试示例
* maxAttempts: 最大重试次数
* backoff: 退避策略,延迟递增
*/
@Retryable(
value = OptimisticLockingFailureException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2)
)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateOrderStatus(String orderId, String status) {
Order order = orderRepository.findById(orderId);
order.setStatus(status);
orderRepository.save(order); // 可能抛出乐观锁异常
}
/**
* 重试失败后的兜底处理
*/
@Recover
public void recoverUpdate(OptimisticLockingFailureException e, String orderId) {
log.error("订单状态更新失败,超过最大重试次数: {}", orderId);
// 发送告警或记录失败日志
}
}
关键配置说明:
@EnableRetry:启动类启用重试功能value:指定重试的异常类型maxAttempts:最大重试次数(默认3次)backoff:退避策略,避免立即重试加剧竞争@Recover:重试耗尽后的兜底方法
@Backoff 退避策略参数详解:
@Backoff 注解支持以下参数:
| 参数 | 含义 | 默认值 | 说明 |
|---|---|---|---|
delay |
初始延迟时间 | 1000ms | 第一次重试前的等待时间(毫秒) |
multiplier |
乘数因子 | 0(不递增) | 每次重试后延迟时间乘以该值,实现指数退避 |
maxDelay |
最大延迟上限 | 无上限 | 限制延迟时间的最大值,防止无限增长 |
延迟计算公式:第 n 次重试的延迟时间 = delay × multiplier^(n-1)
示例说明:
@Backoff(delay = 100, multiplier = 2)
// 重试延迟计算:
// 第1次重试:等待 100ms (100 × 2^0)
// 第2次重试:等待 200ms (100 × 2^1)
// 第3次重试:等待 400ms (100 × 2^2)
@Backoff(delay = 500, multiplier = 1.5, maxDelay = 5000)
// 重试延迟计算:
// 第1次重试:等待 500ms
// 第2次重试:等待 750ms (500 × 1.5)
// 第3次重试:等待 1125ms (750 × 1.5)
// 第4次重试:等待 1687ms (1125 × 1.5)
// 第5次重试:等待 2500ms (1687 × 1.5,不超过 maxDelay)
// ...后续最大等待 5000ms
提示:
multiplier > 1时为指数退避,multiplier = 1时为固定间隔,不设置multiplier时默认不递增。
依赖引入:
标准 Spring Boot 项目需显式引入 spring-retry 依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
注意:Spring Retry 不在 Spring Boot 默认 starter 中,需显式引入。但以下情况可能无需手动引入:
- 父 POM 已定义:项目继承的父 POM 已包含
spring-retry- 内部模块传递:公司内部公共模块已依赖
spring-retry可通过
mvn dependency:tree命令查看依赖树,确认是否已存在传递依赖。
版本兼容说明:
@Retryable 注解属性在不同版本中存在差异:
| 版本 | 指定异常属性 | 说明 |
|---|---|---|
| Spring Retry 1.x | include |
旧版本语法 |
| Spring Retry 1.2+ | value |
推荐使用 |
| Spring Retry 2.x | retryFor |
新版本别名 |
建议:统一使用 value 属性,兼容所有版本。
@Recover 方法签名规则:
兜底方法必须遵循以下签名规范:
- 第一个参数必须是异常类型(与
@Retryable指定的异常一致) - 后续参数必须与原方法参数一致(顺序、类型、数量)
// 正确示例
@Recover
public void recoverUpdate(OptimisticLockingFailureException e, String orderId) { }
// 错误示例:参数顺序不一致
@Recover
public void recoverUpdate(String orderId, OptimisticLockingFailureException e) { } // ❌
多 @Retryable 场景下的 @Recover 使用:
当一个 Service 中存在多个 @Retryable 方法,且异常类型不同时,有两种处理方式:
方式一:每个异常类型单独定义兜底方法(推荐)
Spring Retry 会根据异常类型自动匹配对应的 @Recover 方法:
@Service
public class OrderService {
// 方法1:乐观锁重试
@Retryable(value = OptimisticLockingFailureException.class, maxAttempts = 3)
public void updateOrder(String orderId) { }
// 方法2:支付重试
@Retryable(value = PaymentException.class, maxAttempts = 5)
public void processPayment(String orderId) { }
// 方法3:网络重试(多个异常)
@Retryable(value = {ConnectException.class, SocketTimeoutException.class}, maxAttempts = 3)
public void callExternalApi(String orderId) { }
// 兜底方法1:处理 OptimisticLockingFailureException
@Recover
public void recoverUpdate(OptimisticLockingFailureException e, String orderId) {
log.error("订单更新失败(乐观锁): {}", orderId);
alertService.notify("乐观锁冲突", orderId);
}
// 兜底方法2:处理 PaymentException
@Recover
public void recoverPayment(PaymentException e, String orderId) {
log.error("支付处理失败: {}", orderId);
paymentFailureService.record(orderId, e.getMessage());
}
// 兜底方法3:处理网络异常(父类 Exception 可匹配多种子类)
@Recover
public void recoverApiCall(Exception e, String orderId) {
log.error("外部API调用失败: {}, 异常类型: {}", orderId, e.getClass().getSimpleName());
// 可根据异常类型进一步细分处理
if (e instanceof ConnectException) {
// 连接失败处理
} else if (e instanceof SocketTimeoutException) {
// 超时处理
}
}
}
方式二:统一兜底方法(按异常类型分发)
如果多个方法参数相同,可使用一个统一的方法处理:
@Service
public class OrderService {
@Retryable(value = OptimisticLockingFailureException.class)
public void updateOrder(String orderId) { }
@Retryable(value = PaymentException.class)
public void processPayment(String orderId) { }
// 统一兜底方法(参数为 Exception 父类)
@Recover
public void recoverAll(Exception e, String orderId) {
log.error("操作失败: {}, 异常: {}", orderId, e.getClass().getSimpleName());
// 根据异常类型分发处理
if (e instanceof OptimisticLockingFailureException) {
// 乐观锁失败处理
alertService.notify("乐观锁冲突", orderId);
} else if (e instanceof PaymentException) {
// 支付失败处理
paymentFailureService.record(orderId, e.getMessage());
} else {
// 其他异常通用处理
generalFailureService.record(orderId, e);
}
}
}
@Recover 匹配规则:
| 规则 | 说明 |
|---|---|
| 异常类型匹配 | Spring Retry 按异常类型选择最匹配的 @Recover 方法 |
| 精确匹配优先 | 如果存在精确异常类型的 @Recover,优先使用 |
| 父类匹配 | 无精确匹配时,使用能接收该异常的父类 @Recover |
| 参数匹配 | @Recover 方法的后续参数必须与原方法一致 |
提示:建议使用方式一(单独定义兜底方法),代码更清晰、更易维护。方式二适用于参数相同、处理逻辑相似的场景。
方案二:Guava Retryer
概述
Guava Retryer(实际来自 guava-retrying 库)提供灵活的程序化重试构建器,支持基于异常、基于返回结果等多种重试条件判断。
核心原理:通过 Builder 模式构建 Retryer,执行时按配置策略判断是否重试。
优缺点
| 优点 | 缺点 |
|---|---|
| 灵活的重试条件判断 | 需要手动编码,非注解驱动 |
| 支持基于返回值判断 | 需引入额外依赖 |
| 多种等待策略(固定、递增、随机) | 与 Spring 集成需自行处理 |
| 支持重试监听器 | 代码侵入性较高 |
适用场景
- 需要根据返回值判断是否重试(如返回 null、特定状态码)
- 复杂重试逻辑组合
- 非 Spring 项目
- 需要精细控制重试过程
依赖引入
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
注意:此库虽以 "guava" 命名,但不是 Google Guava 官方组件,而是基于 Guava 的第三方重试库。
使用示例
import com.github.rholder.retry.*;
public class PaymentService {
private final Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
// 根据异常重试
.retryIfExceptionOfType(PaymentException.class)
// 根据返回值重试(返回 false 时重试)
.retryIfResult(Predicates.equalTo(false))
// 固定间隔等待
.withWaitStrategy(WaitStrategies.fixedWait(500, TimeUnit.MILLISECONDS))
// 最大重试5次
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
// 重试监听器
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
log.info("第 {} 次重试", attempt.getAttemptNumber());
}
})
.build();
public boolean processPayment(PaymentRequest request) {
try {
return retryer.call(() -> paymentClient.pay(request));
} catch (Exception e) {
log.error("支付处理失败", e);
return false;
}
}
}
方案三:Resilience4j 容错框架
概述
Resilience4j 是轻量级容错库,设计为 Netflix Hystrix 的替代方案。它提供模块化的容错组件,可灵活组合使用。
核心原理:通过装饰器模式包装函数,支持多层容错策略组合。
五大核心组件:
| 组件 | 功能 | 核心作用 |
|---|---|---|
| Retry | 重试机制 | 处理短暂性故障 |
| CircuitBreaker | 熔断器 | 防止级联失败,快速失败 |
| RateLimiter | 限流器 | 控制请求速率,保护下游服务 |
| Bulkhead | 隔离舱 | 并发控制,资源隔离 |
| TimeLimiter | 超时控制 | 限制执行时间,防止长时间阻塞 |
优缺点
| 优点 | 缺点 |
|---|---|
| 模块化设计,按需引入 | 配置相对复杂 |
| 支持组件灵活组合 | 学习成本较高 |
| 轻量级,性能优于 Hystrix | 文档相对较少 |
| 支持 Spring Boot Starter | 监控需额外集成 |
| 函数式编程风格 | 注解方式灵活性有限 |
适用场景
- 微服务架构容错保护
- 外部服务调用保护
- API 网关限流熔断
- 高并发系统资源保护
- 防止级联失败(雪崩效应)
核心组件详解
1. Retry(重试)
作用:处理短暂性故障,通过重试将暂时失败转化为成功。
核心参数:
| 参数 | 含义 | 默认值 | 推荐值 |
|---|---|---|---|
maxAttempts |
最大重试次数(含首次调用) | 3 | 3-5 |
waitDuration |
重试等待时间 | 500ms | 根据场景调整 |
exponentialBackoffMultiplier |
指数退避乘数 | 1(不递增) | 2 |
retryExceptions |
触发重试的异常类型 | 所有异常 | 指定具体异常 |
ignoreExceptions |
不触发重试的异常类型 | 无 | 业务异常 |
使用场景:
- 网络抖动导致连接失败
- 服务暂时不可达(503)
- 数据库连接池瞬时耗尽
- 乐观锁冲突
2. CircuitBreaker(熔断器)
作用:当失败率达到阈值时"熔断",快速失败,防止级联雪崩。
熔断器状态流转:
CLOSED(关闭) ──失败率超阈值──→ OPEN(打开)
↑ │
│ │ 等待时间后
│ ↓
│ HALF_OPEN(半开)
│ │
│ │
└─────探测成功────←────探测失败────┘
核心参数:
| 参数 | 含义 | 默认值 | 推荐值 |
|---|---|---|---|
failureRateThreshold |
失败率阈值(百分比) | 50% | 50-80% |
slidingWindowSize |
滑动窗口大小(调用次数) | 100 | 50-100 |
slidingWindowType |
滑动窗口类型 | COUNT_BASED | COUNT_BASED |
waitDurationInOpenState |
熔断等待时间 | 60000ms | 10-60s |
permittedNumberOfCallsInHalfOpenState |
半开状态允许调用次数 | 10 | 5-10 |
minimumNumberOfCalls |
计算失败率的最小调用次数 | 100 | 10-50 |
使用场景:
- 外部服务故障保护(如支付网关)
- 下游服务响应缓慢
- 防止级联失败传播
- API 网关熔断保护
3. RateLimiter(限流器)
作用:控制请求速率,防止下游服务过载。
限流原理:基于令牌桶/信号量,按时间周期分配调用许可。
核心参数:
| 参数 | 含义 | 默认值 | 推荐值 |
|---|---|---|---|
limitForPeriod |
一个周期内允许的调用次数 | 50 | 根据下游容量 |
limitRefreshPeriod |
许可刷新周期 | 500ns | 1s-1min |
timeoutDuration |
等待许可的超时时间 | 5s | 0-5s |
使用场景:
- API 接口限流(防刷)
- 保护下游服务(如第三方 API)
- 按用户/租户限流
- 防止突发流量冲击
4. Bulkhead(隔离舱)
作用:限制并发调用数,隔离不同服务的资源,防止一个服务耗尽所有资源。
两种实现方式:
| 方式 | 原理 | 适用场景 |
|---|---|---|
| Semaphore(信号量) | 限制并发线程数 | 无需线程池的场景 |
| ThreadPool(线程池) | 使用独立线程池 | 需要真正隔离的场景 |
核心参数:
| 参数 | 含义 | 默认值 | 推荐值 |
|---|---|---|---|
maxConcurrentCalls |
最大并发调用数 | 25 | 根据服务容量 |
maxWaitDuration |
等待许可的最大时间 | 0(不等待) | 0-5s |
coreThreadPoolSize |
核心线程数(线程池方式) | - | 根据场景 |
queueCapacity |
队列容量(线程池方式) | - | 100 |
使用场景:
- 核心服务资源隔离(如支付服务)
- 防止慢服务拖垮整个系统
- 不同优先级服务隔离
- 外部调用与内部调用隔离
5. TimeLimiter(超时控制)
作用:限制方法执行时间,防止长时间阻塞。
核心参数:
| 参数 | 含义 | 默认值 | 推荐值 |
|---|---|---|---|
timeoutDuration |
超时时间 | 1s | 根据场景 |
cancelRunningFuture |
超时后是否取消正在执行的任务 | true | true |
使用场景:
- 外部 HTTP 调用超时保护
- RPC 调用超时控制
- 防止线程长时间阻塞
组合使用案例
案例1:Retry + CircuitBreaker(重试 + 熔断)
场景:调用外部支付网关,需要重试短暂故障,但连续失败时熔断保护。
效果:
- 短暂网络故障 → Retry 重试恢复
- 服务持续故障 → CircuitBreaker 熔断,快速失败
- 熔断后 → 等待时间后自动探测恢复
@Service
public class PaymentGatewayService {
/**
* 调用支付网关(重试 + 熔断组合)
* 注解执行顺序:CircuitBreaker → Retry(从外到内,按照注解声明顺序装饰执行)
*/
@CircuitBreaker(name = "paymentGateway", fallbackMethod = "paymentFallback")
@Retry(name = "paymentGateway", fallbackMethod = "paymentFallback")
public PaymentResult processPayment(PaymentRequest request) {
log.info("调用支付网关: {}", request.getOrderId());
return paymentClient.process(request);
}
/**
* 兜底方法:熔断或重试耗尽时执行
*/
private PaymentResult paymentFallback(PaymentRequest request, Exception e) {
log.warn("支付网关调用失败,执行兜底: {}", e.getMessage());
// 返回待处理状态,后续通过定时任务补偿
return PaymentResult.pending(request.getOrderId(), "支付服务暂时不可用");
}
}
// application.yml 配置
resilience4j:
retry:
instances:
paymentGateway:
maxAttempts: 3
waitDuration: 500ms
exponentialBackoffMultiplier: 2
retryExceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
circuitbreaker:
instances:
paymentGateway:
failureRateThreshold: 50 # 失败率50%触发熔断
slidingWindowSize: 10 # 最近10次调用计算失败率
minimumNumberOfCalls: 5 # 至少5次调用才开始计算
waitDurationInOpenState: 30s # 熔断后等待30秒
permittedNumberOfCallsInHalfOpenState: 3 # 半开状态允许3次探测
案例2:Retry + RateLimiter(重试 + 限流)
场景:调用第三方 API,需限流防止超配额,限流拒绝时重试等待。
效果:
- 正常请求 → RateLimiter 放行
- 超出限流 → RateLimiter 拒绝,Retry 触发重试等待许可
- 重试耗尽 → 返回兜底结果
@Service
public class ThirdPartyApiService {
/**
* 调用第三方API(限流 + 重试组合)
* 注解执行顺序: RateLimiter → Retry(从外到内,按照注解声明顺序装饰执行)
*/
@RateLimiter(name = "thirdPartyApi", fallbackMethod = "rateLimitFallback")
@Retry(name = "thirdPartyApi", fallbackMethod = "retryFallback")
public ApiResponse callThirdParty(String param) {
log.info("调用第三方API: {}", param);
return thirdPartyClient.request(param);
}
/**
* 限流降级:直接返回,不触发重试
*/
private ApiResponse rateLimitFallback(String param, RequestNotPermitted e) {
log.warn("触发限流: {}", param);
return ApiResponse.defaultResponse("系统繁忙,请稍后重试");
}
/**
* 重试降级:业务调用失败后重试耗尽才执行
*/
private ApiResponse retryFallback(String param, Exception e) {
log.warn("重试耗尽: {}", e.getMessage());
return ApiResponse.defaultResponse("服务暂时不可用");
}
}
// application.yml 配置
resilience4j:
retry:
instances:
thirdPartyApi:
maxAttempts: 3
waitDuration: 1s
exponentialBackoffMultiplier: 2
ratelimiter:
instances:
thirdPartyApi:
limitForPeriod: 10 # 每周期允许10次调用
limitRefreshPeriod: 1s # 每秒刷新周期
timeoutDuration: 0 # 不等待许可,直接拒绝
案例3:Retry + Bulkhead(重试 + 隔离)
场景:支付服务需要资源隔离,限制并发,失败时重试。
效果:
- 正常请求 → Bulkhead 放行
- 并发超限 → Bulkhead 拒绝,Retry 触发重试等待
- 重试耗尽 → 返回兜底结果
@Service
public class IsolatedPaymentService {
/**
* 支付处理(隔离 + 重试组合)
* 限制支付服务并发,防止拖垮整个系统
*/
@Bulkhead(name = "isolatedPayment", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "bulkheadFallback")
@Retry(name = "isolatedPayment", fallbackMethod = "retryFallback")
public PaymentResult processPayment(PaymentRequest request) {
log.info("处理支付请求: {}", request.getOrderId());
return paymentCoreService.process(request);
}
/**
* 隔离舱降级:并发超限直接返回
*/
private PaymentResult bulkheadFallback(PaymentRequest request, BulkheadFullException e) {
log.warn("支付服务并发超限: {}", request.getOrderId());
return PaymentResult.pending(request.getOrderId(), "支付服务繁忙,请稍后重试");
}
/**
* 重试降级:业务异常重试耗尽后执行
*/
private PaymentResult retryFallback(PaymentRequest request, Exception e) {
log.warn("支付处理失败(重试耗尽): {}", request.getOrderId(), e);
return PaymentResult.pending(request.getOrderId(), "支付服务暂时不可用");
}
}
// application.yml 配置
resilience4j:
retry:
instances:
isolatedPayment:
maxAttempts: 3
waitDuration: 200ms
bulkhead:
instances:
isolatedPayment:
maxConcurrentCalls: 5 # 最大5个并发调用
maxWaitDuration: 0 # 不等待许可
案例4:全组件组合(完整容错保护)
场景:核心业务调用外部服务,需要完整容错保护。
效果:
- RateLimiter → 限制调用速率
- Bulkhead → 限制并发数
- TimeLimiter → 限制执行时间
- CircuitBreaker → 失败率高时熔断
- Retry → 短暂故障重试
@Service
public class CoreBusinessService {
/**
* 核心业务调用(完整容错组合)
* 注解执行顺序(从外到内):Retry → CircuitBreaker → RateLimiter → Bulkhead → TimeLimiter
*/
@Retry(name = "coreBusiness", fallbackMethod = "businessFallback")
@CircuitBreaker(name = "coreBusiness")
@RateLimiter(name = "coreBusiness")
@Bulkhead(name = "coreBusiness", type = Bulkhead.Type.SEMAPHORE)
@TimeLimiter(name = "coreBusiness")
public CompletableFuture<BusinessResult> callExternalService(BusinessRequest request) {
log.info("执行核心业务: {}", request.getId());
return CompletableFuture.supplyAsync(() -> externalService.process(request));
}
private BusinessResult businessFallback(BusinessRequest request, Exception e) {
log.error("核心业务调用失败: {}", e.getMessage());
// 记录失败,后续补偿
failureRecordService.record(request, e);
return BusinessResult.failed("服务暂时不可用,已记录待补偿");
}
}
// application.yml 完整配置
resilience4j:
retry:
instances:
coreBusiness:
maxAttempts: 3
waitDuration: 500ms
exponentialBackoffMultiplier: 2
circuitbreaker:
instances:
coreBusiness:
failureRateThreshold: 50
slidingWindowSize: 20
minimumNumberOfCalls: 10
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 5
ratelimiter:
instances:
coreBusiness:
limitForPeriod: 20
limitRefreshPeriod: 1s
timeoutDuration: 0
bulkhead:
instances:
coreBusiness:
maxConcurrentCalls: 10
maxWaitDuration: 0
timelimiter:
instances:
coreBusiness:
timeoutDuration: 5s
cancelRunningFuture: true
YAML 配置详解
推荐配置模板:
resilience4j:
# 重试配置
retry:
configs:
default: # 默认配置
maxAttempts: 3
waitDuration: 500ms
exponentialBackoffMultiplier: 2
retryExceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
- java.io.IOException
ignoreExceptions:
- com.yotexs.BusinessException # 业务异常不重试
instances:
paymentService:
baseConfig: default # 继承默认配置
maxAttempts: 5 # 覆盖重试次数
orderService:
baseConfig: default
# 熔断器配置
circuitbreaker:
configs:
default:
failureRateThreshold: 50
slidingWindowType: COUNT_BASED
slidingWindowSize: 100
minimumNumberOfCalls: 10
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 10
recordExceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
ignoreExceptions:
- com.yotexs.BusinessException
instances:
criticalService: # 关键服务配置更严格
failureRateThreshold: 30 # 失败率30%就熔断
slidingWindowSize: 50
waitDurationInOpenState: 60s # 熔断等待更长
# 限流器配置
ratelimiter:
configs:
default:
limitRefreshPeriod: 1s
limitForPeriod: 50
timeoutDuration: 0
instances:
apiGateway:
limitForPeriod: 100 # API网关限流更宽松
# 隔离舱配置
bulkhead:
configs:
default:
maxConcurrentCalls: 25
maxWaitDuration: 0
instances:
paymentService:
maxConcurrentCalls: 10 # 支付服务并发限制更严格
# 超时配置
timelimiter:
configs:
default:
timeoutDuration: 5s
cancelRunningFuture: true
instances:
slowService:
timeoutDuration: 30s # 慢服务超时更长
装饰器模式(函数式编程)
除注解方式外,Resilience4j 还支持函数式编程风格:
@Service
public class FunctionalStyleService {
private final Retry retry;
private final CircuitBreaker circuitBreaker;
private final RateLimiter rateLimiter;
private final Bulkhead bulkhead;
public FunctionalStyleService() {
// 从配置中获取实例
this.retry = Retry.ofDefaults("functionalService");
this.circuitBreaker = CircuitBreaker.ofDefaults("functionalService");
this.rateLimiter = RateLimiter.ofDefaults("functionalService");
this.bulkhead = Bulkhead.ofDefaults("functionalService");
}
/**
* 装饰器模式组合(灵活控制)
*/
public String executeWithDecorators(String param) {
// 组合装饰器:从内到外包装
Supplier<String> supplier = () -> externalService.call(param);
// 依次装饰
Supplier<String> decorated =
TimeLimiter.decorateFutureSupplier(timeout, () -> CompletableFuture.supplyAsync(supplier));
decorated = Bulkhead.decorateSupplier(bulkhead, decorated);
decorated = RateLimiter.decorateSupplier(rateLimiter, decorated);
decorated = CircuitBreaker.decorateSupplier(circuitBreaker, decorated);
decorated = Retry.decorateSupplier(retry, decorated);
// 执行
try {
return decorated.get();
} catch (Exception e) {
return "fallback-result";
}
}
}
提示:函数式编程方式比注解方式更灵活,可动态调整配置,适合复杂场景。
三、异步重试方案
方案四:消息队列延迟重试
概述
消息队列重试利用 MQ 的延迟投递特性,将失败消息延迟后重新消费。常见实现方式:
- RabbitMQ:死信队列(DLX)或延迟消息插件
- RocketMQ:原生延迟消息级别
核心原理:消费失败 → 投递延迟队列 → 延迟到期 → 重新消费。
优缺点
| 优点 | 缺点 |
|---|---|
| 异步非阻塞,不占用业务线程 | 实时性较低 |
| 支持长时间间隔重试 | 需额外队列配置 |
| 消息持久化,不丢失 | 复杂度较高 |
| 可跨服务重试 | 消息顺序可能变化 |
适用场景
- 外部 HTTP/RPC 服务调用失败
- 跨系统消息处理失败
- 需要长时间间隔重试(秒级到小时级)
- 高并发场景避免线程阻塞
RabbitMQ 实现架构
┌─────────────┐ 消费失败 ┌─────────────────┐
│ 业务队列 │ ──────────────→ │ 死信队列(DLX) │
│ (topic) │ │ 设置 TTL 延迟 │
└─────────────┘ └─────────────────┘
↑ │
│ 延迟到期重新投递 │
└───────────────────────────────────┘
关键代码示例
// 消费者配置
@Component
public class OrderConsumer {
@RabbitListener(queues = "order.queue")
public void processOrder(OrderMessage message, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
try {
orderService.process(message);
channel.basicAck(tag, false);
} catch (Exception e) {
// 拒绝消息,路由到死信队列
channel.basicReject(tag, false);
log.warn("订单处理失败,进入重试队列: {}", message.getOrderId());
}
}
@RabbitListener(queues = "order.retry.queue")
public void retryProcess(OrderMessage message, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag,
@Header("x-death") List<Map<String, Object>> deathInfo) {
int retryCount = extractRetryCount(deathInfo);
if (retryCount > 5) {
// 超过最大重试次数,记录失败
failureLogService.record(message, "超过最大重试次数");
channel.basicAck(tag, false);
return;
}
try {
orderService.process(message);
channel.basicAck(tag, false);
} catch (Exception e) {
channel.basicReject(tag, false);
}
}
}
RabbitMQ 队列配置
@Configuration
public class RabbitConfig {
@Bean
public Queue orderQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "retry.exchange");
args.put("x-dead-letter-routing-key", "order.retry");
return new Queue("order.queue", true, false, false, args);
}
@Bean
public Queue retryQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 30000); // 30秒延迟
args.put("x-dead-letter-exchange", "order.exchange");
args.put("x-dead-letter-routing-key", "order.queue");
return new Queue("order.retry.queue", true, false, false, args);
}
}
RocketMQ 延迟消息示例
RocketMQ 提供原生延迟消息支持,无需额外配置死信队列:
@Component
public class RocketMQProducer {
@Autowired
private DefaultMQProducer producer;
/**
* 发送延迟消息
* delayTimeLevel: 延迟级别
* 1: 1s, 2: 5s, 3: 10s, 4: 30s, 5: 1min, 6: 2min, 7: 3min
* 8: 4min, 9: 5min, 10: 6min, 11: 7min, 12: 8min, 13: 9min
* 14: 10min, 15: 20min, 16: 30min, 17: 1h, 18: 2h
*/
public void sendDelayMessage(String topic, Object message, int delayLevel) {
Message msg = new Message(topic, JSON.toJSONString(message).getBytes());
msg.setDelayTimeLevel(delayLevel); // 设置延迟级别
producer.send(msg);
}
}
// 消费者重试示例
/**
* 消息类需包含重试计数字段
*/
public class OrderMessage {
private String orderId;
private Integer retryCount; // 重试次数存储在消息中(不能使用成员变量)
// 其他业务字段...
}
@Component
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-group")
public class RocketMQConsumer implements RocketMQListener<OrderMessage> {
@Autowired
private DefaultMQProducer producer;
@Override
public void onMessage(OrderMessage message) {
try {
orderService.process(message);
} catch (Exception e) {
// 从消息中获取重试次数
int retryCount = message.getRetryCount() != null ? message.getRetryCount() : 0;
if (retryCount < 5) {
retryCount++;
message.setRetryCount(retryCount);
// 重新发送延迟消息(延迟级别递增)
sendDelayMessage("order-topic", message, retryCount);
} else {
// 超过最大重试次数,记录失败
failureLogService.record(message, "超过最大重试次数(5次)");
}
}
}
private void sendDelayMessage(String topic, OrderMessage message, int retryCount) {
Message msg = new Message(topic, JSON.toJSONString(message).getBytes());
// 延迟级别:第1次重试=级别4(30秒),第2次=级别5(1分钟),第3次=级别6(2分钟)...
msg.setDelayTimeLevel(retryCount + 3);
producer.send(msg);
}
}
RabbitMQ vs RocketMQ 对比:
| 特性 | RabbitMQ | RocketMQ |
|---|---|---|
| 延迟实现 | 死信队列 + TTL | 原生延迟级别 |
| 配置复杂度 | 较高(需配置 DLX) | 低(直接设置级别) |
| 延迟精度 | 可精确到毫秒 | 固定级别,精度较低 |
| 重试次数追踪 | 需通过 x-death 头获取 | 需自行维护 |
方案五:定时任务扫描重试
概述
将失败任务持久化到数据库,通过定时任务扫描并重试。适合重要业务场景,需要保证最终成功。
核心原理:失败任务入库 → 定时扫描 → 状态判断 → 重试执行 → 更新状态。
优缺点
| 优点 | 缺点 |
|---|---|
| 持久化存储,可追溯 | 实时性差 |
| 支持长时间间隔 | 需额外定时任务 |
| 支持手动干预 | 数据库压力 |
| 最终一致性保障 | 重试策略相对固定 |
适用场景
- 重要业务操作(支付、结算)
- 必须保证最终成功
- 需要人工介入兜底
- 跨天重试场景
实现思路
数据表设计:
CREATE TABLE retry_task (
id BIGINT PRIMARY KEY,
task_type VARCHAR(50), -- 任务类型
task_data JSON, -- 任务数据
retry_count INT DEFAULT 0, -- 重试次数
max_retry INT DEFAULT 5, -- 最大重试次数
next_retry_time DATETIME, -- 下次重试时间
status VARCHAR(20), -- PENDING/SUCCESS/FAILED
error_message TEXT, -- 错误信息
created_time DATETIME,
updated_time DATETIME
);
定时任务处理:
@Component
public class RetryTaskScheduler {
@Autowired
private RedissonClient redissonClient; // 分布式锁
@Scheduled(cron = "0 */5 * * * ?") // 每5分钟执行
public void processRetryTasks() {
// 分布式锁防止多节点重复执行
RLock lock = redissonClient.getLock("retry:task:lock");
try {
// 尝试获取锁,最多等待5秒,锁持有时间10分钟
if (!lock.tryLock(5, 600, TimeUnit.SECONDS)) {
log.info("未获取到锁,跳过本次执行");
return;
}
List<RetryTask> tasks = retryTaskRepository
.findByStatusAndNextRetryTimeBefore("PENDING", LocalDateTime.now());
for (RetryTask task : tasks) {
processSingleTask(task);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void processSingleTask(RetryTask task) {
try {
executeTask(task);
task.setStatus("SUCCESS");
// 成功后无需增加重试计数
} catch (Exception e) {
task.setRetryCount(task.getRetryCount() + 1);
task.setErrorMessage(e.getMessage());
if (task.getRetryCount() >= task.getMaxRetry()) {
task.setStatus("FAILED");
alertService.notify(task);
} else {
// 设置下次重试时间(指数退避)
int delayMinutes = (int) Math.pow(2, task.getRetryCount());
task.setNextRetryTime(LocalDateTime.now().plusMinutes(delayMinutes));
}
}
retryTaskRepository.save(task);
}
private void executeTask(RetryTask task) {
switch (task.getTaskType()) {
case "PAYMENT":
paymentService.retry(task.getTaskData());
break;
case "NOTIFICATION":
notificationService.retry(task.getTaskData());
break;
// 其他任务类型...
}
}
}
分布式部署注意事项:
多节点部署时,定时任务扫描需考虑:
- 分布式锁:使用 Redis/Redisson 防止重复执行
- 乐观锁更新:使用版本号或 CAS 更新任务状态
- 任务分片:按任务 ID 分片,不同节点处理不同分片
四、方案选型指南
4.1 方案对比表
| 方案 | 执行方式 | 实时性 | 资源占用 | 持久化 | 适用间隔 | 复杂度 |
|---|---|---|---|---|---|---|
| Spring Retry | 同步 | 高 | 高 | 无 | 毫秒级 | 低 |
| Guava Retryer | 同步 | 高 | 高 | 无 | 毫秒级 | 中 |
| Resilience4j | 同步 | 高 | 高 | 无 | 毫秒级 | 中 |
| MQ 延迟重试 | 异步 | 中 | 低 | 有 | 秒级~小时 | 高 |
| 定时任务扫描 | 异步 | 低 | 低 | 有 | 分钟级~天 | 高 |
4.2 选型决策流程
是否需要持久化?
│
┌─────────────┴─────────────┐
│ │
否 是
│ │
是否需要实时重试? 是否需要高实时性?
│ │
┌───────┴───────┐ ┌───────┴───────┐
│ │ │ │
是 否 否 是
│ │ │ │
Spring Retry MQ延迟重试 定时任务扫描 MQ延迟重试
Guava Retryer │ │
Resilience4j 是否需要组合 配较短延迟
熔断/限流?
│
┌───────┴───────┐
│ │
是 否
│ │
Resilience4j Spring Retry
五、最佳实践建议
5.1 重试次数控制
- 短暂性故障:3-5 次
- 外部服务调用:3-5 次,配合熔断
- 重要业务:可设置更多次数,但需设置告警
5.2 退避策略选择
| 策略 | 适用场景 |
|---|---|
| 固定间隔 | 快速恢复场景(乐观锁) |
| 线性递增 | 网络恢复场景 |
| 指数递增 | 外部服务恢复、避免雪崩 |
| 随机抖动 | 高并发场景分散重试时间 |
5.3 重试边界界定
必须重试:
- 网络超时、连接异常
- 服务暂时不可用(503)
- 乐观锁冲突、死锁
- 限流触发(429)
禁止重试:
- 业务校验失败(参数错误)
- 权限不足(401/403)
- 业务规则冲突
- 明确的永久性错误
5.4 兜底处理
任何重试机制都应有兜底方案:
- 记录失败日志:便于问题排查
- 发送告警通知:运维介入处理
- 降级处理:返回默认值或缓存的旧数据
- 人工介入:重要业务支持手动重试
5.5 幂等性设计(重试必备)
⚠️ 重要:重试机制必须配合幂等性设计,否则可能导致数据重复处理。
幂等性定义:同一操作执行多次与执行一次的效果相同。
常见幂等性实现方式:
| 方式 | 适用场景 | 实现复杂度 |
|---|---|---|
| 唯一业务标识 | 支付、订单创建 | 低 |
| 状态机控制 | 状态流转(待支付→已支付) | 低 |
| 数据库唯一约束 | 防止重复插入 | 低 |
| 乐观锁 | 更新操作 | 低 |
| Token 机制 | API 接口调用 | 中 |
| 分布式锁 | 高并发场景 | 中 |
幂等性代码示例:
// 1. 唯一业务标识实现幂等
@Service
public class PaymentService {
@Transactional
public void pay(PaymentRequest request) {
// 检查是否已处理(幂等判断)
Optional<Payment> existing = paymentRepository.findByRequestId(request.getRequestId());
if (existing.isPresent()) {
log.info("支付请求已处理,跳过: {}", request.getRequestId());
return; // 直接返回,不重复处理
}
// 创建支付记录
Payment payment = new Payment();
payment.setRequestId(request.getRequestId());
payment.setAmount(request.getAmount());
payment.setStatus("PAID");
paymentRepository.save(payment);
}
}
// 2. Token 机制实现 API 幂等
@RestController
public class ApiController {
@PostMapping("/api/order")
public Result createOrder(@RequestBody OrderRequest request,
@RequestHeader("Idempotent-Token") String token) {
// 验证并消费 Token
if (!redisTemplate.delete("idempotent:" + token)) {
return Result.error("重复请求或 Token 无效");
}
// 处理业务逻辑
return orderService.createOrder(request);
}
}
// Token 生成接口(客户端先获取 Token)
@GetMapping("/api/token")
public String generateToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idempotent:" + token, "1", 10, TimeUnit.MINUTES);
return token;
}
// 3. 状态机实现幂等
@Service
public class OrderService {
@Transactional
public void payOrder(String orderId) {
Order order = orderRepository.findById(orderId);
// 状态判断(幂等判断)
if (!"WAIT_PAY".equals(order.getStatus())) {
log.info("订单状态非待支付,跳过: {}, 当前状态: {}", orderId, order.getStatus());
return;
}
// 状态流转:待支付 → 已支付
order.setStatus("PAID");
order.setPayTime(LocalDateTime.now());
orderRepository.save(order);
}
}
幂等性与重试机制配合要点:
- 支付类操作:使用唯一请求 ID,重试时携带相同 ID
- 订单创建:客户端生成唯一订单号,服务端校验唯一性
- 状态更新:前置状态判断,确保状态流转方向正确
- 消息消费:消息携带唯一 ID,消费前检查是否已处理
- API 接口:Token 机制,先获取 Token 后携带 Token 调用
结语
重试机制是分布式系统容错设计的基础能力。选择合适的重试方案,需要在实时性、资源占用、持久化需求、复杂度之间权衡。对于大多数业务场景,Spring Retry 提供的同步重试已足够;对于高并发或跨系统场景,消息队列延迟重试是更优选择;对于核心业务,定时任务扫描重试提供最终一致性保障。
合理使用重试机制,可以显著提升系统稳定性,但也要避免滥用——重试不是解决所有问题的万能钥匙,错误的边界界定可能导致问题掩盖或雪崩效应。

浙公网安备 33010602011771号