本文目录导读:

在Java中构建一套规范的任务重试流程,核心目标是提高系统健壮性(处理瞬态故障)和避免资源浪费(防止无休止重试或雪崩)。
规范的实现通常需要考虑三个层面:策略设计、代码实现和生产监控。
以下是基于行业最佳实践的完整规范指南:
核心规范原则
- 仅重试可恢复的异常:对网络超时、数据库连接池满、服务限流等瞬态异常进行重试,对空指针、参数校验失败、业务逻辑错误等确定性异常立即失败。
- 指数退避 + 随机抖动:防止重试风暴(Retry Storm)拖垮下游。
- 有限重试次数:设置最大次数上限(3-5 次)。
- 幂等性:重试操作必须是幂等的(下游能识别重复请求)。
- 兜底机制:超过重试次数后的降级方案(如保存到死信队列、发告警)。
规范实现方案(三种主流方式)
方案 1:手动编码(基础但灵活)
适合简单场景或无法引入三方库的旧系统。
public class ManualRetryTemplate {
// 配置
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
public <T> T executeWithRetry(Callable<T> task) {
Exception lastException = null;
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
return task.call();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("任务被中断", e);
} catch (Exception e) {
lastException = e;
// 判断是否为可重试异常
if (!isRetryableException(e)) {
throw new NonRetryableException("非可重试异常", e);
}
if (attempt == MAX_RETRIES) {
break; // 最后一次失败,跳出循环
}
// 指数退避 + 随机抖动
long delay = computeDelay(attempt);
try {
TimeUnit.MILLISECONDS.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试等待被中断", ie);
}
log.warn("任务执行失败,第{}次重试,等待{}ms", attempt, delay, e);
}
}
throw new RetryExhaustedException("重试耗尽", lastException);
}
private long computeDelay(int attempt) {
// 指数退避: 1s, 2s, 4s
long baseDelay = BASE_DELAY_MS * (long) Math.pow(2, attempt - 1);
// 随机抖动: 增加 0-50% 的随机延迟(防止同时重试)
long jitter = (long) (baseDelay * 0.5 * Math.random());
return baseDelay + jitter;
}
private boolean isRetryableException(Throwable e) {
return e instanceof TimeoutException
|| e instanceof SocketException
|| e instanceof RetryableException; // 自定义可重试异常标记
}
}
方案 2:Spring Retry(项目推荐)
适合 Spring 生态,通过注解或 Template 实现,代码侵入性低。
依赖引入:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- 如果使用AOP,还需要 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
注解方式(最常用):
@Service
public class OrderService {
@Retryable(
value = {TimeoutException.class, SQLException.class}, // 仅重试指定异常
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000) // 指数退避
)
public Order createOrder(OrderDTO dto) {
// 核心业务逻辑,比如调用远程API
return remoteClient.create(dto);
}
@Recover // 重试耗尽后的兜底方法
public Order recover(TimeoutException e, OrderDTO dto) {
// 降级:记录死信、发告警、返回兜底数据
log.error("订单创建重试耗尽,放入死信队列", e);
deadLetterQueue.send(dto);
return Order.fallback();
}
}
编程式 RetryTemplate(灵活控制):
@Service
public class RetryService {
private final RetryTemplate retryTemplate;
public RetryService() {
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
// 设置可重试异常
Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<>();
policyMap.put(TimeoutException.class, true);
policyMap.put(RetryableException.class, true);
retryPolicy.setRetryableExceptions(policyMap);
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(1000);
backOff.setMultiplier(2);
backOff.setMaxInterval(10000);
this.retryTemplate = new RetryTemplate();
this.retryTemplate.setRetryPolicy(retryPolicy);
this.retryTemplate.setBackOffPolicy(backOff);
}
public Result callRemote() {
return retryTemplate.execute(context -> {
// 重试逻辑
return remoteClient.call();
}, context -> {
// 恢复回调
log.error("远程调用重试耗尽");
return Result.fallback();
});
}
}
方案 3:Guava Retryer(轻量级)
适合非 Spring 环境或需要更细粒度控制。
Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
.retryIfExceptionOfType(IOException.class) // 只重试IO异常
.retryIfResult(result -> result == null) // 也可以根据结果重试
.withWaitStrategy(WaitStrategies.exponentialWait(1000, 10, TimeUnit.SECONDS)) // 指数退避
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 最大尝试3次
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
System.out.println("第 " + attempt.getAttemptNumber() + " 次重试");
}
})
.build();
try {
String result = retryer.call(() -> remoteCall());
} catch (ExecutionException e) {
log.error("重试全部失败", e);
// 进行降级处理
}
生产级监控与告警
重试流程必须可观测,否则重试会掩盖真正的故障。
@Retryable(/* ... */)
public Order createOrder(OrderDTO dto) {
// 埋点:记录重试次数
// 方式1:Metrics(如 Micrometer)
// 方式2:日志 + 告警
}
@Recover
public Order recover(TimeoutException e, OrderDTO dto) {
// 关键:在此处发送监控告警,因为重试已经耗尽,问题依然存在
monitor.alert("订单创建接口重试耗尽,请检查下游系统");
deadLetterQueue.send(dto);
return Order.fallback();
}
需要关注的监控指标:
- 重试次数分布:首次成功 vs 重试 N 次后成功(后者是潜在的性能问题)。
- 重试耗尽次数:业务故障的硬指标。
- 重试耗时:请求总耗时 = 正常耗时 + 重试退避时间。
常见陷阱与规范避坑
- 不要重试写操作的幂等性:如果重试的写操作不是幂等的,会导致重复数据(如重复下单、重复扣款)。必须确保下游接口支持幂等(通常通过幂等键实现)。
- 避免重试链路过长:A 调 B,B 调 C,每个环节都重试 3 次且指数退避,极端情况下请求响应时间会非常长。推荐只在外层 API 网关或最上层调用进行重试。
- 不要重试 InterruptedException:线程中断标志必须被尊重,应立即恢复中断状态并退出。
- 重试与事务分离:如果重试方法带
@Transactional,内部的重试可能会因为事务回滚而无法实现,通常将重试放在事务外层。 - 限流与服务保护:重试逻辑应该配合
@CircuitBreaker(断路器)使用,如果下游持续故障,应先熔断,而不是无脑重试(推荐 Resilience4j 与 Spring Retry 组合)。
规范清单
| 维度 | 规范要求 |
|---|---|
| 异常判断 | 明确区分可重试异常(瞬态)与不可重试异常(业务、程序错误) |
| 重试策略 | 有限次数(3-5次)+ 指数退避 + 随机抖动 |
| 幂等性 | 下游消费方必须幂等 |
| 降级回收 | 重试耗尽必须有兜底逻辑(死信队列、缓存fallback、默认值) |
| 可观测性 | 重试次数、耗时、耗尽次数必须打点或日志监控 |
| 韧性组合 | 重试 + 熔断(CircuitBreaker)配合使用,防止雪崩 |
| 框架选择 | 优先使用 Spring Retry 或 Resilience4j,避免手写循环 |
对于 Java 项目,第一步建议引入 Spring Retry,它很好地平衡了编码规范、易用性和功能性。