分布式锁与RedLock

发布于 作者: Ethan

引言

本文为 How to do distributed locking 的简要笔记,作者为Martin Kleppmann(DDIA作者)。

一、分布式锁的使用目的

  • 效率(Efficiency)
    • 避免重复工作(如耗时计算、重复发邮件)。
    • 锁失效的后果:只是增加一些成本或带来轻微不便。
  • 正确性(Correctness)
    • 确保并发进程不会破坏共享状态。
    • 锁失效的后果:可能导致数据损坏、丢失、不一致,甚至严重错误(如医疗场景)。

如果仅为效率,可以使用单个 Redis 实例即可;如果为正确性,Redlock 并不合适。


二、分布式锁的常见问题

锁与进程暂停

  • 客户端持有锁后可能因 GC 暂停进程调度/内存缺页 等原因被长时间挂起。
  • 锁是 租约(lease),有过期时间。
  • 当租约过期后,另一个客户端可能获取锁,而原客户端恢复后依然执行写操作 → 数据损坏

网络延迟

  • 网络包可能被延迟很久(如 GitHub 曾出现 90 秒延迟事件)。
  • 延迟到达的写请求可能在租约过期后才到达,导致冲突。

distlock-1


三、解决方案:Fencing Token(栅栏令牌)

原理

  • 每次获取锁时生成 递增的令牌号
  • 存储服务在处理写请求时必须携带并检查令牌号:
    • 如果令牌号 小于已处理的最大值,则拒绝写入。
  • 确保即使旧客户端恢复,也不会覆盖新客户端的结果。

distlock-2

关键点

  • 需要锁服务保证 单调递增 的令牌。
  • ZooKeeper 提供的 zxidznode 版本号 可直接作为 fencing token。

Redlock 的问题

  • Redlock 仅依赖随机数,无法保证单调递增 → 无法提供 fencing token
  • 如果要实现,几乎需要一个共识算法来生成令牌。

四、Redlock 的缺陷

时间依赖性

  • Redis 键过期基于 gettimeofday,可能跳跃(NTP 校时、人工修改时钟)。
  • 算法假设:
    • 网络延迟有限且小于 TTL。
    • 进程暂停时间有限且小于 TTL。
    • 时钟误差有限。
  • 这些假设 在实际系统中不可靠

示例场景

  • 时钟跳跃
    • 节点 C 时钟前跳 → 提前过期 → 客户端 2 获得锁 → 客户端 1 和 2 同时认为自己持锁。
  • 进程暂停
    • 客户端 1 在请求中途被 GC 挂起 → 锁过期 → 客户端 2 获得锁 → 客户端 1 恢复后仍认为自己持锁。

模型问题

  • Redlock 实际依赖 同步系统模型(bounded delay/clock/pause)。
  • 现实中只能假设 部分同步异步 + 故障检测
  • 在异步模型下,Redlock 无法保证安全性。

五、对比与替代方案

Redlock

  • 对效率型场景过重(需要 5 个 Redis 实例,多数投票)。
  • 对正确性场景又不安全(缺少 fencing token,依赖不合理的时间假设)。

建议方案

  1. 仅效率需求
    • 使用单实例 Redis(SETNX 获取锁,DEL 检查值释放锁)。
    • 明确标注为“近似锁”,可能偶尔失效。
  2. 需要正确性
    • 使用 ZooKeeper / etcd / Consul 等 共识系统
    • 使用 Curator 提供的锁实现。
    • 强制在资源访问时使用 fencing token。

六、结论

  • Redlock “不上不下”:
    • 效率场景:太复杂、不值。
    • 正确性场景:不安全、不可用。
  • 本质缺陷:
    • 缺少 fencing token。
    • 假设过于依赖同步系统模型。
  • 正确做法:
    • 效率优化 → 单节点 Redis。
    • 正确性保障 → ZooKeeper/共识系统 + fencing token。