一、关于zookeeper
我们知道zookeeper主要服务于分布式系统,它为我们提供了以下几种服务:统一配置管理、统一命名服务、集群管理、分布式锁。本文我们主要来谈谈分布式锁的部分。
zookeeper和redis一样,都是C/S架构,且zookeeper的数据结构和Unix十分类似,可以看做是一颗树,树上的节点称为ZNode
,每个节点可以通过路径进行标识,其中根节点为/
。
ZNode
节点分为四类:
- 持久节点:客户端断开连接后仍然存在。
- 持久顺序节点:带编号的持久节点。
- 临时节点:客户端断开连接后节点会消失。
- 临时顺序节点:带编号的临时节点。
在zk中,同一路径下的同名节点只能有一个,可以通过监听器watcher监听ZNode
的数据变化和子节点的增减变化。zk实现分布式锁就用到了以上两点,接下来我们看看具体是如何实现的。
二、zookeeper实现分布式锁
1.通过持久节点实现(不可行)
和上篇一样,我们仍然以秒杀场景下的扣减库存为例。
假设现在库存为1,有主机A和主机B同时要进行扣减库存的操作。我们可以通过zk中同一路径下同名节点的唯一性,来实现锁的效果,具体实现如下:
在主机A、B在扣减库存之前,先让其去创建一个同名节点。谁先创建了这个节点,就代表谁获得了锁,此时另一个主机再去创建该节点就会报错。假设A创建了该节点,名为lock,那么此时B就会被阻塞,并监听该节点的删除事件,直到A删除该节点(也就是释放锁)之后,才能进行下一步操作(获取锁,然后扣减库存)。
但是这样有一个很明显的问题,如果A获取锁之后宕机了怎么办?那么A将永远不会释放锁,从而造成死锁!假如我们现在有很多台服务器同时监听这个节点,那么当其释放的时候,所有服务器都会收到通知,但最终只有一个能获得锁,这会占用网络带宽和服务资源。
为了解决以上问题,往往采用第二种方案。
2.通过临时顺序节点实现(推荐)
将持久节点换为临时节点,这样保证了就算宕机,节点也能够得到释放,不至于死锁。并且因为是顺序的,我们可以让每个服务器只监听他的前一个服务器创建的节点,所以释放锁的时候也就是一个一个的释放下去了。
每个节点都是有一个编号的,所以我们不需要再通过同名节点的唯一性来实现锁,具体实现如下:
- 当服务器1获取锁时,会创建一个临时顺序节点lock1(假设该节点编号为1),然后查找根节点下所有的临时顺序节点并对其进行排序,如果自己创建的节点是所有临时顺序节点中编号最小的节点,那么成功获取锁;
- 此时服务器2也要获取锁,于是创建了一个lock2(编号为2)的节点,同样查找所有节点并排序,此时临时顺序节点有lock1和lock2,而lock1是最小的节点,所以服务器2会进入等待状态,并注册watcher监听lock1是否删除;
- 这样,每来一个要获取锁的就按照这样的规则创建、监听节点,从而实现分布式锁。
三、存在的缺点
从上文我们知道,zk是通过创建、删除节点来实现分布式锁的。
这就意味着zk的性能上没有redis那么高,因为每次获取锁和释放锁都需要动态的对节点进行操作。并且在集群中,zk创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
由于创建的是临时节点,在网络抖动导致连接断开时,zk服务器会以为客户端挂掉了,就会删除临时节点,这时候其他客户端就可以获取到锁了。不过这不是很常见,因为zk有心跳检测重试机制,在多次重试不行才会删除临时节点。