Zookeeper分布式锁安全吗
Martin和Antirez争论点
在之前的文章讨论基于Redis的RedLock分布式锁中,有提到剑桥分布式专家Martin指出,RedLock安全性并不高,并且其中有一个假设场景如下
假设存在多个实例A、B、C、D、E,同时存在客户端1和2有如下场景
- 客户端1请求实例A、B、C、D、E获取锁成功。
- 客户端1开始操作共享资源,这时发生GC网络暂停,stop-the-world。
- 在GC期间客户端1持有的所有实例上的锁过期。
- 客户端2向实例A、B、C、D、E请求获取锁,成功。
- 客户端2操作共享资源,这时客户端1从GC中恢复,客户端1无法感知锁已经过期,也操作共享资源导致冲突。
这个假设Redis之父Antirez指出在获取锁后发生的NPC(N:网络延迟、P:进程暂停、C:时钟跳跃)问题RedLock无法处理,但这不仅仅是RedLock的问题,其余分布式锁也有这个问题,如Zookeeper。
Zookeeper是否安全呢
上面的场景与我们理解的如果需要构建更加安全的分布式锁,首要参考的就是Zookeeper思想有冲突,那么我们应该如何抉择呢,下面参考Zookeeper的作者之一Flavio Junqueira写的一篇博客Note on fencing and distributed locks
原文链接:https://fpj.systems/2016/02/10/note-on-fencing-and-distributed-locks/
Zookeeper构建分布式锁的步骤
Flavio Junqueira指出构建的一种方式,步骤如下
- 客户端尝试创建一个znode节点如/lock,第一个客户端创建成功后相当于拿到了锁,后续的客户端再去创建/lock因为已经存在那么创建会失败。
- 持有锁的客户端在访问共享资源后,将znode节点删除,那么其它客户端可以继续创建znode节点。
- znode的节点应该是临时的,这样才能保证如果客户端突然崩溃,这个客户端持有的znode节点才会被删除,保证锁的释放。
这样看起来非常完美,因为没有Redis那种设置自动过期时间,可能因为时钟跳跃导致锁提前过期的情况,但真是这样吗,我们先来思考一个问题,客户端突然崩溃zookeeper如何能快速感知到呢?
Zookeeper心跳检测
每个客户端都会和Zookeeper之间维护一个Session,这个Session依赖心跳维护,如果Zookeeper在规定时间内未收到客户端心跳回复,那么将认为客户端失去链接,这时就会将这个Session创建的所有临时节点删除。
Zookeeper的安全问题
正是因为Zookeeper存在心跳检测这个问题,那么可能出现以下场景。
- 客户端1连接Zookeeper后创建一个znode临时节点/lock成功。
- 客户端1进入长时间的GC,进程暂停。
- 客户端1连接到Zookeeper的session长时间未收到心跳回复,并且超过过期时间,自动删除创建的临时节点/lock。
- 客户端2连接Zookeeper后同样创建znode临时节点/lock成功。
- 客户端2开始操作共享资源,这时客户端1恢复同样操作共享资源,冲突产生。
这个场景就和上面分布式专家Martin提出的场景类似,在获取锁后发生NPC问题,这是单纯依靠分布式锁无法处理。
ZooKeeper的watch机制
ZooKeeper虽然单纯依赖自己无法解决获取锁后的NPC安全性问题,但是其watch特性,将分布式锁变地像一个单机锁实现。
当客户端试图创建一个临时节点/lock时,如果发现节点已经创建,这时客户端可以不立即失败,客户端可以进入一个阻塞等待状态,等待当/lock节点被删除后,Zookeeper通过watch机制通知给客户端,这样的方式就好像JAVA中获取单机锁一样方便。
Zookeeper和Redis对比
Zookeeper和Redis都能实现分布式锁,优势如下
- 在客户端和Zookeeper连接正常的情况下,客户端可以持有锁任意时长,这可以确保客户端在持有锁后操作共享资源并不会因为业务操作过长而导致锁过期,可以解决Redis过期时间到底设置多久的难题。
- Zookeeper支持的Watch机制,可以让Zookeeper实现的分布式锁,使用起来更加灵活,像使用本地锁一样。
劣势
- Zookeeper与客户端如果长期没有心跳那么锁将自动释放。
- 性能不如Redis。
总结
分布式锁可以由多种方式实现,但是看完分布式专家Martin和Redis作者的讨论后,在极端情况分布式锁并不是完全安全的,Zookeeper也不能例外,所以这也就是分布式锁都面临着NPC三座大山的考验,如果我们放在关键业务处理时并不能完全依靠分布式锁,还需要有类似Martin提出的fecing token防护令牌兜底。