内容简介:作者 | Sunny杏仁后端工程师,专注高并发和分布式编程,Golang爱好者。
作者 | Sunny
杏仁后端工程师,专注高并发和分布式编程,Golang爱好者。
在 ZooKeeper 分布式锁实践(上篇)排它锁 中我们通过代码实践了如何使用 ZooKeeper 组件来实现排他锁。
排他锁简单易用,但是缺点也很明显:
-
竞争压力大:当锁被占用之后,其他获取锁的操作只能阻塞等待;当锁释放后,所有等待锁的进程会在同一时刻争抢锁的使用权。
-
羊群响应:锁释放后,会通知所有等待锁的进程,如果等待者特别多,一时间锁的竞争压力将会特别大。
简单来说就是: 通知范围太广、锁的粒度太大 。我们可以分别从这两个层面去寻找解决方案:
-
缩小通知范围:等待锁的小伙伴们按先来后到的顺序排队吧,排好队了,接下来我只需要关心我前面一个节点的状态,当前一个节点被释放,我再去抢锁。
-
缩小锁的粒度:锁不关心业务,但是可以简单地通过操作的读、写性质来二分锁的粒度:
-
读锁:又称共享锁,如果前面没有写节点,可以直接上锁;当前面有写节点时,则等待距离自己最近的写节点释放( 删除 )。
-
写锁:如果前面没有节点,可以直接上锁;如果前面有节点,则等待前一个节点释放( 删除 )。
思考:为什么不是关注前面距离自己最近的写节点?
如果两个写节点之间有读节点,必需等待读节点释放之后再进行写节点请求,否则会有不可重复读的问题。
数据结构 和排他锁一样,我们通过 ZooKeeper 的节点来表示一个读写锁的父节点,如 /SHARE_LOCK
,通过父节点下的临时自增子节点来表示一个读写操作请求,如 /SHARE_LOCK/R_0000000001
。整体数据结构如下图所示。
算法
获取锁
获取锁的算法步骤:
-
开始尝试获取锁
-
如果持久化父节点不存在,则创建父节点
-
如果当前临时自增子节点不存在,则创建子节点
-
获取父节点下的所有子节点
-
在所有子节点中,查找序号比当前子节点小的前置子节点( 最近的兄节点 )有两种情况:
-
读请求:查找比自己小的前置**写**子节点 ( 最近的兄节点 )
-
写请求:查找比自己小的前置子节点 ( 最近的兄节点 )
如果没有更小的前置子节点,则持有锁
如果有更小的前置子节点,则监听该子节点被释放( 删除 )的事件
释放 ( 删除 )子节点事件被触发后,重复第 1 步
释放锁
释放锁的算法与排他锁部分的释放锁算法相似,这里不再赘述。
加锁、解锁流程
加锁、解锁完整的流程图。
代码实现
子节点定义
子节点属性
-
lockName
读写锁的名称,即父节点的名称 -
name
子节点的名称,格式为 :{请求类型:R/W}_{自增序号}
( 子节点的路径为:{lockName}/{name}
) -
seq
子节点的自增序号,通过解析name
属性_
下划线分隔符后面的数字字符串来获取序号( ZooKeeper 创建临时自增节点时会自动分配 Int 范围内的序号 ) -
isWrite
子节点是否为写请求,通过解析name
属性_
下划线分隔符前面的英文字符来判断请求类型 : -
R :读请求
-
W :写请求
读写锁定义和初始设置
读写锁的属性
-
lockName
读写锁的名称,即父节点的路径 -
locker
获取锁的请求方,即锁的持有者,释放锁时需要验证请求者与锁的持有者是否一致 -
isWrite
请求类型: -
读:
false
-
写:
true
读写锁的初始设置
-
连接到 ZooKeeper 实例
-
连接后,如果父节点不存在,则创建父节点
尝试获取锁
尝试获取锁的算法实现
-
获取或创建 ZooKeeper 子节点
-
获取当前子节点后,遍历所有的子节点,查找:
-
front
离当前子节点最近的兄节点:序号比当前子节点的序号小、且在小于当前序号的子节点中序号是最大的 -
fontWrite
离当前子节点最近的写、兄节点:序号比当前子节点的序号小、且在小于当前序号的子节点中序号是最大的、且为写子节点
查找后,返回序号更小的兄节点:
-
读请求:返回最近的写、兄节点,用于 Watch 监听释放( 删除 )事件
-
写请求:返回最近的兄节点,用于 Watch 监听释放( 删除 )事件
如果没有更小的子节点,返回 None
,表示成功地获取了锁
同步获取锁
同步获取锁的算法实现
-
尝试获取锁
-
如果没有兄节点,则成功持有锁
-
如果得到更小的兄节点,则监听该兄节点的释放( 删除 )事件
-
收到兄节点的释放( 删除 )事件通知后,重复第 1 步
测试验证
最后,通过一个简单的测试方法来验证读写锁的加、解锁过程:
测试结果
测试结果、分析
# | 请求方 | 操作 | 输出 |
---|---|---|---|
1 | LOCK1_读 | 加锁:成功 | [LOCK1] : Lock |
2 | LOCK2_写 | 加锁:等待,因为有未释放的兄节点 LOCK1 | |
3 | LOCK3_写 | 加锁:等待,因为有未释放的兄节点 LOCK2 | |
4 | LOCK4_读 | 加锁:等待,因为有未释放的兄、写节点 LOCK3 | |
5 | LOCK5_读 | 加锁:等待,因为有未释放的兄、写节点 LOCK3 | |
6 | LOCK1_读 | 解锁:成功,通知到 LOCK2 | [LOCK1] : Unlock |
7 | LOCK2_写 | 收到通知,尝试加锁:成功 | [LOCK2] : Lock |
8 | LOCK2_写 | 解锁:成功,通知到 LOCK3 | [LOCK2] : Unlock |
9 | LOCK3_写 | 收到通知,加锁成功 | [LOCK3] : Lock |
10 | LOCK3_写 | 解锁成功,通知到 LOCK4、LOCK5 | [LOCK3] : Unlock |
11 | LOCK4_读 | 收到通知,尝试加锁:成功 | [LOCK4] : Lock |
12 | LOCK5_读 | 收到通知,尝试加锁:成功 | [LOCK5] : Lock |
13 | LOCK4_读 | 解锁:成功 | [LOCK4] : Unlock |
14 | LOCK5_读 | 解锁:成功 | [LOCK5] : Unlock |
尾声
通过 ZooKeeper 分布式锁实践,对它的接口最直观的感受就是 简单 。虽然它没有直接提供加锁、解锁这样的原语,但是当你了解了它的数据结构、接口和事件设计之后,加锁、解锁功能简直呼之欲出,实现起来毫无障碍,一切都是那么地合理、妥当。
而 ZooKeeper 的能力远不止于此,就像前面提到的它能够十分轻松地实现诸如:数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁、分布式队列这些小菜,不得不佩服 ZooKeeper 设计者的抽象能力。本篇只是浅尝了 ZooKeeper 的基本能力,有关它的设计思路、实现细节仍待进一步发掘和探索。
全文完
以下文章您可能也会感兴趣:
-
分布式锁实践之一:基于 Redis 的实现
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。
杏仁技术站
长按左侧二维码关注我们,这里有一群热血青年期待着与您相会。
以上所述就是小编给大家介绍的《ZooKeeper 分布式锁实践(下):读写锁》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【分布式锁】07-Zookeeper实现分布式锁:Semaphore、读写锁实现原理
- 一种区分读写操作的对工程实践友好的分布式一致性协议
- 探秘分布式解决方案: 多机DB带来的挑战——谈数据库的读写分离、非对称复制和数据分发
- 想用数据库“读写分离” 请先明白“读写分离”解决什么问题
- Java 读写锁浅析
- Golang文件读写
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
智能家居:商业模式+案例分析+应用实战
陈国嘉 / 人民邮电出版社 / 2016-4 / 49.80元
作为万物互联的关键一环,智能家居的出现和普及已经势不可当,以移动互联网为核心的新技术正在重构智能家居。只有成为智能家居行业的先行者,才能抢占“风口”。 《智能家居:商业模式+案例分析+应用实战》紧扣“智能家居”,从3个方面进行专业、深层次的讲解。首要方面是基础篇,从智能家居的发展现状、产业链、商业分析、抢占入口等方面进行阐述,让读者对智能家居有个初步的认识;第二个方面是技术篇,从智能家居的控......一起来看看 《智能家居:商业模式+案例分析+应用实战》 这本书的介绍吧!