分布式锁如何续期?看门狗机制有什么风险?
简化版
看门狗在持锁任务仍存活时,于租约到期前周期性校验 owner 并延长 TTL,避免正常长任务被误判失锁。它不能消除 GC 暂停、网络分区和旧持有者恢复的问题;续期失败后任务必须停止副作用,下游最好再用 Fencing Token 拒绝迟到写。
详细版
获取锁时生成唯一 owner token,并登记当前进程的续期任务。续期脚本只在 key 的 value 仍属于自己时延长过期时间;业务完成后取消续期并以同样的 owner 校验释放。续期间隔常取租期的约三分之一,并结合调度抖动和失败重试预算校准。
风险包括长时间 STW 超过租期、网络分区下客户端继续工作、调度线程阻塞、业务永不结束导致无限占锁,以及续期与释放竞态。必须监控租约剩余量、续期延迟/失败、长持锁和任务截止时间;对不可安全中断的写入,仅靠看门狗仍不够。
完整版教学
一、为什么需要续期
固定 TTL 太短,正常任务尚未结束锁就被别人获得;TTL 太长,进程宕机后资源长期不可用。看门狗把“故障回收上限”与“正常任务实际耗时”部分解耦:只要 owner 活跃且能通信就续期,失联后由 TTL 回收。
易错点:续期提高的是长任务可用性,不会把租约变成绝对所有权。
二、标准生命周期
acquire(owner=A, ttl=30s)
-> every 10s: compare owner=A and extend to 30s
-> business completes
-> cancel watchdog
-> compare owner=A and delete
比较与续期必须原子执行,否则读取到 A 后可能被删除重建为 B,再被 A 的续期错误延长。Redis 常用 Lua,具备事务 API 的存储用条件更新。
三、参数怎样计算
租期至少覆盖调度抖动、一次续期 RTT 和允许的重试窗口。若 TTL=30 秒、间隔=10 秒,正常有约 20 秒余量;一次 25 秒 STW 仍可能让锁过期。间隔过密增加锁服务负载,过疏则容错余量小。
| 参数 | 过小风险 | 过大风险 |
|---|---|---|
| TTL | 正常任务失锁 | 故障回收慢 |
| 续期间隔 | 服务压力 | 来不及重试 |
| 最大持锁时长 | 误杀长任务 | 永久占用 |
| 重试次数 | 偶发抖动失锁 | 不确定期延长 |
四、GC 与网络分区的安全边界
客户端暂停 40 秒,30 秒租约已过期,新客户端 B 获锁;A 恢复时本地线程仍以为业务未完成。此时即使续期请求失败,A 已发出的数据库请求也可能在路上。任务要在关键步骤检查租约状态,下游用更大 fencing token 才能可靠拒绝 A 的迟到写。
A token=7 --pause--> lease expires
B token=8 ----------> write accepted
A resumes token=7 --> write rejected
五、续期与释放的竞态
业务线程结束并释放的同时,调度线程可能正准备续期。正确顺序是标记本地完成、取消后续任务,再通过 owner 条件删除;即使在途续期先到,也只能延长自己的锁,随后条件删除仍安全。不能执行无条件 DEL 或 EXPIRE。
若业务抛异常,finally 只负责尝试释放;释放超时结果未知时,不能盲目删除,应等待 TTL 或查询 owner。
六、防止无限续期与观测
任务逻辑卡死但进程健康时,看门狗会一直续期。应设置业务 deadline 和最大持锁时长,超过后停止续期、标记任务异常并触发人工或补偿。长作业更适合拆分为带 checkpoint 的小步骤,而非持锁数小时。
监控续期成功率、耗时、剩余 TTL、连续失败数、持锁 P99、超过 deadline 数和锁服务负载,并让续期线程池与业务线程池隔离。
七、常见误区与追问
- 误区:有看门狗就不会锁过期。 长暂停或网络分区仍会超过租约。
- 误区:续期时直接刷新 TTL 即可。 必须原子校验 owner。
- 误区:任务还活着就应无限续期。 逻辑卡死会永久占锁。
- 误区:续期失败后继续写没关系。 此时可能已有新持有者。
- 追问:为什么常取 TTL 的三分之一续期? 为抖动和多次重试保留余量,但仍需实测。
- 追问:释放和续期并发怎么办? 两者都做 owner 条件操作,并先取消本地调度。
- 追问:怎样保护不可中断写? 使用资源侧 fencing 或原子条件版本,而非只依赖续期。
八、加强记忆
看门狗是“带 owner 校验的周期续租”:它平衡短 TTL 与长任务,却不能对抗超出租期的暂停。设计要同时具备原子续期、安全释放、最大执行时间和失锁停止机制;真正涉及数据安全时,再由下游 fencing 把旧持有者挡住。