前面介绍了如何用redis来构建分布式锁 ,今天来介绍下如何通过zookeeper来实现分布式锁。
Curator是zookeeper的一个高级客户端操作API,在Curator中实现了分布式锁,主节点选举等功能。其中分布式锁实现的关键是通过zookeeper创建的节点来实现,稍后会通过代码来说明是如何实现的。
1.1 InterProcessMutex基本简介
InterProcessMutex一个可重入锁,提供分布式锁的入口服务。基本的构造过程如下:
public InterProcessMutex(CuratorFramework client, String path)
{
this(client, path, LOCK_NAME, 1, new StandardLockInternalsDriver());
}
构造器内部最终会构造一个
internals = new LockInternals(client, driver, path, lockName, maxLeases);
LockInternals这个是所有申请锁与释放锁的核心实现
1.2 InterProcessMutex的获取锁
InterProcessMutex.internalLock()提供两种机制来加锁,
第一种是无限等待,直到获取到锁。第二种是有限等待,在规定的时间内获取锁,如果木有失败。
该方法内部简略如下:
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
lockData.lockCount.incrementAndGet();
return true;
}
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
对于同一个线程再次获取锁的时候,会判断当前线程是否已经拥有了,
如果拥有了,则直接做原子操作加1,然后返回true,这样就实现了可重入锁。
对于其他情况,则都会调用 LockInternals.attemptLock();
1.3 LockInternals.attemptLock()
1)根据传入的超时做判断,是否需要millisToWait设置
2)创建临时顺序节点路径:
ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
假如basepath=/zklock/activityno,这个是活动no的根路径,这个path是构建InterProcessMutex设置的。
则path是/zklock/activityno/lock-,注意这个path是在basepath下创建的,lock-是curator自动添加的
则创建的顺序节点如: /zklock/activityno/_c_f4a49d75-86f8-40b2-8b9c-d813392aa1db-lock-0000000008
尤其要注意这里,LockInternals会对所有请求获取锁的线程都会创建一个临时顺序节点,节点后缀顺序由zk来保证,
同时zk客户端底层能够保证同一个客户端发送的请求是按照顺序的,这样就能够保证同一个客户端先申请锁创建的后缀序号比后申请的编号小。
3)循环等待尝试枷锁internalLockLoop(startMillis, millisToWait, ourPath);
内部核心代码流程如下:
3.1) 获取所有的子节点
List<String> children = getSortedChildren();
排序:获取basepath下所有的子节点,然后截取lock-后面的编号,做升序排序,注意这里的升序排序,保证了最先申请锁的排在最开始,公平策略是根据谁先申请那么你的优先级就应该最高
String sequenceNodeName = ourPath.substring(basePath.length() + 1);
获取节点名如: _c_f4a49d75-86f8-40b2-8b9c-d813392aa1db-lock-0000000008
3.2) 核心获取锁的判断.
根据拿到的所有子节点路径以及当前子节点去尝试获取锁。
maxLeases代表是租赁个数,对于分布式互斥锁,这里值为1,保证了只允许租赁一个。
这里就是获取锁的核心实现,若最终获取成功,则直接return,否则进行无限wait或者有限wait()
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
if ( predicateResults.getsTheLock() )
{
haveTheLock = true;
}else{
//wait等待,有限或者无限等待,注意这里用了线程的wait来做等待
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
}
根据driver.getsTheLock的结果,如果木有获取到,则就会watcher返回的path,然后根据传入的时间来做wait操作。
注意这里wait是互斥信号量是LockInternals. 自定义的watcher很简单,一旦监听的到的节点数据变更或删除,则就直接notifyFromWatcher();
driver.getsTheLock内部代码如下:
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
//先根据子节点名获取在已经升序的list中的索引位置。
int ourIndex = children.indexOf(sequenceNodeName);
validateOurIndex(sequenceNodeName, ourIndex);
//比较索引位置,由于maxLeases=1,则只有ourIndex=0才成立,这样就可以用来判断当前子节点是否是升序第一个节点,并且也有很好的扩展性
//可以改变maxLeases来允许同时租赁的数量
//注意,这里升序是根据zk生成的顺序编号排序的,申请越早编号越小。
boolean getsTheLock = ourIndex < maxLeases;
//若getsTheLock=true,表示获取到锁,否则获取它上一个位置的路径,注意这个路径会用来做watche的
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
return new PredicateResults(pathToWatch, getsTheLock);
}
1.4 InterProcessMutex的释放锁
主要是判断是否是当前线程,或者非当前线程。最终会根据线程号找到对应的path路径,然后直接删除该临时节点。
1.5 InterProcessMutex总结
1)curator的InterProcessMutex提供了多种锁机制,互斥锁,读写锁,以及可定时数的互斥锁。
2)所有申请锁都会创建临时顺序节点,保证了都能够有机会去获取锁。
3)内部用了线程的wait()和notifyAll()这种等待机制,可以及时的唤醒最渴望得到锁的线程。避免常规利用Thread.sleep()这种无用的间隔等待机制。
4) 利用redis做锁的时候,一般都需要做锁的有效时间限定。而curator则利用了zookeeper的临时顺序节点特性,一旦客户端失去连接后,则就会自动清除该节点。
1.6.cas指令是通过获取锁来控制并发的 其伪指令大概如下
期望值 0
更新值1
1.比较内存中实际值与期望值是否相等
2.如果相等跟新值为1从而阻止其他线程进来
3.执行线程操作
4.操作完成将值重新设置为0 线程重新竞争锁
那么我完全在软件代码层面来考虑就有个疑问如果是单核处理器因为任意时刻其实只有一个进程能执行操作我们要做的就是保证进程在它执行期间的改变能让接下来的进程看到并更新到最新结果就可以了,但是如果是多核处理器完全可能在同一时刻有多个进程执行上面的过程,我们再做一个大胆的假设:两个进程其中一个在完成上面第二部操作前另一个也判断到0了进来执行接下面的部分,那么是不是可能两个进程都能拿到锁,虽然大家都知道其实这四步操作是原子性的,但是每当读到这个地方我就很迷惑,往上应用层面保证原子性的可以通过cas实现的锁机制来完成,那么作为大厦的基石cas是怎么保证自己的原子性呢?毕竟我看到这四步伪代码在软件代码层面我看不到原子操作的存在,不知道有没有同感的人。那么既然cas指令是由cpu提供的指令那么我们就只能在硬件层面去试着理解,这个问题实现的关键就是多核处理器工作对于并发操作同一时刻要只能允许一个cpu工作,那么小伙伴们立马就能想到锁总线,我也是查阅了一些资料看到的主流观点就是说根据因特尔芯片资料说明大概意思就是这么实现的,更高级点的就是说现在只会对cas操作进行锁定不会对总线上的其他操作做锁定从而提高cpu效率。回到最初的疑问硬件就不存在那种刚好同时发生的可能性吗?这里我自己做了一个完全主观的电路图假想,完全是为了自我安慰消除疑惑做的,毕竟这个什么锁总线对于没实际搞过电路CPU的我来说太抽象了。
图中方框表示参与竞争的进程 ,圆圈表示开关电路,最上面表示一个获取锁的状态值。电路的大概逻辑是进程开始竞争的时候去触发方框上面的开关使其闭合,闭合后会导致其他三个进程左边默认闭合的开关断开,电路上半部分其实就是一个并行的开关电路,只有其中一个进程左边的开关闭合另外三个断开就能触发获取锁的状态的改变,也就是电路里面左右两边有电势差就会触发开关的闭合,都为高或者都为低电平都不会触发开关闭合,这样即使是及其罕见的有多个进程同时触发了方框上面的开关然后又同时触发了除自己外的其他三个进程的左边开关的断开,也只会导致获取锁的状态值维持竞争态,所有参与的线程重新来一遍,而不会出现有多个进程获取锁。真正的电路实现肯定更复杂,这里我只是想说明硬件实现原子性的一个想法。