4. 错误恢复与重试:重试策略、幂等性设计与断路器模式

在分布式系统中,瞬时故障(网络抖动、服务重启、资源争用)是常态而非异常。错误恢复与重试机制是构建高可用系统的核心防线。本章聚焦三种经典模式:重试策略(指数退避与固定间隔)、幂等性设计(确保重复请求不产生副作用)以及断路器模式(防止级联故障)。三者协同工作,形成从“尝试恢复”到“安全重放”再到“主动熔断”的完整闭环。

4.1 重试策略:指数退避与固定间隔

重试策略决定了在请求失败后,客户端等待多久再次发起请求。选择不当的重试策略可能导致“重试风暴”,使下游服务雪上加霜。

4.1.1 固定间隔重试

每次重试前等待相同的时间间隔。实现简单,但缺乏弹性,不适合高并发或长耗时故障场景。

参数说明典型值
间隔时间每次重试等待的固定时长500ms / 1s / 2s
最大重试次数超过此次数后放弃3 次
适用场景低负载、已知短时抖动本地缓存刷新

4.1.2 指数退避重试

每次重试的等待时间呈指数增长,并通常引入随机抖动(Jitter)防止惊群效应。这是生产环境中最推荐的重试策略。

// 指数退避 + 随机抖动(Java 伪代码)
public long computeBackoff(int attempt, long baseDelayMs, long maxDelayMs) {
    long exponentialDelay = baseDelayMs * (long) Math.pow(2, attempt);
    long jitter = ThreadLocalRandom.current().nextLong(0, exponentialDelay / 2);
    long finalDelay = Math.min(exponentialDelay + jitter, maxDelayMs);
    return finalDelay;
}
关键参数对比:
  • 基础延迟(baseDelay):首次重试等待时间,通常 100ms~500ms。
  • 乘数因子:指数增长基数,通常为 2。
  • 最大延迟(maxDelay):防止无限等待,通常 10s~30s。
  • 随机抖动(Jitter):在计算出的延迟上增加随机偏移,避免所有客户端同时重试。
最佳实践: 对于读操作,可适当增加重试次数(如 5 次);对于写操作,必须结合幂等性设计,且重试次数建议不超过 3 次。
重试策略对比:固定间隔 vs 指数退避 固定间隔重试 请求失败 等待 1s 等待 1s 放弃(3次) 指数退避 + 抖动 请求失败 等待 ~200ms 等待 ~400ms 放弃(3次) 指数退避通过递增等待时间 + 随机抖动,有效避免重试风暴,保护下游服务

4.2 幂等性设计:安全重放的核心

幂等性(Idempotency)是指无论请求被执行一次还是多次,系统状态始终保持一致。在重试机制中,幂等性是防止重复扣款、重复下单、重复创建资源等严重数据错误的唯一保障。

4.2.1 幂等性实现策略

策略实现方式适用场景示例
唯一请求 ID客户端生成全局唯一 ID,服务端去重写操作(下单、支付)UUID / 雪花算法 ID
乐观锁(版本号)更新时校验版本号,版本不符则拒绝数据更新UPDATE ... WHERE version = ?
状态机校验只允许特定状态转换,重复请求被忽略订单状态流转已支付 → 已发货,不可重复支付
去重表利用数据库唯一索引记录请求指纹消息队列消费INSERT ... ON DUPLICATE KEY

4.2.2 唯一请求 ID 模式详解

这是最通用的幂等性实现方式。客户端在首次请求时生成一个 Idempotency-Key,服务端在处理成功后缓存该 Key。后续相同 Key 的请求直接返回第一次的结果,不再执行实际逻辑。

// 幂等性中间件伪代码(服务端)
public Response handleRequest(Request request, String idempotencyKey) {
    // 1. 检查缓存中是否已存在该 Key 的处理结果
    Response cached = cache.get(idempotencyKey);
    if (cached != null) {
        return cached; // 直接返回,不执行业务逻辑
    }

    // 2. 执行实际业务逻辑(加锁防止并发)
    lock.lock(idempotencyKey);
    try {
        cached = cache.get(idempotencyKey);
        if (cached != null) {
            return cached;
        }
        Response result = doBusinessLogic(request);
        cache.set(idempotencyKey, result, ttl);
        return result;
    } finally {
        lock.unlock(idempotencyKey);
    }
}
注意: 幂等性 Key 的过期时间(TTL)需要合理设置。太短可能导致重复请求穿透,太长会占用存储空间。通常建议设置为 24 小时或与业务补偿周期一致。

4.3 断路器模式:防止级联故障

当下游服务持续故障时,无休止的重试只会浪费资源并加剧系统负载。断路器模式通过监控失败率,在达到阈值时“断开”电路,快速失败后续请求,并定期探测服务是否恢复。

4.3.1 断路器状态机

断路器包含三个核心状态:关闭(Closed)、打开(Open)、半开(Half-Open)。

  • 关闭(Closed):正常状态,请求直接通过。当失败次数超过阈值(如 5 秒内失败 10 次)时,切换到打开状态。
  • 打开(Open):所有请求立即失败(快速失败),不发起实际调用。经过超时时间(如 30 秒)后,切换到半开状态。
  • 半开(Half-Open):允许少量请求通过,探测服务是否恢复。如果请求成功,则切换回关闭状态;如果失败,则回到打开状态并重置超时计时器。
断路器状态机 关闭 (Closed) 请求正常通过 打开 (Open) 快速失败 半开 (Half-Open) 探测恢复 失败率超阈值 超时后自动 探测成功 探测失败

4.3.2 断路器配置参数

参数说明推荐值
失败阈值触发断开的失败次数或失败率10 次 / 50% 失败率
统计窗口统计失败率的滑动时间窗口10 秒
打开超时断路器保持打开状态的时间30 秒
半开请求数半开状态下允许通过的探测请求数3 次
三者协同工作流程:
  1. 断路器关闭:请求正常发送,若失败则触发重试策略(指数退避)。
  2. 重试失败:业务逻辑必须保证幂等性,防止重复执行产生脏数据。
  3. 失败率飙升:断路器打开,后续请求快速失败,不再发起重试。
  4. 半开探测:断路器半开后,允许少量幂等请求探测服务状态,成功则关闭断路器,恢复完整重试能力。
生产建议: 在微服务架构中,推荐使用 Resilience4j(Java)、Polly(.NET)或 Sentinel(Go/Java)等成熟库,它们内置了指数退避、幂等性过滤和断路器模式,避免重复造轮子。