2026/1/23 6:23:58
网站建设
项目流程
网站建设公制度,做家居商城网站,外贸企业网站评价案例,专业网站设计公司哪家好好#xff0c;现在我们来到了分布式锁讨论的核心部分。如果还没读过前一篇#xff0c;我强烈建议先读一遍#xff0c;了解加锁和解锁的基本流程。后面我不仅会解释 Redlock 的原理#xff0c;还会抛出很多与分布式系统相关的疑问。最好能跟着我的思路#xff0c;在心里一起…好现在我们来到了分布式锁讨论的核心部分。如果还没读过前一篇我强烈建议先读一遍了解加锁和解锁的基本流程。后面我不仅会解释 Redlock 的原理还会抛出很多与分布式系统相关的疑问。最好能跟着我的思路在心里一起分析答案。现在我们来看看 Redis 创始人提出的 Redlock 方案如何解决主从切换后锁失效的问题。Redlock 方案基于两个前提不再部署**从库replica和哨兵sentinel实例只部署主库master**实例但需要部署多个主库实例官方建议至少 5 个换句话说使用 Redlock 需要部署至少 5 个 Redis 实例而且都是主节点。它们之间没有关系各自是独立的实例。注意这不是部署 Redis Cluster而是单纯部署 5 个独立的 Redis 实例。Redlock 具体怎么用整体流程分 5 步客户端先获取当前时间戳 T1客户端依次向这 5 个 Redis 实例发送加锁请求用前面提到的 SET 命令每个请求都设超时毫秒级且远小于锁的有效期。如果某个实例加锁失败网络超时、锁被占用等各种异常立即尝试下一个实例如果客户端在≥3 个多数Redis 实例上成功加锁就再次获取当前时间戳 T2。如果 T2 - T1 锁的过期时间认为加锁成功否则算失败加锁成功后操作共享资源例如修改 MySQL 某行或调用 API加锁失败向所有节点发送释放锁请求用前面提到的 Lua 脚本释放简单总结 4 个要点客户端在多个 Redis 实例上加锁必须保证在大多数节点上加锁成功在大多数节点上加锁的总时间必须小于锁的过期时间释放锁时要向所有节点发送请求可能第一次不太好理解建议把上面文字多读几遍加深理解。然后记住这 5 步很重要。下面的讨论会基于这个流程分析各种可能导致锁失效的假设场景。搞懂 Redlock 流程后我们来看看它为什么这么设计1为什么要多个实例加锁本质是为了容错。如果部分实例异常崩溃只要剩余实例成功加锁整体锁服务依然可用。2为什么多数加锁就算成功多个 Redis 实例一起本质上构成了分布式系统。在分布式系统中“故障节点总会存在。因此讨论分布式系统问题时要考虑系统能容忍多少个故障节点同时不影响整体系统的正确性”。这是分布式系统的容错性问题。这类问题的结论是如果只存在宕机-停止型故障只要多数节点正常整个系统仍能提供正确服务。这个问题的模型就是常听的拜占庭将军问题感兴趣可以看看算法推导。3为什么第 3 步成功加锁后还要计算加锁总耗时因为操作多个节点耗时肯定比单实例长。而且这是网络请求网络情况复杂可能有延迟、丢包、超时等网络请求越多异常概率越高。因此即使成功在多数节点加锁如果加锁总耗时超过了锁的过期时间那部分实例上的锁可能已经过期整体锁就没意义了。4为什么要在所有节点上释放锁在某个 Redis 节点加锁时可能因网络原因失败。比如客户端在某个 Redis 实例上成功加锁但读响应结果时因网络问题出现读取失败。这种情况下锁其实已经在这个 Redis 实例上设置成功了。因此释放锁时无论当初在该节点加锁是否成功都要在所有节点上释放确保清理掉节点上任何残留的锁。好了理解了 Redlock 流程和相关原理后看起来 Redlock 确实解决了 Redis 节点异常故障时锁失效的问题保证了锁的安全性。但真是这样吗Redlock 之争谁对谁错Redis 创始人刚提出这个方案立即遭到分布式系统领域一位著名专家的质疑。这位专家是Martin Kleppmann英国剑桥大学分布式系统研究员之前是从事大规模数据基础设施的软件工程师和创业者。他经常出席大会演讲、写博客和书也是开源贡献者。他迅速发表文章挑战 Redlock 的算法模型提出了自己对分布式锁该如何设计的看法。Redis 创始人 Salvatore SanfilippoAntirez也不示弱撰文反驳批评并阐述了更多 Redlock 设计细节。这场交锋在当时互联网上引发了异常激烈的争论。两位作者逻辑清晰论据扎实堪称领域专家的真刀真枪对决分布式系统领域思想碰撞的盛宴。两人都是公认的分布式系统权威却在同一问题上得出相反结论。怎么可能下面我提取并梳理了他们文章中的核心论点。以下内容为拓展阅读信息量较大可能会难消化建议慢慢看。分布式系统专家 Martin 对 Redlock 的挑战Martin 提出了四条主要论点。1分布式锁用来干什么他说必须先明确目标。用分布式锁只有两个原因效率单纯为了避免重复执行昂贵工作如重量级计算而实现互斥。即使锁偶尔失效只是多发两条警告烦人但不致命正确性需要锁来防止并发进程互相干扰。一旦锁失效多个进程会同时操作同一份数据导致关键数据损坏、永久不一致、数据丢失岂不是相当于给病人重复用药如果你的目标是前者——效率单实例 Redis 就够了偶尔因崩溃或主从切换导致的故障可容忍Redlock 属于过度设计。但 Martin 认为如果目标是正确性Redlock 依然不安全锁可能失效而你甚至察觉不到。2分布式系统中锁会出什么问题分布式系统是个不可控的野兽会遭遇各种意外异常。Martin 把它们归为常见的 NPC trio** N **——网络延迟** P **——进程停顿如 GC** C **——时钟漂移他用 GC 停顿说明危险客户端 1 在节点 A、B、C、D、E 上加锁成功客户端 1 遭遇长时间 GC 停顿所有 Redis 节点上的锁 TTL 过期客户端 2 现在成功在 A、B、C、D、E 上加锁客户端 1 的 GC 完成仍认为自己持有锁客户端 2 也认为自己持有锁 → 冲突Martin 认为 GC 可能在任何时候发生持续时间不可预测。注即使没有 GC 的语言如果发生网络延迟或时钟漂移也会遇到同样问题Martin 只是用 GC 作为具体说明。3所有时钟都是准确的是不安全的假设如果 Redis 节点上的时钟异常Redlock 同样会失去互斥性客户端 1 在 A、B、C 上加锁成功到 D、E 的包丢了节点 C 的时钟跳变导致它的 key 提前过期客户端 2 现在成功在 C、D、E 上加锁到 A、B 的包丢了两个客户端都认为自己拥有锁 → 冲突Martin 的观点Redlock 严重依赖所有节点时钟保持同步一个坏时钟就能毁掉算法。如果 C 崩溃重启而不是时钟跳变结果也一样。他列举了时钟出错的日常原因系统管理员手动重置机器时间NTP 大幅跳变而非缓慢调整总之Redlock 假设了同步系统模型而分布式系统文献表明这在实际部署中不安全。在混乱的现实世界中不能依赖时钟必须为异步现实设计。4用防护令牌fencing token保证正确性Martin 提出不要依赖时间而是用防护令牌锁服务授予锁时同时返回一个单调递增的令牌客户端向共享资源发出的每个请求都带上这个令牌资源服务器拒绝任何令牌小于已见过最大令牌的请求保证只有最新的锁持有者能成功这样无论 NPC 哪种异常发生分布式锁的安全性都能得到保障因为算法建立在异步模型上。相比之下Redlock 无法提供类似防护令牌的功能所以无法保证正确性。Martin 进一步强调设计良好的分布式锁遇到任何 NPC 故障时允许错过截止时间但绝不能返回错误答案。换句话说牺牲的只能是活性性能正确性必须保持完好。Martin 的结论Redlock 是个错配对效率来说太重对正确性来说又太弱不合理的时钟假设算法做了危险假设——认为不同机器的时钟完美同步。一旦违反锁就会失效无法保证正确性因为 Redlock 无法提供类防护令牌机制解决不了正确性问题。如果关心正确性使用提供共识的系统如 ZooKeeper以上就是 Martin 质疑 Redlock 的论点听起来很有根据。接下来看看 Redis 作者 Salvatore SanfilippoAntirez如何回应。Redis 作者 Antirez 的反驳Antirez 的文章围绕三个核心点1澄清时钟问题Antirez 直击核心异议时钟。他认为 Redlock 不需要完美同步的时钟——只需要在已知误差范围内大致同步。比如想计时 5 秒实际可能是 4.5 秒或 5.5 秒。只要累积误差小于锁自动释放窗口算法就是安全的。这种容差在生产环境是现实的。关于显式时钟修改他反驳手动调时钟别这么干就行如果你愿意手写修改 Raft 日志同样能破坏 Raft时钟跳变做好运维规范比如用微小步进而非跳变就能避免大幅跳变实践中可实现Antirez 先解决时钟问题因为他其余的辩护都依赖这个前提。2回应网络延迟和 GC 问题Antirez 反驳 Martin 的场景网络延迟或长 GC 停顿破坏 Redlock。回顾 Martin 的顺序客户端 1 尝试在 A、B、C、D、E 上加锁客户端 1 加锁成功后遭遇长 GC 停顿所有 Redis 节点上的锁条目过期客户端 2 成功在同一组节点上加锁客户端 1 的 GC 完成仍认为自己持有锁客户端 2 也认为自己持有锁 → 冲突Redis 作者反驳这个假设有缺陷——Redlock 能保证锁安全性。如何做到回顾前面描述的 Redlock 五步流程重述一下客户端获取当前时间戳 T1向 5 个 Redis 实例发送加锁请求每个设短超时。任一请求失败立即尝试下一个如果在多数≥3实例上成功加锁获取 T2。如果 T2 - T1 锁 TTL算成功否则算失败成功则操作共享资源失败则向所有节点发释放请求关键是第 1-3 步。为什么第 3 步要重取 T2 并比较 T2 - T1 与锁 TTL作者强调如果第 1-3 步期间发生网络延迟、GC 停顿或任何长时间停滞第 3 步会通过 T2 - T1 检测到。如果耗时超过锁 TTL就判定失败并释放所有锁。进一步论证如果延迟/GC 发生在第 3 步之后即客户端已确认加锁并正在操作资源那么锁可能在操作进行中过期。但这不是 Redlock 特有的缺陷任何锁服务包括 ZooKeeper都有同样问题超出了协议讨论范围。例子客户端通过 Redlock 加锁多数成功耗时检查开始操作共享资源发生长 GC/网络停顿锁自动过期客户端最终发起 MySQL 更新锁可能已被别人持有结论锁确认前Redlock 在第 3 步检测任何长时间延迟确认后NPC网络、停顿、时钟漂移影响所有分布式锁Redlock 和 ZooKeeper 都无法阻止因此在时钟正确的前提下Redlock 是正确的。3对防护令牌机制的挑战作者也挑战 Martin 提出的防护令牌方案提出两点首先这要求共享资源服务器拒绝旧令牌。比如更新 MySQL 时从锁服务获取单调递增令牌附加到行更新中。这要求 MySQL或事务层强制隔离小令牌。UPDATE table T SET val $new_val, current_token $token WHERE id $id AND current_token $token但如果操作不是针对 MySQL 呢比如写磁盘文件或发 HTTP 请求这种方法就无能为力了。这对被操作的资源服务器要求更高。换句话说大多数需要操作的资源服务器并不具备这种互斥能力。再者如果资源服务器已经有互斥能力那还要分布式锁干嘛因此 Redis 作者认为这个方案不成立。其次退一步说即使假设 Redlock 不提供防护令牌能力它仍然提供了一个随机值即前面提到的 UUID。用这个随机值也能实现和防护令牌一样的效果。怎么做Redis 作者只提了一句可以实现类似功能但没展开细节。根据我查到的资料大概流程如下。如有错误欢迎讨论客户端用 Redlock 加锁操作共享资源前客户端用锁的 VALUE 标记共享资源客户端处理业务逻辑最后修改共享资源时检查标记是否一致只有一致才修改类似 Compare-And-Swap 的 CAS 思想仍以 MySQL 为例可能是这样客户端用 Redlock 加锁修改 MySQL 表某行前先把锁的 VALUE 更新到该行的某个字段假设叫 current_token客户端处理业务逻辑修改 MySQL 该行数据时用 VALUE 作为 WHERE 条件确保 token 匹配才修改UPDATE table T SET val $new_val WHERE id $id AND current_token $redlock_value可见这依赖 MySQL 的事务机制也达到了 Martin 提到的防护令牌效果。不过讨论中仍有网友提出小问题当两个客户端用这种方式先标记再检查修改共享资源时这两个客户端之间的操作顺序无法保证。而 Martin 提出的防护令牌——既然是单调递增的数字——资源服务器可以拒绝小令牌从而保证操作的顺序性。Redis 作者对此给出了不同解释我觉得很合理。他解释说**分布式锁的本质是互斥。只要能保证两个客户端并发操作时一个成功一个失败就够了无需关心顺序。**在 Martin 的原文批评中他一直强调这个顺序性问题的重要性。但 Redis 作者持不同看法。总结一下 Redis 作者的结论作者认同对方关于时钟跳变对 Redlock 影响的观点但认为时钟跳变可以避免取决于基础设施和运维实践。Redlock 在设计时考虑了 NPC 问题。如果 NPC 发生在 Redlock 流程的第 3 步之前锁的正确性仍可保证但如果发生在第 3 步之后那不单是 Redlock 的问题其他分布式锁服务同样面临这个问题。因此这类场景不在讨论范围内。很有意思吧分布式系统中一个看似简单的锁竟会遇到这么多影响安全性的复杂场景。不知看完双方观点后你觉得哪边更有说服力既然讲完了 Redis 分布式锁的争议你可能也注意到 Martin 在文章中推荐用ZooKeeper实现分布式锁声称更安全。但真的吗基于 ZooKeeper 的锁安全吗熟悉 ZooKeeper 的话用它实现分布式锁的方式是客户端 1 和客户端 2 都尝试创建临时节点ephemeral node比如/lock假设客户端 1 先到——成功获得锁客户端 2 失败客户端 1 操作共享资源完成后客户端 1 删除/lock 节点释放锁可见不同于 RedisZooKeeper 不用你操心设置锁过期时间。它用临时节点保证客户端 1 只要连接还在就能一直持有锁。而且如果客户端 1 意外崩溃临时节点会自动删除确保即使故障也能释放锁。听起来很美妙。不用担心过期异常自动释放很完美。实则并不完全。想想看客户端 1 创建临时节点后ZooKeeper 怎么保证该客户端继续持有锁原因在于客户端 1 与 ZooKeeper 服务器保持 Session这个 Session 依赖客户端定期发送心跳来维持连接。如果 ZooKeeper 长时间没收到客户端心跳就认为 Session 过期于是临时节点也会被自动删除。同样基于这个问题我们也讨论下 GC 如何影响 ZooKeeper 的锁客户端 1 成功创建临时节点/lock获得锁客户端 1 遭遇长时间垃圾回收GC客户端 1 无法向 ZooKeeper 发送心跳ZooKeeper 删除临时节点即锁被释放客户端 2 成功创建临时节点/lock获得锁客户端 1 的 GC 结束仍认为自己持有锁产生冲突可见即使使用 ZooKeeper在进程 GC 或网络异常延迟场景下也无法保证安全性。这正是 Redis 作者在反驳文章中提到的如果客户端已拿到锁但与锁服务失去连接如因 GC那不单 Redlock 有这个问題——其他锁服务同样有类似问题ZooKeeper 也不例外。因此可以得出结论分布式锁在极端情况下未必安全。如果业务数据高度敏感使用分布式锁时必须注意这个问题不能假定分布式锁 100%安全。现在总结用 ZooKeeper 实现分布式锁的优缺点ZooKeeper 的优势无需考虑锁过期时间Watch 机制让获取锁失败的客户端能监听锁释放实现乐观锁方式ZooKeeper 的劣势性能不如 Redis部署和运维成本高存在客户端与 ZooKeeper 长时间断连导致锁释放的问题我对分布式锁的理解前面详细讨论了用 Redis Redlock 和 ZooKeeper 实现分布式锁在各种异常场景下的安全性问题。现在我想分享个人观点仅供参考欢迎理性讨论。1该不该用 Redlock前面分析过Redlock 正常工作的前提是系统时钟准确。如果能保证这个前提可以考虑使用。但在我看来保证时钟准确不像听起来那么简单。首先从硬件层面时钟漂移是常见且不可避免的现象。CPU 温度、机器负载、芯片材质等因素都会导致时钟偏移。其次从工作经验看我遇到过时钟出错或运维强制修改系统时钟的情况这会影响系统正确性。所以人为失误也很难完全避免。因此我对 Redlock 的看法是能不用就尽量别用。不仅如此它的性能比单实例 Redis 差部署成本也相对较高。我个人会优先考虑用Redis 主从哨兵模式实现分布式锁。那怎么保证正确性这就引出下一点。2如何正确使用分布式锁分析 Martin 的观点时他提到的防护令牌fencing token思路很有启发。虽然这个方案局限性大但在正确性至关重要的场景下是很好的思路。因此我们可以结合两种方案上层用分布式锁实现互斥目标虽然极端情况下锁可能失效但它能在最高层拦截大部分并发请求从而降低底层资源层压力。但对需要绝对数据正确性的业务必须在资源层实现兜底设计思路可以参考 fencing token 方案。两者结合我认为对绝大多数业务场景已经能满足需求了。