基于zookeeper的分布式lock实现
背景
?继续上一篇文章:http://agapple.iteye.com/blog/1183972?,项目中需要对分布式任务进行调度,那对应的分布式lock实现在所难免。
?
?这一周,在基于BooleanMutex的基础上,实现了zookeeper的分布式锁,用于控制多进程+多线程的lock控制
?
算法可以预先看一下zookeeper的官方文档:?
?
http://zookeeper.apache.org/doc/trunk/recipes.htmllock操作过程:首先为一个lock场景,在zookeeper中指定对应的一个根节点,用于记录资源竞争的内容每个lock创建后,会lazy在zookeeper中创建一个node节点,表明对应的资源竞争标识。 (小技巧:node节点为EPHEMERAL_SEQUENTIAL,自增长的临时节点)进行lock操作时,获取对应lock根节点下的所有字节点,也即处于竞争中的资源标识按照Fair竞争的原则,按照对应的自增内容做排序,取出编号最小的一个节点做为lock的owner,判断自己的节点id是否就为owner id,如果是则返回,lock成功。如果自己非owner id,按照排序的结果找到序号比自己前一位的id,关注它锁释放的操作(也就是exist watcher),形成一个链式的触发过程。unlock操作过程:将自己id对应的节点删除即可,对应的下一个排队的节点就可以收到Watcher事件,从而被唤醒得到锁后退出其中的几个关键点:
- node节点选择为EPHEMERAL_SEQUENTIAL很重要。
* 自增长的特性,可以方便构建一个基于Fair特性的锁,前一个节点唤醒后一个节点,形成一个链式的触发过程。可以有效的避免"惊群效应"(一个锁释放,所有等待的线程都被唤醒),有针对性的唤醒,提升性能。
* 选择一个EPHEMERAL临时节点的特性。因为和zookeeper交互是一个网络操作,不可控因素过多,比如网络断了,上一个节点释放锁的操作会失败。临时节点是和对应的session挂接的,session一旦超时或者异常退出其节点就会消失,类似于ReentrantLock中等待队列Thread的被中断处理。获取lock操作是一个阻塞的操作,而对应的Watcher是一个异步事件,所以需要使用信号进行通知,正好使用上一篇文章中提到的BooleanMutex,可以比较方便的解决锁重入的问题。(锁重入可以理解为多次读操作,锁释放为写抢占操作)
注意:使用EPHEMERAL会引出一个风险:在非正常情况下,网络延迟比较大会出现session timeout,zookeeper就会认为该client已关闭,从而销毁其id标示,竞争资源的下一个id就可以获取锁。这时可能会有两个process同时拿到锁在跑任务,所以设置好session timeout很重要。同样使用PERSISTENT同样会存在一个死锁的风险,进程异常退出后,对应的竞争资源id一直没有删除,下一个id一直无法获取到锁对象。没有两全其美的做法,两者取其一,选择自己一个能接受的即可
?
代码相关说明:
?
?
测试代码:
?
?
升级版?实现了一个分布式lock后,可以解决多进程之间的同步问题,但设计多线程+多进程的lock控制需求,单jvm中每个线程都和zookeeper进行网络交互成本就有点高了,所以基于DistributedLock,实现了一个分布式二层锁。
?
大致原理就是ReentrantLock 和?DistributedLock的一个结合。
?
?
?单jvm的多线程竞争时,首先需要先拿到第一层的ReentrantLock的锁拿到锁之后这个线程再去和其他JVM的线程竞争锁,最后拿到之后锁之后就开始处理任务。锁的释放过程是一个反方向的操作,先释放DistributedLock,再释放ReentrantLock。?可以思考一下,如果先释放ReentrantLock,假如这个JVM?ReentrantLock竞争度比较高,一直其他JVM的锁竞争容易被饿死。
代码:最后其实再可以发散一下,实现一个分布式的read/write lock,也差不多就是这个理了。项目结束后,有时间可以写一下
?
大致思路:
?
竞争资源标示: ?read_自增id , write_自增id首先按照自增id进行排序,如果队列的前边都是read标识,对应的所有read都获得锁。如果队列的前边是write标识,第一个write节点获取锁watcher监听: read监听距离自己最近的一个write节点的exist,write监听距离自己最近的一个节点(read或者write节点)
?
?
