分布式锁抢锁失败后如何退避重试?
简化版
抢锁失败后应在请求总截止时间内做有界指数退避并加入随机抖动,避免所有竞争者按固定周期同时重试。能由锁服务定向通知时优先等待通知;超过预算就快速失败、排队或降级,不能无限占用业务线程。
详细版
区分“锁被占用”和“锁服务错误”:前者按竞争策略等待,后者受重试预算与熔断控制。Full Jitter 可从 0..min(cap, base×2^attempt) 随机取等待时间,每次重试前检查 deadline 和取消信号;释放通知也要配超时轮询,防止通知丢失。
参数从持锁 P99、上游延迟预算和最大并发反推。监控每次请求尝试数、总等待、最终结果、重试 QPS 与 top key;热点长期输入大于单 key 服务能力时,应排队、合并或重构资源模型,而不是继续加快重试。
完整版教学
一、立即重试为什么危险
一百个客户端同时失败后立即循环,会持续轰击锁服务;锁释放瞬间又同时抢占,只有一个成功,其余继续制造流量。业务线程、连接池和锁后端可能先被重试耗尽,这就是典型 thundering herd。
心法:重试不是消除失败,而是用时间换取下一次成功机会;没有预算的重试就是放大器。
二、指数退避与抖动
第 n 次上限可取 min(cap, base×2^n),Full Jitter 再在 0 到上限间随机。随机化打散客户端节奏,cap 防止等待无限增长。例如 base=50ms、cap=2s,第 0~3 次上限为 50、100、200、400ms。
delay_n = random(0, min(2000ms, 50ms * 2^n))
if now + delay_n >= deadline: stop
三、轮询与通知怎么选
轮询实现简单但有空请求;发布/订阅或前驱监听能定向唤醒,却要处理通知先于订阅、连接中断和消息丢失。稳妥实现通常“通知 + 最长等待超时后复查”,通知只是加速器,锁状态仍是事实来源。
| 策略 | 负载 | 唤醒速度 | 风险 |
|---|---|---|---|
| 忙轮询 | 高 | 快 | 风暴 |
| 固定间隔 | 中 | 中 | 同步碰撞 |
| 抖动退避 | 低 | 可控 | 参数选择 |
| 定向通知 | 低 | 快 | 丢通知/连接状态 |
四、总截止时间比次数更重要
上游只剩 300ms 时,即使“最多重试 5 次”也没有意义。每次获取、等待与业务执行共享同一个 deadline,并预留临界区执行时间。取消或客户端断开后立即停止排队,避免幽灵请求随后获锁。
例如请求预算 1 秒、业务 P99 400ms,则锁等待最多约 600ms,还要扣除网络余量;不能把 1 秒全部花在抢锁上。
五、不同失败原因不同处理
明确返回 BUSY 才进行竞争退避;认证失败、非法 key 不重试;锁服务超时属于结果未知,应查询或等待,不能假定未获得后直接再建一把锁。后端大面积故障时使用熔断和全局重试预算,避免每个请求独立重试三次。
业务允许时可返回“处理中”或投递队列,让消费者按 key 串行,避免 HTTP 线程同步等待。
六、容量与观测
临界区平均 25ms 时单 key 理论能力约 40 QPS;输入 100 QPS 无论怎样退避都无法长期稳定。退避只能平滑瞬时竞争,不能创造串行资源容量。此时要削峰、合并操作、拆分冲突域或拒绝过量请求。
监控 attempt 分布、总等待 P99、放弃率、重试/原始流量比、等待者数量与 top key,并在 Trace 中显示每轮原因和延迟。
重试预算应在调用链上共享:上游已经重试一次时,下游不能再独立放大为三次,否则三层各重试三次可能形成最多 27 次尝试。
服务端还可返回建议等待时间,但客户端仍需加入抖动并受自身 deadline 限制,避免所有请求完全遵循同一个时间点再次唤醒。
七、常见误区与追问
- 误区:重试越快成功越快。 高频重试会扩大争用并拖慢持有者。
- 误区:固定间隔已经能降压。 同时开始的客户端仍会同步碰撞。
- 误区:限制重试次数就够了。 还要受统一 deadline 和全局预算约束。
- 误区:收到通知就一定能获得锁。 通知只是提示,仍需原子竞争。
- 追问:为什么推荐 Full Jitter? 它比确定性间隔更能打散群体重试。
- 追问:通知丢失怎么办? 设置等待超时后重新检查权威锁状态。
- 追问:热点持续过载怎么办? 排队限流或改变资源模型,退避不能修复容量缺口。
八、加强记忆
退避重试按“分原因、有抖动、受 deadline、可取消、看容量”设计。锁忙用指数抖动或定向通知,参数错误不重试,未知结果先确认;当输入长期超过单 key 串行能力时,必须排队或拒绝,而不是靠更多重试掩盖过载。