简介
当我们分布式系统中多个节点需要访问同一共享数据,就需要加一把分布式锁,因为如果是同一进程的线程的话,完全可以采用Java的同步锁实现,但是这是多进程间的锁,所以就需要一个协调者来协调进程间的通信。该协调者就可以是Zookeeper。
使用Zookeeper实现分布式排它锁
demo是采用Zkclient客户端框架实现。
原理:
使用Zookeeper实现分布式排它锁的主要原理是利用Zookeeper节点的特性和Watcher机制实现的。
常用的有两种逻辑:
第一种
步骤:
- 需要访问某共享资源的节点进程,往Zookeeper的同一父节点(比如/locks)下去创建一个临时有序节点。
- 每个进程获取该父节点下的子节点列表,并且获取一个最小的子节点的路径,与当前自己创建的临时有序节点是否是同一节点,是的话就获取锁,不是的话就获取锁失败。
- 获取锁失败后,需要监听比它小的上一个节点的删除事件,并阻塞进程。一旦被监听的节点被删除了,触发了节点删除事件,在回调中就可以唤醒该进程进行获得锁。
主要就这三步,就可以使用Zookeeper实现一个分布式排它锁。
使用临时有序节点的原因:
使用有序节点是因为获取锁的顺序是按照节点的顺序来获取的。
使用临时节点的原因是:使用持久化节点有一个弊端,就是当客户端在释放锁之前失去连接,导致没有删除节点,此时又是持久化节点,就会形成一个死锁。如果是临时节点的话,客户端会话超时后,就会自动删除该节点释放锁,就不会形成死锁。
demo(代码上的注释很重要):
/**
* @author YeHaocong
* @decription 分布式锁 ,用线程模拟进程
*/
public class DistributedWriteLock implements IZkDataListener{
//模拟多个进程的共享数据
private static int count = 0;
//ZkClient客户端
private ZkClient zkClient = ZkClientDemo.getZkClient();
//父节点路径
private static final String PARENT = "/locks";
//节点的数据
private static final byte[] DEFAULT_DATA = "".getBytes();
//锁的路径,只要当前最小的路径等于该路径,就证明该客户端获得了锁
private String lockPath;
//发令器
private CountDownLatch latch;
/**
* 获取锁的方法
*/
public void lock(){
//创建一个临时有序节点,有序节点是必须的,临时节点是可以在客户端会话连接超时,无法删除节点时自动删除节点,释放锁。
lockPath = zkClient.createEphemeralSequential(PARENT + "/",DEFAULT_DATA);
String threadName = Thread.currentThread().getName();
//节点创建了,就开始竞争锁了。
System.out.println("进程" + threadName + "创建节点了" + lockPath + "开始竞争锁");
//获取父节点下的所有子节点
List<String> children = zkClient.getChildren(PARENT);
//将子节点排序,不过要加上PARENT前缀,因为获取子节点的路径只是节点的名称,不是全路径
TreeSet<String> treeSet = new TreeSet<>();
for (String child:children){
treeSet.add(PARENT +"/"+ child);
}
//获取有序节点中最小的 节点
String canGetLockNode = treeSet.first();
if (lockPath.equals(canGetLockNode)){
//如果当前客户端所创建的节点与最小的节点是同一节点,就获取到了锁。
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
}else {
//不一样就获取不到锁。就要阻塞住
latch = new CountDownLatch(1);
//获取当前客户端创建的节点的上一个节点,也就是排序后,刚好比它小的上一个节点
String previousNode = treeSet.headSet(lockPath).last();
//当前客户端监听该节点,当该节点被删除时,也就是释放锁的时刻,就唤醒该进程获得锁。
zkClient.subscribeDataChanges(previousNode,this);
if (!zkClient.exists(previousNode)){
//这里有一种情况,就是在当前客户端监听previousNode节点之前,该节点就已经被删除了,此时客户端就监听不到
//previousNode节点的删除事件,因为监听前都被删除了,此时就无法调用回调函数唤醒进程。
//所以在阻塞进程之前,就要进行一次判断,判断该节点是否已经被删除,如果删除了,就代表已经释放锁了,这里
//就直接不用当前进程不用阻塞,直接就获取锁就行了。
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
}else {
//假如监听前节点没被删除,此时就是正确的监听了。
//这里还有一种情况,就是假如我监听了之后,还没来得及进入这里节点被删除了,触发了节点删除事件,
//发令器latch的值被countDown减为0,下面的阻塞latch.await()等于无效,这种情况也符合了逻辑。
//如果正常情况,下面阻塞住了当前进程,当监听的节点被删除时,就会触发节点删除事件,在回调函数中就会把
//发令器减为0唤醒被阻塞的进程。
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
}
}
}
/**
* 释放锁的方法
*/
public void unLock(){
//就是直接删除节点
zkClient.delete(lockPath);
System.out.println("进程" + Thread.currentThread().getName() + "释放了分布式锁");
}
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
//节点被删除后,就触发了该事件回调,把发令器减一,唤醒进程
latch.countDown();
}
//测试
public static void main(String[] args) {
//模拟10个进程区抢分布式锁
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0;i<10;i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
//先阻塞住,等所有进程准备好就一个唤醒
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建一个分布式锁对象。用于争取分布式锁
DistributedWriteLock fenBuShiSuo = new DistributedWriteLock();
//争取锁,如果获取到了,就直接执行下面的逻辑,获取不到就阻塞住,等待锁的释放。
fenBuShiSuo.lock();
//对共享资源的操作
count ++ ;
//操作完就释放锁
fenBuShiSuo.unLock();
}
});
//启动
thread.start();
latch.countDown();
}
try {
//主进程睡眠50秒,等待count的最终值
TimeUnit.SECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
}
第二种
步骤:
- 需要访问某共享资源的节点进程,往Zookeeper的同一父节点(比如/locks)下去创建同一个临时节点(节点路径一致)。
- 因为只能有一个进程能创建成功,其他进程会报错节点已经存在,创建成功就等于获得锁。失败就监听该节点的删除事件并进入阻塞状态,等待锁的释放(删除节点)。
- 一旦锁释放(节点被删除)了,就会调用回调方法,唤醒所有线程再次创建该节点,此时也只能有一个进程创建成功,获取锁。
第二种方法就不实现了,逻辑比第一种要简单一点。
两种方法比较:
第一种方法:每次获得锁的进程是确定的,因为创建的临时有序节点小的进程就会先获取到锁,并且每次释放锁之后都只会唤醒一个进程。
第二种方法:每次获得锁的进程是不确定的,因为不知道哪个进程会创建节点成功,并且每次释放锁之后会唤醒所有进程进行抢占锁。
分布式共享锁的原理和实现
共享锁的特点是:在读操作时可以进行读操作,在读操作时不能进行写操作,在写操作时不能进行读操作和写操作。
共享锁的实现逻辑要比排它锁复杂很多。
所以使用Zookeeper实现共享锁的步骤如下:
- 需要访问某共享资源的节点进程,往Zookeeper的同一父节点(比如/locks)下去创建一个临时有序节点。此时创建的节点分为读节点和写节点。可以用前缀指定或者用节点值指定。
- 按顺序第一个写节点前的所有读节点都可以获取到锁同时进行读操作。
- 当前写节点为w1,上一个写节点为w2,w2应该监听w1与w2之后的所有读节点,这些读节点全部释放锁(删除后)才到该写节点w2获取锁。有一种情况是w1和w2是相邻,中间没有读节点,此时w2就要监听w1,直到w1释放锁w2才能获取锁。
- 读节点就要监听上一个写节点,该写节点释放锁就能获取锁。
总结步骤是:先处理最小写节点前面的读节点、再处理最小写节点、最后处理最小写节点后面的节点。
比如锁顺序
r1-r2-w3-r4-r5-w6-w7-r8-r9-w10
第一步:获取最小的写节点,也就是w3,然后让前面的两个读操作r1、r2获取锁,然后w3,监听r1、r2节点。
第二步:r4、r5监听w3节点,w6监听r4、r5节点,w7监听w6节点,r8、r9监听监听w7节点,w10监听r8、r9节点。
比如锁顺序
w1-r2-w3-r4-r5-w6-w7-r8-r9-w10
第一步:获取到第一个写节点w1,发现前面没有读节点,直接获取锁。
第二步:r2监听w1,w3监听r2,r4、r5监听w3,w6监听r4、r5节点,w7监听w6节点,r8、r9监听监听w7节点,w10监听r8、r9节点。
比如锁顺序:
r1-r2-r3-r4-r5
此时没有写节点,所以获取到的最小写节点为null,此时直接让所有读节点获取锁。
比如锁顺序:
w1-w2-w3-w4-w5
此时没有读节点,这里就跟排它锁一样。
代码(注释很重要):
/**
* @author YeHaocong
* @decription Zookeeper分布式共享锁实现
*/
public class DistributedReadLock implements IZkDataListener{
//模拟多个进程的共享数据
private static int count = 0;
//读节点的前缀。
private static final String READ_PREFIX = "r-";
//写节点的前缀。
private static final String WRITE_PREFIX = "w-";
//这里就限定死读节点前缀与写节点前缀一样长了。为了使逻辑简单一些
private static final int PREFIX_LENGTH = WRITE_PREFIX.length();
//ZkClient客户端
private ZkClient zkClient = ZkClientDemo.getZkClient();
//父节点路径
//父节点的路径上都不能带有 读节点前缀 或者写节点前缀,不然就会使得逻辑不能符合预期
private static final String PARENT = "/readLocks";
//节点的数据
private static final byte[] DEFAULT_DATA = "".getBytes();
//锁的路径,只要当前最小的路径等于该路径,就证明该客户端获得了锁
private String lockPath;
//是读节点还是写节点true是读节点
private Boolean isRead;
//发令器
private CountDownLatch latch;
//当前锁路径创建的lockEntry 键时把路径前缀w-或者r-去掉,值就是锁的全路径
private Map.Entry<String,String> lockEntry;
//两个写节点之前已经释放了锁的节点路径会被记录到这里。用于防止重复释放锁
private Set<String> readNodesSets = new CopyOnWriteArraySet<>();
public DistributedReadLock(boolean isRead){
this.isRead = isRead;
}
/**
* 获取锁的方法
* isRead 是否是读节点、
*/
public void lock(){
//创建一个临时有序节点,有序节点是必须的,临时节点是可以在客户端会话连接超时,无法删除节点时自动删除节点,释放锁,防止死锁。
if (isRead){
//如果是读进程,就创建读节点 r-开头
lockPath = zkClient.createEphemeralSequential(PARENT + "/" + READ_PREFIX,DEFAULT_DATA);
}else {
//如果是写进程,就创建写节点 w- 开头
lockPath = zkClient.createEphemeralSequential(PARENT + "/" + WRITE_PREFIX,DEFAULT_DATA);
}
//获取当前线程名称
String threadName = Thread.currentThread().getName();
//节点创建了,就开始竞争锁了。
System.out.println("进程" + threadName + "创建节点了" + lockPath + "开始竞争锁");
//获取父节点下的所有子节点
List<String> children = zkClient.getChildren(PARENT);
//将子节点存进有排序功能的TreeMap里,键是子节点路径把前缀去掉,值是节点的全路径
TreeMap<String,String> treeMap = new TreeMap<>();
String key = null;
for (String child:children){
//因为前缀会影响实际的排序。
// 例如本来w-1要排在r-2前面,由于w比r大,就会变得r-2排在w-1前面,所以排序前要把前缀去掉,作为treemap的key来由treemap排序
//把子节点的前缀去掉,只剩下Zookeeper为我们生成的带顺序的后缀
String tempChild = child.substring(PREFIX_LENGTH);
//把节点补上父节点路径作为treemap的值
String childWithParent = PARENT + "/" + child;
//记录 当前 lockPath的key,用于下面保存一个lockEntry到本地
if (childWithParent.equals(lockPath))
key = tempChild;
treeMap.put(tempChild,childWithParent);
}
//保存到本地
lockEntry = treeMap.floorEntry(key);
//获取子节点中最小的写节点
Map.Entry<String,String> minWriteNode = null;
for (Map.Entry<String,String> entry:treeMap.entrySet()){
if (entry.getValue().contains(WRITE_PREFIX)){
minWriteNode = entry;
break;
}
}
if (minWriteNode == null){
//如果minWriteNode为null 表示没有写节点,表示全部都是读操作,直接就全部都能获取锁。
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
return;
}
//获取最小的写节点前面的读节点,这些首先获取到锁,并且能够同时获取锁。
//headMap方法时获取比指定key小的treeMap集合。默认不包括指定key。
SortedMap<String,String> readNodes = treeMap.headMap(minWriteNode.getKey());
//获取最小的写节点后面的所有节点集合,tailMap与headMap方法不同,headMap方法默认是不包括本节点的,tailMap默认是包括的。
//我们要让他不包括
NavigableMap<String,String> afterNodes = treeMap.tailMap(minWriteNode.getKey(),false);
//最小写节点前面有读节点的情况,也就是最小写节点不是最小节点的情况
if (!readNodes.isEmpty()){
if (readNodes.containsValue(lockPath)){
//如果当前的进程锁创建的节点是readNodes的元素的话,就直接获取锁。
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
}else {
//如果当前的进程锁创建的节点是readNodes的元素的情况
if (lockPath.equals(minWriteNode.getValue())){
//如果当前的进程锁创建的节点是最小写节点的话,就进入这里
//创建一个发令器,发令器的count为最小写节点前面的读节点个数,因为写节点获取锁要等到前面的读节点全部释放后才能获取锁,
//所以每一个读节点释放锁了,latch就countdown一次,知道所以读节点释放了,count就为0,就换唤醒该进程
latch = new CountDownLatch(readNodes.size());
for (Map.Entry<String,String> entry:readNodes.entrySet()){
//遍历最小写节点前面的读节点,使用最小写节点的客户端区监听这些读节点的删除事件。
zkClient.subscribeDataChanges(entry.getValue(),this);
if (!zkClient.exists(entry.getValue())){
//这个判断是防止监听该读节点前该读节点就已经被删除了,这样的话,就不会触发该读节点的删除事件了。这里就要防止这个情况。
//还要做多一层判断,判断该读节点的删除事件回调是否已经被执行,使用readNodesSets中是否包含该节点判断,包含的话就证明
//该读节点的删除事件回调是否已经被执行,这里就不用重复执行一次是否锁的操作。
if (!readNodesSets.contains(entry.getValue())){
//如果读节点已经被删除,但是读节点的删除事件没有触发,也就是监听该读节点前,该读节点已经被删除了,就在这里释放锁。
//使用latch.countDown();释放。
latch.countDown();
//释放之后把他加到已释放读锁集合里。
readNodesSets.add(entry.getValue());
}
}
}
try {
//阻塞当前写进程,直到读节点全部释放锁。latch的count值就为0,就会唤醒该进程,获取锁。
latch.await();
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//处理最小写节点后面的节点,不包括最小写节点。
handElseNode(minWriteNode,afterNodes,threadName);
}
}
}
//当最小写节点是最小节点,也就是最小写节点前面没有读节点的情况
else {
//判断当前进程是不是最小写节点的进程,是就直接获取锁。
if (lockPath.equals(minWriteNode.getValue()))
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
//处理最小写节点后面的节点,不包括最小写节点。
handElseNode(minWriteNode,afterNodes,threadName);
}
}
/**
* 处理最小写节点后面的节点,不包括最小写节点。
* @param minWriteNode 最小写节点
* @param afterNodes 最小写节点后面的节点,不包括最小写节点。
* @param threadName 进程名称
*/
private void handElseNode(Map.Entry<String,String> minWriteNode,NavigableMap<String,String> afterNodes,
String threadName){
//当前进程的节点是读节点的情况,读节点就要监控比它小,并且离他最近的写节点
if (lockEntry.getValue().contains(READ_PREFIX)){
//获取比当前进程节点小的节点集合
NavigableMap<String,String> stringStringNavigableMap = (NavigableMap<String, String>) afterNodes.headMap(lockEntry.getKey());
//把节点顺序进行翻转。比如 本来比它小的节点结合有 w-2、r-3、r-4、w-5、r-6、r-7,如果要顺序遍历找到比它小,并且离他最近的写节点的话比较困难
//因为不到最后都不知道哪个是离他最近的写节点,所以将他反过来遍历,遍历到的第一个写节点就是离它最近的写节点,也就是w5
NavigableMap<String, String> tempMap = stringStringNavigableMap.descendingMap();
Map.Entry<String,String> preWriteNodeEntry = null;
for (Map.Entry<String,String> tempEntry:tempMap.entrySet()){
//遍历,找到离它最近的写节点
if (tempEntry.getValue().contains(WRITE_PREFIX)){
preWriteNodeEntry = tempEntry;
break;
}
}
//如果找不到离它最近的写节点,因为最小写节点不包含在afterNodes里面,所以如果离读节点最近的写节点是最小写节点的话,就会获取不到,获取不到时,就
//把最小写节点赋值给最近写节点。
preWriteNodeEntry = preWriteNodeEntry == null?minWriteNode:preWriteNodeEntry;
//获取最近写节点的路径。
String preWriteNode = preWriteNodeEntry.getValue();
//因为读节点只需监听一个写节点,所以latch的count为1
latch = new CountDownLatch(1);
zkClient.subscribeDataChanges(preWriteNode,this);
if (!zkClient.exists(preWriteNode)){
//这里防止监听最近写节点前该写节点就被删除了,导致无法触发删除事件的情况发生。因为已经被删除(释放锁)了,所以该读节点就可以直接获取锁。
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
}else {
try {
//阻塞当前读节点,直至最近写节点释放锁。就会被唤醒
latch.await();
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//当前进程的节点是写节点的情况,要监听它与他上一个写节点之间的读节点,比如当前写节点是w8,r4、 w5、r6、r7、w8,就要监听w5与w8之间的读节点r6、r7。
else {
//获取比当前进程写节点小的节点,也就是 r4、 w5、r6、r7
NavigableMap<String,String> stringStringNavigableMap = (NavigableMap<String, String>) afterNodes.headMap(lockEntry.getKey());
//因为首先要找到最近写节点w5,才能获取他们之间的读节点,这个如果正常遍历的话要遍历到最后才能确定最近写节点,但是如果反向遍历的话。
// r7、r6、w5、r4 我就可以直接遍历到r7就把 他添加到集合,遍历r6就把他添加到集合,遍历到的第一个写节点就是最近写节点,就直接停止遍历。
//所以此处将map节点顺序反过来了
NavigableMap<String, String> tempMap = stringStringNavigableMap.descendingMap();
//记录两写节点间的读节点的集合
TreeMap<String,String> readNodesMap = new TreeMap<>();
//记录最近写节点
Map.Entry<String,String> preWriteNodeEntry = null;
for (Map.Entry<String,String> tempEntry:tempMap.entrySet()){
if (tempEntry.getValue().contains(WRITE_PREFIX)){
//如果遍历到写节点,就是最近写节点,直接复制给preWriteNodeEntry,然后终止循环
preWriteNodeEntry = tempEntry;
break;
}
//还没遍历到写节点,就把读节点添加到集合中。
readNodesMap.put(tempEntry.getKey(),tempEntry.getValue());
}
//stringStringNavigableMap没有写节点,就是最近写节点是最小写节点preWriteNodeEntry
preWriteNodeEntry = preWriteNodeEntry == null?minWriteNode:preWriteNodeEntry;
//创建latch,count值是两写节点之间的读节点个数。
latch = new CountDownLatch(readNodesMap.size());
//分两种情况,这里这种是两个写节点之间有读节点的情况。
if (!readNodesMap.isEmpty()){
//这里就像处理上面的最小写节点前面有读节点的操作一样。
for (Map.Entry<String,String> tempEntry:readNodesMap.entrySet()){
zkClient.subscribeDataChanges(tempEntry.getValue(),this);
if (!zkClient.exists(tempEntry.getValue())){
if (!readNodesSets.contains(tempEntry.getValue())){
latch.countDown();
readNodesSets.add(tempEntry.getValue());
}
}
}
try {
latch.await();
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//这是另外一种情况,就是两个写节点是挨着的,就是中间没有读节点,比如w5,w6,当前节点是w6,这种情况w6就要监听w5节点,
//当w5节点释放他就唤醒获取锁
else {
//监控上一个写节点,比如w5
zkClient.subscribeDataChanges(preWriteNodeEntry.getValue(),this);
//创建latch
latch = new CountDownLatch(1);
if (!lockPath.equals(minWriteNode.getValue())){
if (!zkClient.exists(preWriteNodeEntry.getValue())){
//防止还没监听相邻写节点之前就相邻写节点就被删除的情况
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
}
try {
//阻塞,直至相邻写节点释放。
latch.await();
System.out.println("进程" + threadName + "获得了分布式锁,节点是[" + lockPath + "]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 释放锁的方法
*/
public void unLock(){
//就是直接删除节点
zkClient.delete(lockPath);
System.out.println("释放了分布式锁,节点[" + lockPath + "]");
}
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
synchronized (readNodesSets){
if (!readNodesSets.contains(s)){
latch.countDown();
readNodesSets.add(s);
}
}
}
public static void main(String[] args) {
//模拟10个进程区抢分布式读锁 ,7个读进程、3个写进程
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0;i<7;i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
//先阻塞住,等所有进程准备好就一个唤醒
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建一个分布式锁对象。用于争取分布式锁
DistributedReadLock distributedReadLock = new DistributedReadLock(true);
//争取锁,如果获取到了,就直接执行下面的逻辑,获取不到就阻塞住,等待锁的释放。
distributedReadLock.lock();
//对共享资源的操作
System.out.println("读进程" + Thread.currentThread().getName() + "读到的count = " + count);
//操作完就释放锁
distributedReadLock.unLock();
}
});
//启动
thread.start();
latch.countDown();
}
for (int i = 0;i<3;i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
//先阻塞住,等所有进程准备好就一个唤醒
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建一个分布式锁对象。用于争取分布式锁
DistributedReadLock distributedReadLock = new DistributedReadLock(false);
//争取锁,如果获取到了,就直接执行下面的逻辑,获取不到就阻塞住,等待锁的释放。
distributedReadLock.lock();
//对共享资源的操作
System.out.println(Thread.currentThread().getName() + "执行写操作");
count ++ ;
//操作完就释放锁
distributedReadLock.unLock();
}
});
//启动
thread.start();
latch.countDown();
}
try {
//主进程睡眠50秒,等待count的最终值
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
}
结果:
由结果可知:
锁的顺序是
r-0 、r-1 、w-2 、r-3 、r-4 、w-5 、r-6 、r-7 、r-8 、w-9
所以获取锁的顺序是
- r-0和r-1能同时获取锁。
- 等r-0和r-1都释放了锁,w-2就获取锁。
- 等w-2释放了锁 r-3和r-4就能同时获取锁。
- 等 r-3和r-4都释放了锁,w-5就能获取锁。
- 等w-5释放了锁,r-6 、r-7 、r-8 就能同时获得锁。
- 等r-6 、r-7 、r-8 都释放了锁,w-9就能获得锁。
分布式队列:
分布式队列主要有两种:
先进先出队列:
此队列就是先入列就先执行。与排它锁的第一种实现原理基本一模一样,不过如果不想队列中的任务丢失的话,可以使用持久化有序节点。不使用临时有序节点。创建的节点小的任务就是先入列的任务。不过他与排它锁有着一点本质的区别,排它锁的作用是防止分布式节点同时访问同一共享资源,而分布式队列更多的是协调任务先后顺序有序进行,与共享资源没有关系。
Barrier模式:
这个其实也是先进先出的队列,只是他加了个阈值,比如阈值为10,那么队列要等到任务为10个时才进行执行任务。