Zookeep 分布式锁
Posted jianminglin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zookeep 分布式锁相关的知识,希望对你有一定的参考价值。
什么是分布式锁
概述
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。
分布式锁应具备的条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 高可用的获取锁与释放锁
- 高性能的获取锁与释放锁
- 具备可重入特性
- 具备锁失效机制
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
分布式锁有哪些实现
- Memcached:利用 Memcached 的
add
命令。此命令是原子性操作,只有在key
不存在的情况下,才能add
成功,也就意味着线程得到了锁。 - Redis:利用
setnx
命令。此命令同样是原子性操作,只有在key
不存在的情况下,才能set
成功。 - Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。
- Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。
Redis分布式锁的实现
加锁
setnx(lock_sale_商品ID, 1)
当一个线程实行 setnx
返回 1
,说明 key
原本不存在,该线程成功的到了锁;当一个线程执行setnx
返回 0
,说明 key
已经存在,该线程抢锁失败。
解锁
当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行 del
指令,伪代码如下:
del(lock_sale_商品ID)
释放锁后,其他线程就可以继续执行 setnx
命令来获得锁。
锁超时
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将永远被锁住(死锁),别的线程再也别想进来。所以,setnx
的 key
必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx
不支持超市参数,所以需要额外的指令,伪代码如下:
expire(lock_sale_商品ID, 30)
综合伪代码
if(setnx(lock_sale_商品ID, 1) == 1)
expire(lock_sale_商品ID, 30)
try
do something
finally
del(lock_sale_商品ID)
存在什么问题
setnx
和 expire
的非原子性
setnx
不支持传入超时时间,但可以使用 set
指令,并增加可选参数:
set(lock_sale_商品ID, 1, 30, NX)
del
导致误删
可以在 del
释放锁之前做一个判断,验证当前的锁是不是自己加的锁。具体实现:可以在加锁的时候把当前的线程ID当做 value
,并在删除之前验证 key
对应的 value
是不是自己线程的ID。
加锁:
String threadId = Thread.currentThread().getId()
set(key, threadId, 30, NX)
解锁: 导致新问题:判断和释放锁是两个独立操作,不是原子性的。
if(threadId.equals(redisClient.get(key)))
del(key)
对于分布式锁,应减少死锁的可能,并保证操作的原子性
什么是Zookeeper
概述
Zookeeper 是一种分布式协调服务,用于管理大型主机。在分布式环境中协调和管理服务是一个复杂的过程。Zookeeper 通过极其简单的架构和 API 解决了这个问题。
Zookeeper 的数据模型
Zookeeper 的数据模型很像数据结构中的树,也很像文件系统的目录。
树是由节点组成的,Zookeeper 的数据存储也是同样基于节点,这种节点佳作Znode。对于不同树的节点,Znode 的引用方式是路径引用,类似于文件路径:/动物/猫
/汽车/宝马
Znode包含哪些元素
- data:Znode 存储的数据信息。
- ACL:记录 Znode 的访问权限,即那些人或哪些IP可以访问本节点。
- stat:包含 Znode 的各种元数据,比如事务ID、版本号、时间戳、大小等。
- child:当前节点的子节点引用。
注意:Zookeeper 是为读多写少的场景所设计。Znode并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息,每个节点的数据最大不能超过 1MB。
Zookeeper 的基本操作
- 创建节点
create
- 删除节点
delete
- 判断节点是否存在
exists
- 获得一个节点的数据
getData
- 设置一个节点的数据
setData
- 获取节点下的所有节点
getChildren
其中,exests
, getData
, getChildren
属于读操作。Zookeeper 客户端在请求读操作的时候,可以选择是否设置 Watch。
Zookeeper 的事件通知
我们可以把 Watch 理解成是注册在特定 Znode 上的触发器。当这个 Znode 发生改变,也就是调用了 create
, delete
,setData
等方法的时候,将会出发 Znode 上注册的对应事件,请求 Watch 的客户端会接受到异步通知。
具体交互过程:
- 客户端调用
getData
方法,watch
参数是true
。服务端接收到请求,返回节点数据,并且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。 - 当 Watch 的 Znode 已删除,服务端会查找哈希表,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 Key-Value。
Zookeeper 的一致性
Zookeeper 身为分布式系统协调服务,为了防止单机挂掉的情况,Zookeeper 维护了一个集群。
Zookeeper Service 集群是一主多从结构。
在更新数据时,首先更新到主节点(这里的节点指的是服务器,不是 Znode),在同步到从节点。
在读取数据时,直接读取任意从节点。
为了保证主从节点的数据一致性,Zookeeper 采用 ZAB协议,这种协议非常类似于一致性算法那 Paxos 和 Raft。
什么是ZAB协议
Zookeeper Atomic Broadcast,有效解决了 Zookeeper 集群崩溃恢复,以及主从同步数据的问题。
ZAB 协议定义的三种节点状态
- Looking:选举状态。
- Following:Follower节点(从节点)所处的状态。
- Leading:Leader节点(主节点)所处状态。
最大 ZXID
最大 ZXID 也就是节点本地的最新事务编号,包含 epoch 和计数两部分。epoch是纪元的意思,相当于Raft算法选主时候的 term。
ZAB 崩溃恢复
加入 Zookeeper 当前主节点挂掉了,集群会进行奔溃恢复。ZAB 的奔溃恢复分成三个阶段:
Leader Election
- 选举阶段,此时集群中的节点处于 Looking 状态。它们会向其他节点发起投票,投票当中包含自己的服务器ID 和最新事务ID(ZXID)。
- 接下来,节点会用自身的 ZXID 和从其他节点接收到的 ZXID 作比较,如果发现别人家的 ZXID 比自己大,也就是数据比自己新,那么就重新发起投票,投票给目前已知最大的ZXID所属节点。
- 每次投票后,服务器都会统计投票数量,判断是否有某个节点得到半数以上的投票。如果存在这样的节点,该节点就会成为准 Leader,状态变成 Leading。其他节点的状态变为 Following。
Discovery
发现阶段,用于在从节点中发现最新的 ZXID和事务日志(为了防止某些意外的情况,比如因网络原因在上一阶段产生了多个Leader的情况)。
- 在这一阶段,Leader 集思广益,接受所有Follower发来各自的最新 epoch 值。Leader从中选出最大的epoch,基于此值加 1,生成新的 epoch 分发给各个 Follower。
- 各个 Follower 收到全新的 epoch 后,返回 ACK 给Leader,带上各自最大的 ZXID 和历史事务日志。Leader 选出最大的ZXID,并更新自身历史日志。
Synchronization
同步阶段,把 Leader 刚才收集得到的最新历史事务日志,同步给集群中所有的 Follower。只有当半数 Follower 同步成功,这个准 Leader 才能成为正式的 Leader。
自此,故障恢复正式完成。
ZAB 的数据写入
ZAB 的数据写入涉及到 Broadcast 阶段,简单来说,就是 Zookeeper 常规情况下更新数据的时候,由 Leader 广播到所有的 Follower。其过程如下:
- 客户端发出写入数据请求给任意 Follower。
- Follower 把写入数据请求转发给 Leader。
- Leader 采用二阶段提交方式,先发送 Propose 广播给 Follower。
- Follower 接收到 Propose 消息,写入日志成功后,返回 ACK 消息给 Leader。
- Leader 接收到半数以上 ACK 消息,返回成功给客户端,并且广播 Commit 请求给 Follower。
总结
ZAB 协议既不是强一致性,也不是若一致性,而是处于两者之间的单调一致性(顺序一致性)。它依靠事务 ID 和版本号,保证了数据的更新和读取是有序的。
Zookeeper 的应用场景
分布式锁
利用 Zookeeper 的临时顺序节点,可以轻松实现分布式锁。
服务注册和发现
利用 Znode 和 Watcher,可以实现分布式服务的注册和发现。
共享配置和状态信息
Redis 的分布式解决方案 Codis,就利用了 Zookeeper 来存放数据路由表和 codis-proxy 节点的元信息。同时 codis-config 发起的命令都会通过 Zookeeper 同步到各个存活的 codis-proxy。
此外, Kafka、HBase、Hadoop,也都依靠 Zookeeper 同步节点信息,实现高可用。
Zookeeper 分布式锁
什么是临时顺序节点
Zookeeper 的数据结构就像一棵树,这棵树由节点组成,这种节点叫做 Znode。
Znode 分为四种类型:
持久节点 Persistent
默认的节点类型。创建节点的客户端与 Zookeeper 断开连接后,该节点依旧存在。
持久节点顺序节点 Persistent_Sequential
所谓顺序节点,就是在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号。
临时节点 Ephemeral
与持久节点相反,当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。
临时顺序节点 Ephemeral_Sequential
临时顺序节点结合 临时节点和顺序节点的特点:在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。
Zookeeper 分布式锁的原理
获取锁
- 首先,在 Zookeeper 当中创建一个持久节点 ParentLock。当第一个客户端想要获得锁时,需要在 ParentLock 这个节点下面创建一个临时顺序节点 Lock1。
- 之后,Client1 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock1 是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
- 这时候,如果再有一个客户端 Client2 前来获取锁,则在 ParentLock 下再创建一个临时顺序节点 Lock2。
- Client2 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock2 是不是顺序最靠前的一个,结果发现 Lock2 并不是最小的。
- 于是,Client2 向排序仅比它靠前的节点 Lock1 注册 Watcher,用于监听 Lock1 节点是否存在。这意味着 Client2 抢锁失败,进入了等待状态。
- 这时候,如果又有一个客户端 Client3 前来获取锁,则在 ParentLock 下载再创建一个临时顺序节点 Lock3。
- Client3 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock3 是不是顺序最靠前的一个,结果同样发现节点 Lock3 并不是最小的。
- 于是,Client3 向排序仅比它靠前的节点 Lock2 注册 Watcher,用于监听 Lock2 节点是否存在。这意味着 Client3 同样抢锁失败,进入了等待状态。
- 这样一来,Client1 得到了锁,Client2 监听了 Lock1,Client3 监听了 Lock2。这恰恰形成了一个等待队列。
释放锁
释放锁的分为两种情况:
任务完成,客户端显示释放
当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。
任务执行过程中,客户端崩溃
获得锁的 Client1 在执行任务过程中,如果崩溃,则会断开与 Zookeeper 服务端的链接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除。
- 由于 Client2 一致监听着 Lock1 的存在状态,当 Lock1 节点被删除, Client2 会立即收到通知。
这时候Client2 会再次查询 ParentLock 下面的所有节点,确认自己创建的 Lock2 是不是目前最小的节点。如果是最小,则 Client2 顺理成章地获得了锁。
Zookeeper 和 Redis 分布式锁的比较
分布式锁 | 优点 | 缺点 |
---|---|---|
Zookeeper | 1. 有封装好的框架,容易实现。 2. 有等待锁的队列,大大提升抢锁效率。 |
添加和删除节点性能低。 |
Redis | Set 和 Del 指令的性能较高。 | 1. 实现复杂,需要考虑超时、原子性、误删等情形。 2. 没有等待锁的队列,只能在客户端自旋来等锁,效率低下。 |
本文整理自 (https://www.funtl.com/zh/apache-dubbo-zookeeper/)
以上是关于Zookeep 分布式锁的主要内容,如果未能解决你的问题,请参考以下文章