在上一篇文章,我们已经实现了分布式锁。今天更进一步,在分布式锁的基础之上,实现读写锁。

完整代码在 https://github.com/SeemSilly/codestory/tree/master/research-zoo-keeper

1 读写锁的概念

参考维基百科的条目: https://zh.wikipedia.org/wiki/读写锁

读写锁是计算机程序的并发控制的一种同步机制,用于解决读写问题,读操作可并发重入,写操作是互斥的。 读写锁有多种读写权限的优先级策略,可以设计为读优先、写优先或不指定优先级。

  • 读优先:允许最大并发的读操作,但可能会饿死写操作;因为写操作必须在没有任何读操作的时候才能够执行。
  • 写优先:只要排队队列中有写操作,读操作就必须等待;
  • 不指定优先级:对读操作和写操作不做任何优先级的假设

不指定优先级的策略,最适合使用ZooKeeper的子节点模式来实现,今天就来尝试这种策略。

2 锁设计

同前面介绍的普通分布式锁,也使用子节点模式实现。先用容器模式(CreateMode.CONTAINER)创建唯一的锁节点,每个锁客户端在锁节点下使用临时循序模式(CreateMode. SEQUENTIAL)创建子节点。这些子节点会自动在名称后面追加10位数字。

2.1 如何标识读锁还是写锁?

有两种简单的方案:在子节点名中标识、在节点的值中标识。如果采用在值中标识,每次子节点列表后,还需要再分别读一下子节点的值,才能判断是读锁还是写锁,会比较耗时。如果在子节点名称中标识,会面临一个问题:在同一个节点中创建的子节点,如果给定的名称不同,追加的10位数字是否仍然是递归的?

写个测试用例验证一下。

public class SequentialTest extends TestBase {
@Test
public void testSequential() throws Exception {
String rootNodeName = "/container-" + System.currentTimeMillis();
ZooKeeperBase zooKeeper = new ZooKeeperBase(address);
zooKeeper.createRootNode(rootNodeName, CreateMode.CONTAINER); Random random = new SecureRandom();
long lastNumber = -1L;
String[] prefixs = new String[] {"/a", "/b", "/c", "/d", "/e", "/f", "/g"};
for (int i = 0; i < 10; i++) {
int index = random.nextInt(prefixs.length);
String childNodeName = rootNodeName + prefixs[index];
String fullNodeName = zooKeeper.getZooKeeper().create(childNodeName, new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
long number = Long.parseLong(fullNodeName.substring(childNodeName.length()));
assert number == lastNumber + 1;
lastNumber = number;
}
}
}

  

测试用例通过,说明在同一个Container中创建的子节点,不论提供的节点名是什么,后续追加的10位数字都是顺序递增的。这样,就可以使用节点名来区分读锁和写锁。

2.2   类设计

介绍分布式锁的时候,已经创建了阻塞锁 ChildrenBlockingLock,读写锁正好可以基于这个类做重载。

2.3   获取锁的逻辑

写锁是一个独占锁,逻辑跟普通分布式锁相同,只要它之前有锁就必须等待。所以,完全沿用阻塞锁的逻辑即可。

读锁允许并发,它之前可以有任意读锁,但不能有写锁。所以只需要判断有没有写锁即可。

3      关键代码

3.1   ChildrenNodeLock.java

这个类,主要是增加了一个获取排序后子节点列表的方法,这样方便实现读写锁的代码。当然,这个操作会增加一些耗时,如果子节点数量太大,可能不适用。

首先定义一个函数,用来返回子节点的前缀

/** 子节点的前缀,缺省是element,子类可以重载 */
protected String getChildPrefix() {
return "element";
}

然后定义一个内部类,子节点排序时会用到

/** 子节点名称比较 */
private class StringCompare implements Comparator<String> {
@Override
public int compare(String string1, String string2) {
return string1.substring(string1.length() - 10)
.compareTo(string2.substring(string2.length() - 10));
}
}

最后实现子节点排序方法,用于代替 getChildren 函数

/** 获取排好序的子节点列表 */
final public List<String> getOrderedChildren(String path, boolean watch)
throws KeeperException, InterruptedException {
List<String> children = getZooKeeper().getChildren(path, watch);
Collections.sort(children, new StringCompare());
return children;
}

3.2   ChildrenBlockingLock.java

在多客户端随机测试时,经常出现程序卡死的情况,无法正常退出。经过添加日志跟踪,发现WatchedEvent可能会丢失,也可能会发送给并不是注册事件的ZooKeeper客户端。在网上搜索,发现很多人也碰到类似问题。

简单修改了一下ChildrenBlockingLock#isLockSuccess等待信号的代码,从无参数的死等变成设置一定超时时间等待。关键代码如下

protected boolean isLockSuccess() {
boolean lockSuccess;
try {
while (true) {
String prevElementName = getPrevElementName();
if (prevElementName == null) {
log.trace("{} 没有更靠前的子节点,加锁成功", elementNodeName);
lockSuccess = true;
break;
} else {
// 有更小的节点,说明当前节点没抢到锁,注册前一个节点的监听。
log.trace("{} 监控 {} 的事件", elementNodeName, prevElementName);
getZooKeeper().exists(this.guidNodeName + "/" + prevElementName, true);
synchronized (mutex) {
// 等待最多一秒
mutex.wait(1000);
log.trace("{} 监控的 {} 有子节点变化", elementNodeName, guidNodeName);
}
}
}
} catch (KeeperException e) {
lockSuccess = false;
} catch (InterruptedException e) {
lockSuccess = false;
}
return lockSuccess;
}

3.3   写锁 ZooKeeperWriteLock.java

代码基本是沿用父类,只需要重载getChildPrefix()方法,

/** 返回写锁的前缀 */
protected String getChildPrefix() {
return "w-lock-";
}

3.4   读锁 ZooKeeperReadLock.java

同写锁相比,除了重载getChildPrefix()方法,还重载了getPrevElementName()用来查找最近一个写锁。

/** 返回读锁的前缀 */
protected String getChildPrefix() {
return "r-lock-";
} /** 是写锁 */
private boolean isWriteLock(String elementName) {
return elementName.startsWith(ZooKeeperWriteLock.FLAG);
} /** 读取前一个写锁 */
protected String getPrevElementName() throws KeeperException, InterruptedException {
List<String> elementNames = super.getOrderedChildren(this.guidNodeName, false);
super.traceOrderedChildren(this.guidNodeName, elementNames);
String prevWriteElementName = null;
for (String oneElementName : elementNames) {
if (this.elementNodeFullName.endsWith(oneElementName)) {
// 已经到了当前节点
break;
}
if (isWriteLock(oneElementName)) {
prevWriteElementName = oneElementName;
}
}
return prevWriteElementName;
}

4      测试用例

测试用例没想到好的判断方法,很难使用assert判断结果,因此做了简化,根据日志输出,靠人眼判断是否正确。

4.1   测试线程类

分别为都锁和写锁构建了两个内部类

/** 写锁线程 */
class WriteLockClient extends Thread {
ZooKeeperWriteLock writeLock; public WriteLockClient() {
try {
this.writeLock = new ZooKeeperWriteLock(address);
} catch (IOException e) {
}
} public void run() {
writeLock.lock(guidNodeName, this.getName());
try {
Thread.sleep(1000 + random.nextInt(20) * 100);
} catch (InterruptedException e) {
}
writeLock.release(guidNodeName, this.getName());
}
} /** 读锁线程 */
class ReadLockClient extends Thread {
ZooKeeperReadLock readLock; public ReadLockClient() {
try {
this.readLock = new ZooKeeperReadLock(address);
} catch (IOException e) {
}
}
public void run() {
readLock.lock(guidNodeName, this.getName());
try {
Thread.sleep(1000 + random.nextInt(20) * 100);
} catch (InterruptedException e) {
}
readLock.release(guidNodeName, this.getName());
try {
readLock.getZooKeeper().close();
} catch (InterruptedException e) {
}
}
}

4.2   读-读锁测试

代码

@Test
public void testReadRead() throws IOException, InterruptedException {
ReadLockClient readLock1 = new ReadLockClient();
ReadLockClient readLock2 = new ReadLockClient();
readLock1.start();
readLock2.start();
readLock1.join();
readLock2.join();
}

测试结果可以看到,两个读锁并发执行

22:18.861 [Thread-2 INFO] r-lock-0000000000 get read lock : true
22:18.865 [Thread-1 INFO] r-lock-0000000001 get read lock : true
22:20.065 [Thread-2 INFO] r-lock-0000000000 release read lock
22:21.366 [Thread-1 INFO] r-lock-0000000001 release read lock

4.3   读-写锁测试

代码

@Test
public void testReadWrite() throws IOException, InterruptedException {
ReadLockClient readLock1 = new ReadLockClient();
WriteLockClient writeLock1 = new WriteLockClient();
readLock1.start();
Thread.sleep(50);
writeLock1.start();
readLock1.join();
writeLock1.join();
}

测试结果可以看到,首先获取读锁,释放之后才获取到写锁。

27:40.800 [Thread-1 INFO] r-lock-0000000000 get read lock : true
27:43.310 [Thread-1 INFO] r-lock-0000000000 release read lock
27:43.423 [Thread-2 INFO] w-lock-0000000001 get write lock : true
27:44.423 [Thread-2 INFO] w-lock-0000000001 release write lock

4.4   写-读锁测试

代码

@Test
public void testWriteRead() throws IOException, InterruptedException {
ReadLockClient readLock1 = new ReadLockClient();
WriteLockClient writeLock1 = new WriteLockClient();
writeLock1.start();
Thread.sleep(50);
readLock1.start();
writeLock1.join();
readLock1.join();
}

测试结果可以看到,首先获取写锁,释放之后才获取到读锁。

29:17.661 [Thread-2 INFO] w-lock-0000000000 get write lock : true
29:19.966 [Thread-2 INFO] w-lock-0000000000 release write lock
29:19.976 [Thread-1 INFO] r-lock-0000000001 get read lock : true
29:22.476 [Thread-1 INFO] r-lock-0000000001 release read lock

4.5   多客户端随机读写锁测试

测试代码

@Test
public void testRandomReadWriteLock() throws IOException, InterruptedException {
int threadCount = 20;
Thread[] lockThreads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
// 一定概率是写锁
boolean writeLock = random.nextInt(5) == 0;
if (writeLock) {
lockThreads[i] = new WriteLockClient();
} else {
lockThreads[i] = new ReadLockClient();
}
lockThreads[i].start();
} for (int i = 0; i < threadCount; i++) {
lockThreads[i].join();
}
}

测试结果可以看出,如果连续多个读锁会并发执行。为了方便查看,我添加了一些横线分隔。

30:31.317 [Thread-1 INFO] w-lock-0000000000 get write lock : true
30:32.824 [Thread-1 INFO] w-lock-0000000000 release write lock
------------------------------------------------------------------
30:32.834 [Thread-17 INFO] r-lock-0000000004 get read lock : true
30:32.835 [Thread-19 INFO] r-lock-0000000002 get read lock : true
30:32.835 [Thread-20 INFO] r-lock-0000000001 get read lock : true
30:32.836 [Thread-18 INFO] r-lock-0000000003 get read lock : true
30:34.135 [Thread-20 INFO] r-lock-0000000001 release read lock
30:34.634 [Thread-17 INFO] r-lock-0000000004 release read lock
30:34.935 [Thread-19 INFO] r-lock-0000000002 release read lock
30:35.036 [Thread-18 INFO] r-lock-0000000003 release read lock
------------------------------------------------------------------
30:35.053 [Thread-16 INFO] w-lock-0000000005 get write lock : true
30:36.154 [Thread-16 INFO] w-lock-0000000005 release write lock
------------------------------------------------------------------
30:36.160 [Thread-14 INFO] r-lock-0000000007 get read lock : true
30:36.160 [Thread-15 INFO] r-lock-0000000006 get read lock : true
30:38.160 [Thread-14 INFO] r-lock-0000000007 release read lock
30:38.661 [Thread-15 INFO] r-lock-0000000006 release read lock
------------------------------------------------------------------
30:38.669 [Thread-13 INFO] w-lock-0000000008 get write lock : true
30:39.969 [Thread-13 INFO] w-lock-0000000008 release write lock
------------------------------------------------------------------
30:39.976 [Thread-12 INFO] r-lock-0000000009 get read lock : true
30:39.977 [Thread-8 INFO] r-lock-0000000014 get read lock : true
30:39.977 [Thread-6 INFO] r-lock-0000000015 get read lock : true
30:39.984 [Thread-10 INFO] r-lock-0000000011 get read lock : true
30:39.985 [Thread-3 INFO] r-lock-0000000018 get read lock : true
30:39.984 [Thread-7 INFO] r-lock-0000000013 get read lock : true
30:39.984 [Thread-11 INFO] r-lock-0000000010 get read lock : true
30:39.983 [Thread-9 INFO] r-lock-0000000012 get read lock : true
30:39.983 [Thread-2 INFO] r-lock-0000000019 get read lock : true
30:39.982 [Thread-5 INFO] r-lock-0000000016 get read lock : true
30:39.986 [Thread-4 INFO] r-lock-0000000017 get read lock : true
30:40.986 [Thread-3 INFO] r-lock-0000000018 release read lock
30:41.086 [Thread-2 INFO] r-lock-0000000019 release read lock
30:41.285 [Thread-6 INFO] r-lock-0000000015 release read lock
30:41.576 [Thread-12 INFO] r-lock-0000000009 release read lock
30:42.185 [Thread-10 INFO] r-lock-0000000011 release read lock
30:42.186 [Thread-5 INFO] r-lock-0000000016 release read lock
30:42.187 [Thread-11 INFO] r-lock-0000000010 release read lock
30:42.286 [Thread-9 INFO] r-lock-0000000012 release read lock
30:42.586 [Thread-7 INFO] r-lock-0000000013 release read lock
30:42.677 [Thread-8 INFO] r-lock-0000000014 release read lock
30:42.887 [Thread-4 INFO] r-lock-0000000017 release read lock

ZooKeeper实现读写锁的更多相关文章

  1. 使用ZooKeeper实现Java跨JVM的分布式锁(读写锁)

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 读写 ...

  2. 三、curator recipes之共享的可重入读写锁

    简介 curator实现了跨JVM的可重入读写互斥锁.它使用zookeeper去进行加锁,所以指定相同路径的处理线程将会基于“公平锁”的机制去竞争锁资源. 读写锁包含了读锁.写锁两个,它们的互斥关系如 ...

  3. 二、多线程基础-乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁

    1.10乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将 比较-设置 ...

  4. 【分布式锁】07-Zookeeper实现分布式锁:Semaphore、读写锁实现原理

    前言 前面已经讲解了Zookeeper可重入锁的实现原理,自己对分布式锁也有了更深的认知. 我在公众号中发了一个疑问,相比于Redis来说,Zookeeper的实现方式要更好一些,即便Redis作者实 ...

  5. 技术笔记:Delphi多线程应用读写锁

    在多线程应用中锁是一个很简单又很复杂的技术,之所以要用到锁是因为在多进程/线程环境下,一段代码可能会被同时访问到,如果这段代码涉及到了共享资源(数据)就需要保证数据的正确性.也就是所谓的线程安全.之前 ...

  6. java多线程-读写锁

    Java5 在 java.util.concurrent 包中已经包含了读写锁.尽管如此,我们还是应该了解其实现背后的原理. 读/写锁的 Java 实现(Read / Write Lock Java ...

  7. 让C#轻松实现读写锁分离

    ReaderWriterLockSlim 类 表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问. 使用 ReaderWriterLockSlim 来保护由多个线程读取但每次只采用一 ...

  8. C#读写锁ReaderWriterLockSlim的使用

    读写锁的概念很简单,允许多个线程同时获取读锁,但同一时间只允许一个线程获得写锁,因此也称作共享-独占锁.在C#中,推荐使用ReaderWriterLockSlim类来完成读写锁的功能. 某些场合下,对 ...

  9. 可重入锁 公平锁 读写锁、CLH队列、CLH队列锁、自旋锁、排队自旋锁、MCS锁、CLH锁

    1.可重入锁 如果锁具备可重入性,则称作为可重入锁. ========================================== (转)可重入和不可重入 2011-10-04 21:38 这 ...

随机推荐

  1. Java多线程下载器FileDownloader(支持断点续传、代理等功能)

    前言 在我的任务清单中,很早就有了一个文件下载器,但一直忙着没空去写.最近刚好放假,便抽了些时间完成了下文中的这个下载器. 介绍 同样的,还是先上效果图吧. Jar包地址位于 FileDownload ...

  2. 万字长文:ELK(V7)部署与架构分析

    ELK(7版本)部署与架构分析 1.ELK的背景介绍与应用场景 在项目应用运行的过程中,往往会产生大量的日志,我们往往需要根据日志来定位分析我们的服务器项目运行情况与BUG产生位置.一般情况下直接在日 ...

  3. Atom实用插件

    下载atom狠戳本链接 中文简体插件(工具栏,右键菜单,设置菜单) simplified-chinese-menu 代码格式化插件(支持多种语言) atom-beautify 智能补全资源路径插件 a ...

  4. case和decode的用法(行转列)

    创建了一张成绩表,如下图所示: 在oracle中,这两个函数我们都可以使用,代码及结果如下: decode用法: select Name,decode(Subject,'语文',1,'数学',2,'英 ...

  5. Log4Net 配置日志按日期和日志级别分类写入

    配置效果图: 配置代码: <?xml version="1.0" encoding="utf-8" ?> <log4net> <! ...

  6. Appium+python自动化(二十六)- 烟花一瞬,昙花一现 -Toats提示(超详解)

    简介 今天宏哥在这里首先给小伙伴们和童鞋们分享一个有关昙花的小典故:话说昙花原是一位花神,她每天都开花,四季都灿烂.她还爱上了每天给她浇水除草的年轻人.后来,此事给玉帝得知.于是,玉帝大发雷霆,要拆散 ...

  7. C# 10分钟完成百度图片提取文字(文字识别)——入门篇

    现在图片文字识别已经很成熟了,比如qq长按图片,点击图片识别就可以识别图片的文字,将不认识的.文字数量大的.或者不能赋值的值进行二次可复制功能. 我们现在就基于百度Ai开放平台进行个人文字识别,dem ...

  8. 【Python-Django定义用户模型类】Python-Django定义用户模型类详解!!!

    定义用户模型类 1. Django默认用户认证系统 Django自带用户认证系统 它处理用户账号.组.权限以及基于cookie的用户会话. Django认证系统位置 django.contrib.au ...

  9. html+css+dom补充

    补充1:页面布局 一般像京东主页左侧右侧都留有空白,用margin:0 auto居中,一般.w. <!DOCTYPE html> <html lang="en"& ...

  10. 浅入深出Vue:自动化路由

    在软件开发的过程中,"自动化"这个词出现的频率是比较高的.自动化测试,自动化数据映射以及各式的代码生成器.这些词语的背后,也说明了在软件开发的过程中,对于那些重复.千篇一律的事情. ...