RedLock 是什么药方
前三篇攒出的 Redisson 锁有个前提没挑明:锁数据只存在一台 Redis 上。单点不可用时锁服务全停(可用性问题),主从复制时锁记录可能来不及同步到从库就随主库一起消失(安全性问题,第 7 篇展开)。Redis 之父 antirez 给出的处方是 RedLock(红锁):不搞主从、不用集群,摆 N 个(推荐 5 个)完全独立、互不通信的 Redis 主节点,客户端依次向每个节点加锁——
1. 记录开始时间 T0,依次向 5 个节点 SET NX PX(同一个随机 value)
2. 收集成功数,并记录结束时间 T1
3. 判定拿到锁 = 成功节点数 >= 3(多数派)
且 T1 - T0 < 锁有效期(别把剩余时间全耗在加锁路上)
4. 真实有效期 = 原始 TTL - 加锁耗时
5. 任何一步不满足 → 向全部 5 个节点发解锁请求(哪怕没加成功)思路是多数派仲裁:锁的状态不再取决于一台机器的生死,而取决于过半节点的共识——挂 1 台、2 台不影响判定,也不存在「锁记录没同步到从库」的问题,因为根本没有从库。2016 年之前,这被普遍视为 Redis 分布式锁的「正确姿势」,直到有人写了篇博客。
Martin 的两板斧
Martin Kleppmann——《DDIA》的作者,分布式系统的布道者——发文《How to do distributed locking》,对 RedLock 的安全性提出了两个否定。第一板斧是GC 停顿:客户端 1 拿到 RedLock,随即发生长停顿(GC、虚拟机挂起、缺页),停顿期间锁过期,RedLock 的多数派到期自动放锁,客户端 2 顺利拿到锁开始写;客户端 1 复活后完全不知道锁已易主,继续往下游写——两个持有者同时进临界区,RedLock 的「安全」在这个场景下不成立。注意,这不是 RedLock 的实现 bug,而是「锁的过期靠时间、进程不知道自己被冻结过」这一类时间型锁的共同软肋。
第二板斧是时钟跳变:RedLock 判定「锁未过期」依赖各节点的本地时钟。某节点管理员手动改时间、NTP 校准大幅回拨,锁的 TTL 被瞬间清零——多数派里多一台时钟异常,判定就失真。Martin 的结论很刻薄也很清晰:RedLock 建立在「时间可信」的假设上,而分布式系统恰恰不能假设时间可信。
antirez 的反击与论战的真正遗产
antirez 逐条回击:GC 停顿场景,RedLock 客户端持有随机 token,下游可以拒绝「token 已过期」的写入(Martin 自己提的 fencing token,antirez 认为可用);时钟跳变,RedLock 可以用「相对时间计数」缓解(TTL 用单调时钟推进),实际工程里 NTP 配置成步进而非跳变。Martin 则回敬:fencing token 要下游存储配合校验,配得上这套配置的存储(多数数据库)自己就有并发控制,RedLock 反而多余了。论战没有判出胜负,却给所有用锁的人沉淀了两条真理。
真理一:先分清锁的用途——效率还是正确性。效率锁:两个人同时干重复的活浪费资源,锁只是避免浪费(定时任务防重复跑、缓存防击穿重建),偶尔双跑一次无伤大雅;正确性锁:双跑就是事故(扣款、扣库存),绝不容忍同时进入临界区。真理二:正确性场景锁本身靠不住,必须给下游发「单调递增的令牌」(fencing token)让写入可仲裁——这块留给第 16 篇细讲。
| 方案 | 安全性 | 复杂度 | 适用 |
|---|---|---|---|
| 单实例 Redisson | 主从切换可能丢锁(小概率) | 低,一个 Redis 就够 | 效率锁,配幂等兜底可覆盖大多数业务 |
| RedLock | 优于单实例,GC/时钟场景仍有缺口 | 高,5 个独立主节点运维成本大 | 容忍极小概率风险又要比单实例稳的场景 |
| fencing token 方案 | 可做到严格安全 | 需要下游存储配合校验 | 正确性锁,钱和库存 |
| ZooKeeper / etcd | 一致性协议背书,会话机制天然防死锁 | 需独立维护协调服务 | 正确性锁的现成答案(第 8 篇起) |
老王听完论战的转述,总结得比我狠:「就是说 RedLock 花五台机器的钱,买到『比单实例强点,但还是不保证不出事』?」——大抵如此。多数团队的最优解其实是:单实例 Redisson 当效率锁用,配上幂等(分布式事务系列第 10 篇)兜底;真正碰不得的业务,交给后面几篇的 ZooKeeper 和 etcd。下一篇插播一个性能话题:一百个线程同时抢一把锁会发生什么——等待与唤醒的艺术。
评论 (0)