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