利用zookeeper实现分布式任务锁
依赖原理
- 在ZK中添加基本节点,路径为锁名称,节点类型为持久节点(PERSISTENT)。
- 对需要获取锁的每个线程,在ZK中分别添加基本节点的子节点,路径程序自定为temp,类型为临时自编号节点(EPHEMERAL_SEQUENTIAL),并保存创建返回的实际节点路径。
- 通过delete方式删除本线程创建的子节点,可以作为锁释放的方式。
- 基本节点的子节点类型为临时自编号节点(EPHEMERAL_SEQUENTIAL),当线程与ZK连接中断后,ZK会自动将该节点删除,确保了断连之后的锁释放。
- 由于ZK自编号产生的路径是递增的,因此可以通过判断基本节点的子节点中最小路径数字编号的节点是否是本线程新建的节点来判断是否获取到锁。
原理图示
利用zk实现的分布式任务锁实现原理如下:
8个线程分别尝试获取分布式任务锁,情况如下:
- (1)8个线程分别在ZK基本节点下创建临时自编号节点,获取创建成功后的实际路径
- (2)在基本节点子节点列表中,判断本线程创建节点编号是否为最小
- (3)最小编号线程获取分布式任务锁,执行临界区程序,完成任务
线程执行完任务,释放锁,情况如下:
- (1)线程释放锁,将ZK中对应的临时节点删除,此时基本节点下路径最小的子节点获取分布式任务锁
- (2)某线程由于网络原因与ZK断开了连接,退出锁竞争,ZK自动将其对应的临时节点删除
- (3)新出现的线程加入锁竞争,在ZK下创建临时节点,排队等待锁竞争
方案一 :轮询方式
实现原理
程序流程图如下:
实现代码
1 | import org.apache.zookeeper.*; |
测试
测试程序
1 | private void testZKLockOneWithMultiThread() throws Exception { |
结果:
1 | 线程:[17]获取到任务锁,并执行了任务 |
方案一优劣
优点
- 通过递归实现循环轮询
- 程序实现逻辑简单易懂
- 不需要实现监听节点变动的watcher
劣势
- 每个在阻塞状态下竞争锁的线程,都需要在固定时间间隔查询所有存活节点情况,导致网络开销巨大,资源浪费巨大
方案二 :父节点监听方式
实现原理
程序流程图如下:
实现代码
1 | import org.apache.zookeeper.*; |
测试
测试程序
1 | private void testZKLockTwoWithMultiThread() throws Exception { |
结果:
1 | 线程:[4]获取到任务锁,并执行了任务 |
方案二优劣
优点
- 实现对父节点变动状态(主要是子节点列表变化)的监听
- 当子节点列表出现变化后,ZK通知监听的各个线程,各个线程查询子节点状态
- 相对于轮询方式来说,避免了很大一部分网络开销和资源浪费
- 对父节点进行监听,实现起来相对简单
劣势
- 每个在阻塞状态下竞争锁的线程,都监听了父节点状态,即父节点出现变动(主要是子节点列表变化)后,ZK服务器需要通知到所有注册监听的线程,网络消耗和资源浪费依然比较大
方案三 :子节点监听方式
实现原理
程序流程图如下:
实现代码
1 | import org.apache.zookeeper.*; |
测试
测试程序
1 | private void testZKLockThreeWithMultiThread() throws Exception { |
结果:
1 | 线程:[4]获取到任务锁,并执行了任务 |
方案优劣
优点
- 实现对子节点变动状态(排序在本线程对应节点之前的一个节点)的监听
- 被监听子节点变动(删除)之后,ZK通知本线程执行相应操作,进行锁竞争
- 相对于父节点监听方式来说,子节点监听方式在每一次锁释放(或者节点变动)时,ZK仅通知到一个线程的watcher操作,节省了大量的网络消耗和资源占用
劣势
- 实现方式与程序逻辑较轮询和父节点监听来说比较繁琐
总结比较
对这三种基于ZK的分布式任务锁的实现方式进行比较,可以得出这些结论:
程序复杂度:
轮询方式 < 父节点监听方式 < 子节点监听方式网络资源消耗:
轮询方式 >> 父节点监听方式 >> 子节点监听方式程序可靠性
轮询方式 << 父节点监听方式 < 子节点监听方式