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 重试触发条件的知识图谱

下面这张图,是我自己总结的重试决策树。每次设计重试策略时,我都会对照着走一遍:

重试触发条件决策树 收到异常 是临时异常还是永久异常? 临时异常 值得重试 永久异常 不该重试 网络超时 / 连接超时 服务限流 / 503 不可用 数据库死锁 / 锁争抢 参数错误 / 400 Bad Request 权限不足 / 资源不存在 业务逻辑错误 / 数据冲突 核心原则:只对临时异常重试,永久异常直接抛出

3.6 一个真实的案例

最后分享一个我亲身经历的故事。

之前做一个订单系统,下游的库存服务偶尔会超时。我一开始把所有超时都重试了 3 次。结果有一次,库存服务因为磁盘满了,所有请求都超时。3 次重试下来,原本 100 个请求变成了 400 个,直接把库存服务打死了。

后来我改了策略:先判断异常类型,再决定是否重试。对于磁盘满这种永久性故障,直接返回降级结果,不再重试。对于网络抖动这种临时故障,才进行重试。

改完之后,系统的稳定性提升了一大截。你想想看,重试不是越多越好,而是要"该重试的重试,不该重试的坚决不试"。

总结一下:

  • 临时异常(网络超时、限流、503)→ 值得重试
  • 永久异常(参数错误、权限不足、业务错误)→ 不该重试
  • 不确定的异常 → 默认不重试,宁可降级也不要乱试