读写锁:使用场景和实现方案(ZooKeeper 实现)

业余草

共 3495字,需浏览 7分钟

 ·

2021-04-17 10:39

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,我们一起精进!你不来,我和你的竞争对手一起精进!

编辑:业余草

http://rrd.me/g6P3V

推荐:https://www.xttblog.com/?p=5178

0.概要

锁,分为「互斥锁」和「共享锁」:

  • 互斥锁写锁,是互斥锁
  • 共享锁读锁,是共享锁

互斥锁,保证了资源的串行有序访问,但,系统并发性能较低,在「高频读-低频写」的场景中,一般采用「读写锁」方案,ReadWriteLock,支持共享读操作。

下文从几个方面展开:

  1. 读写锁的特性
  2. ZooKeeper 中,读写锁的实现
  3. 实践建议

1.读写锁的特性

「读-写锁」,业务上,要满足「读共享、写互斥」即可,实际场景中,需要考虑多种策略,他们都会影响最终「读写锁」的性能:

  1. 释放优先:当一个操作「释放写锁」时,并且队列中同时存在读线程写线程时,那么是读线程优先获得锁,还是写线程,或者说是最先发出请求的线程

  2. 读线程插队:如果当读线程「持有读锁」时,有写线程在等待,那么新到达的读线程能否立即获得访问权,还是应该在写线程后面等待?

    • 如果允许读线程插队到线程前面,那么将提高并发性,但却可能造成写线程发生饥饿问题。
  3. 重入性:读锁、写锁是否允许重入。

  4. 降级:如果一个线程「持有写锁」,那么它能否在不释放锁的情况下「降级成读锁」?

  5. 升级:「持有读锁」的线程能否优于其他正在等待的读线程和写线程而「升级成写锁」?

    • 在大多数的「读-写锁」实现中,并不支持升级,因为很容易造成死锁(如果两个读线程同时升级为写锁,那么二者都不会释放读取锁)

读写锁的含义

  1. 写锁:互斥,同一时刻,只有一个进程,持有写锁

  2. 读锁:共享,同一时刻,可以多个进程,持有读锁

  3. 组合 :

    1. 读锁:所有的「写锁」都失效时,可以「加读锁」,读锁共享
    2. 写锁:所有的「读锁」和「写锁」都失效时,可以「加写锁」,写锁互斥,跟所有的读写动作都互斥

2.ZooKeeper 中,读写锁的实现

ZooKeeper 的读写锁,本质是

  1. 生成 2 类锁:一个读锁(共享)、一个写锁(互斥)
  2. 同一个目录下,创建「临时顺序节点」,前缀不同共享自增序号

补充信息

  1. 创建 /zookeeperLock/sharedLock/ip-type-id 的「临时顺序节点」,来代表读写锁

  2. 其中

    type

    有 2 种枚举值:

    1. R:读锁(共享)
    2. W:写锁(互斥)
  3. 获取「读锁」,会在ZooKeeper 上,创建类似节点:/sharedLock/10.0.10.1-R-0000000001

  4. 获取「写锁」,会在ZooKeeper 上,创建类似节点:/sharedLock/10.0.10.1-W-0000000002

具体 ZooKeeper 内部,采用 InterProcessReadWriteLock 实现共享的读写锁,具体过程:

  1. 创建临时顺序节点」:根据需要获取的锁,创建对应的「读」或者「写」对应的「临时顺序节点」

  2. 获取「临时顺序节点」的全量列表

  3. 获取「读锁」或者「写锁」,业务

    逻辑判断:

    1. 读锁:所有前驱节点中,没有「W 类型」节点存在,则,获取 R 读锁成功
    2. 写锁:所有前驱节点都不存在,则,获取 W 写锁成功;即,当前节点的「序号最小」,则,获取 W 写锁成功;
  4. 获取「读锁」或者「写锁失败,则,监听「当前路径」的「子节点列表变更」,进入等待锁状态。Note:为了避免「羊群效应」,可以只监听「前驱节点」。

Note:

从上述处理逻辑中,可以看出,InterProcessReadWriteLock 是「公平锁」。

具体 ZooKeeper 命令,使用示例:

[zk: localhost:2181(CONNECTED) 1] ls /[zookeeper, zk_test]# 1. 创建「临时顺序节点」[zk: localhost:2181(CONNECTED) 2] create -s -e /zk_test/P-W- helloCreated /zk_test/P-W-0000000000[zk: localhost:2181(CONNECTED) 3] create -s -e /zk_test/P-W- helloCreated /zk_test/P-W-0000000001# 2. 创建「临时顺序节点」,前缀不同[zk: localhost:2181(CONNECTED) 5] create -s -e /zk_test/P-R- helloCreated /zk_test/P-R-0000000002[zk: localhost:2181(CONNECTED) 6] create -s -e /zk_test/P-R- helloCreated /zk_test/P-R-0000000003[zk: localhost:2181(CONNECTED) 7] create -s -e /zk_test/P-R- helloCreated /zk_test/P-R-0000000004[zk: localhost:2181(CONNECTED) 8] create -s -e /zk_test/P-R- helloCreated /zk_test/P-R-0000000005# 3. 查看「临时顺序节点」[zk: localhost:2181(CONNECTED) 9] ls /zk_test[P-R-0000000002, P-W-0000000000, P-R-0000000003, P-W-0000000001, P-R-0000000004, P-R-0000000005]# 4. 查询「临时顺序节点」所属的 Session:ephemeralOwner 对应为 session 的 ID[zk: localhost:2181(CONNECTED) 10] get /zk_test/P-W-0000000000hellocZxid = 0x8ctime = Wed Feb 27 15:38:11 CST 2019mZxid = 0x8mtime = Wed Feb 27 15:38:11 CST 2019pZxid = 0x8cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x1692ddeb5fb0002dataLength = 5numChildren = 0[zk: localhost:2181(CONNECTED) 11] get /zk_test/P-W-0000000001hellocZxid = 0x9ctime = Wed Feb 27 15:38:17 CST 2019mZxid = 0x9mtime = Wed Feb 27 15:38:17 CST 2019pZxid = 0x9cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x1692ddeb5fb0002dataLength = 6numChildren = 0

3.实践建议

与「互斥锁」相比,「读-写锁」允许对共享数据,进行更高的并发访问

虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。

实践中:

在实践中,「读-写锁」只有在多处理器上,高频读 + 低频写 场景下,才能提高性能。

而在其他情况下,「读-写锁」的性能却比「独占锁」的性能要差一点,这是因为「读-写锁」的复杂性更高

所以,要对程序进行分析,判断「读-写锁」是否能提高性能,特别是,大多数场景下,高频读的场景,可依赖读取缓存提升并发能力

4.参考资料

  1. 读写锁ReadWriteLock
  2. ZooKeeper典型应用——分布式锁

浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报