分布式锁的实现二(转载+整理)

前言

在上一篇博客中,我们可以用redis来实现分布式锁,但是仔细观察下面的伪代码,你会发现redis的竞争锁的机制太低效了,因为我们是通过自旋的方式去获得锁,而且实现比较复杂,需要考虑原子性、误删的问题。所以这篇我将介绍如何使用Zookeeper来实现分布式锁。

1
2
3
4
5
6
String threadId = Thread.currentThread().getId()
while(true){//通过自旋的方式去竞争锁,低效浪费cpu
if (jredis.set(key,threadId,30,nx)==1)
break;
}
逻辑处理...

Zookeeper的数据结构

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。Znode的四种类型:

1.持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与Zookeeper断开连接后,该节点依旧存在 。

2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

3.临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除:

4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除。

Watcher机制

要想避免出现redis低效竞争锁的方式,就需要有类似通知的机制,比如说A释放锁之后,B可以监测到A释放的信息,而不是通过自旋的方式去获取锁。恰好Zookeeper提供了Watcher,这让Zookeeper在实现分布式锁上更加高效。

Zookeeper实现分布式锁

Zookeeper分布式锁恰恰应用了临时顺序节点,先来看获取锁的详细步骤。

获取锁:

首先需要在Zookeeper当中创建一个持久节点ParentLock,假设有两个客服端线程Client1,Client2同时对一块临界区中的代码进行操作,Client1会先在ParentLock下创建一个临时顺序节点Lock1。

然后获取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同样抢锁失败,进入了等待状态。

你想输入的替代文字

释放锁:
释放锁分种情况正常释放和异常释放。
1.任务完成,客户端显示释放

当任务完成时,Client1会显示调用删除节点Lock1的指令。

你想输入的替代文字

2.任务执行过程中,客户端崩溃

获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。

由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。

同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。

Zookeeper和Redis分布式锁的比较

下面的表格总结了Zookeeper和Redis分布式锁的优缺点:

你想输入的替代文字

以上是基于程序员小灰的文章的转载和整理。