java中无“可恢复异常”语言概念,需通过@retryable显式配置重试策略;它不依赖异常类型,而由include指定、@recover兜底,且必须作用于spring bean方法,并注意幂等性与异常设计合理性。

Java中没有“可恢复异常”这个语言级概念,但你可以通过设计让某些异常具备重试语义——关键不在异常本身,而在你如何定义它、捕获它、并决定是否重试。
怎么用 @Retryable 实现自动重试
Spring Retry 是最常用、也最稳妥的方案。它不依赖异常类型是否“可恢复”,而是靠你显式标注哪些方法允许重试、重试几次、间隔多久。
- 必须启用 retry 支持:
@EnableRetry加在配置类上 -
@Retryable只能加在 Spring 管理的 Bean 方法上(不能是 private 或普通 new 出来的对象) - 默认只对
RuntimeException重试;若想对受检异常(如IOException)也重试,得显式写include = { IOException.class } - 别忘了配
@Recover方法处理最终失败——否则重试完直接抛异常,调用方可能没准备兜底逻辑
@Service
public class PaymentService {
@Retryable(
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2),
include = { SocketTimeoutException.class, IOException.class }
)
public void charge(String orderId) throws IOException {
// 调第三方支付接口,可能因网络抖动失败
http.post("https://api.pay/charge", orderId);
}
@Recover
public void recover(IOException e, String orderId) {
log.warn("支付重试失败,转入人工审核: {}", orderId);
sendToManualReview(orderId);
}
}
为什么不用 while 循环手写重试
手写循环看似简单,但容易漏掉关键细节,尤其在高并发或资源敏感场景下隐患明显。
- 没做线程中断响应:
Thread.sleep()可能被中断,但你不恢复中断状态(Thread.currentThread().interrupt()),上游就收不到信号 - 没控制重试粒度:整个方法体重试,可能把不该重试的副作用(如已发短信、已扣库存)重复执行
- 没区分异常类型:一次
NullPointerException和三次ConnectException混在一起重试,逻辑混乱 - 日志难追溯:5次重试打5条日志,但没唯一 trace ID 关联,排查时分不清是同一请求还是不同请求
自定义异常要不要继承 Exception 还是 RuntimeException
这直接决定调用方是否“被迫处理”——而强制处理未必带来更好恢复能力。
立即学习“Java免费学习笔记(深入)”;
- 如果异常代表**临时性外部故障**(如网络超时、限流拒绝),用
RuntimeException子类更合理。调用方本就不该在业务层 try-catch,而应由框架统一重试 - 如果异常代表**必须由调用方决策的业务约束**(如余额不足、订单已取消),才考虑受检异常,但要慎用——现代微服务中,这类判断通常由下游返回明确错误码,而非抛异常
- 别为了“看起来可恢复”就生造一个
RetryableException:Spring Retry 不看异常名,只看你有没有配@Retryable;自己写的重试逻辑也不靠 instanceof 判断,而是靠策略配置
重试不是万能的,最容易被忽略的三个点
重试只对幂等操作安全。一旦涉及状态变更,必须先确认:这次重试,会不会让数据库多扣一笔钱?消息会不会被重复消费?用户会不会收到两份通知?
- HTTP 调用前,检查是否带了幂等 key(如
X-Idempotency-Key) - 本地事务里重试,要避免在
try块里提前 commit 部分数据 - 异步任务重试(比如 MQ 消费失败),得确保消费者本身支持幂等,而不是靠重试掩盖设计缺陷
真正难的从来不是“怎么加重试”,而是“哪里不该加”和“加了之后怎么保证不翻车”。









