3、重试策略基础:重试的触发条件——哪些异常值得重试?哪些不该重试?
大家好,我是老赵。今天我们来聊一个很实在的问题:到底哪些异常值得重试?
说实话,我刚做分布式系统那会儿,踩过一个大坑。当时线上有个任务老是报错,我一拍脑袋,把所有异常都加了重试。结果呢?本来只是偶尔超时,加了重试之后,系统直接雪崩了。数据库连接池被打满,CPU 飙升,最后整个集群都挂了。
嗯,从那以后我明白了一个道理:重试不是万能药,乱用重试反而是毒药。
3.1 重试的核心原则:幂等性与可恢复性
先问大家一个问题:什么样的异常才值得重试?
我个人习惯用一个简单的判断标准——这个异常是临时的,还是永久的?
- 临时异常(Transient Fault):网络抖动、连接超时、服务暂时不可用。这类问题过一会儿可能就自己好了,值得重试。
- 永久异常(Permanent Fault):参数错误、权限不足、资源不存在。你重试一万次也没用,只会浪费资源。
核心原则:只对临时异常进行重试,永久异常直接抛出或降级。
你想想看,如果接口返回 400 Bad Request,说明客户端传参有问题。这时候重试有意义吗?没有。但如果返回 503 Service Unavailable,说明服务端暂时扛不住了,过几秒再试可能就成功了。
3.2 哪些异常值得重试?
我在项目中总结了几类值得重试的异常,给大家列个清单:
| 异常类型 | 典型场景 | 重试建议 |
|---|---|---|
| 网络超时 | SocketTimeoutException、ConnectTimeoutException | 建议重试,通常 1-3 次 |
| 服务端限流 | HTTP 429 Too Many Requests、RateLimitExceeded | 建议重试,但需配合退避策略 |
| 服务暂时不可用 | HTTP 503 Service Unavailable | 建议重试,通常 2-3 次 |
| 数据库死锁 | DeadlockLoserDataAccessException | 建议重试,通常 1-2 次 |
| 分布式锁争抢失败 | 锁获取超时、锁已被占用 | 建议重试,配合指数退避 |
小技巧:我个人习惯在重试前先判断异常类型。比如用 instanceof 检查是否是 TransientException 的子类,或者检查 HTTP 状态码是否在 5xx 范围内。
3.3 哪些异常不该重试?
这部分更重要。我曾经见过一个团队,把所有异常都加了重试,结果把线上环境搞崩了。下面这些异常,千万别重试:
- 参数校验失败:400 Bad Request、IllegalArgumentException。重试一万次也是错。
- 权限不足:401 Unauthorized、403 Forbidden。重试只会触发安全告警。
- 资源不存在:404 Not Found。资源都没了,重试有什么用?
- 数据冲突:409 Conflict。比如版本号不一致,重试前需要先刷新数据。
- 业务逻辑错误:比如余额不足、库存不够。重试解决不了业务问题。
避坑指南:我曾经在一个支付系统中,把"余额不足"的异常也加了重试。结果用户连续扣了三次款,虽然最终都退回了,但用户体验极差。从那以后,我对业务异常一律不重试。
3.4 异常分类的实践方法
在实际项目中,我一般会设计一个异常分类器。说白了,就是给异常打标签:
public enum RetryDecision {
RETRYABLE, // 值得重试
NON_RETRYABLE, // 不该重试
RETRY_WITH_DELAY // 延迟后重试
}
public RetryDecision classify(Throwable ex) {
if (ex instanceof TimeoutException) {
return RetryDecision.RETRYABLE;
}
if (ex instanceof IllegalArgumentException) {
return RetryDecision.NON_RETRYABLE;
}
if (ex instanceof RateLimitExceededException) {
return RetryDecision.RETRY_WITH_DELAY;
}
// 默认:未知异常不重试
return RetryDecision.NON_RETRYABLE;
}
这样做的好处是,重试逻辑和业务逻辑解耦了。你只需要维护一个异常分类表,后续加新异常也很方便。
3.5 重试触发条件的知识图谱
下面这张图,是我自己总结的重试决策树。每次设计重试策略时,我都会对照着走一遍:
3.6 一个真实的案例
最后分享一个我亲身经历的故事。
之前做一个订单系统,下游的库存服务偶尔会超时。我一开始把所有超时都重试了 3 次。结果有一次,库存服务因为磁盘满了,所有请求都超时。3 次重试下来,原本 100 个请求变成了 400 个,直接把库存服务打死了。
后来我改了策略:先判断异常类型,再决定是否重试。对于磁盘满这种永久性故障,直接返回降级结果,不再重试。对于网络抖动这种临时故障,才进行重试。
改完之后,系统的稳定性提升了一大截。你想想看,重试不是越多越好,而是要"该重试的重试,不该重试的坚决不试"。
总结一下:
- 临时异常(网络超时、限流、503)→ 值得重试
- 永久异常(参数错误、权限不足、业务错误)→ 不该重试
- 不确定的异常 → 默认不重试,宁可降级也不要乱试