前言:Zookeeper 是一个开源的分布式协调服务,可以用于实现分布式锁。本文将介绍如何使用 Zookeeper 实现分布式锁。
简介
- 本地锁:比如在单机服务A JVM 上使用 Synchronized 关键字来对一个共享资源上锁,但是这个 Synchronized 只能保证这台机器的锁,如果是集群模式,服务A也在另一台机器上运行呢?就无法保证同一时间只有一个线程访问到共享资源了。
- 分布式锁:在分布式系统中,为了保证数据的一致性和安全性,需要使用分布式锁来协调各个节点的访问。
Zookeeper 实现分布式锁
Zookeeper 实现分布式锁有两种方式,一种是排他锁(X锁),一种是共享锁(S锁)。
排他锁实现
排他锁 Exclusive Locks,简称 X 锁,又称写锁/独占锁,如果事务 A 对某个对象加了排他锁,其他事务则不可对该对象增加 X 锁或者 S 锁,直到事务 A 释放了该锁。
- 在 Zookeeper 中,先创建一个节点,假设是
/exclusive_lock
,然后多个客户端就可以在这个节点下创建一个规定命名的临时节点,假设是 lock,所有客户端只有一个客户端能创建成功,创建成功即获得锁。

- 同时,没有获得锁的客户端就在
/exclusive_lock
上注册一个子节点变更的 Watcher 监听,以便实时监听 lock 节点被释放(删除)的情况。 - 一般来说会有两种情况:一是客户端正常执行完逻辑后,删除节点 lock。二是客户端宕机了,因为 lock 是临时节点,也会被删除。无论如何锁都会被释放,Zookeeper 就会通知所有在
/exclusive_lock
注册监听的客户端,他们收到通知后,就可以重新开始对锁进行抢占,从步骤 1 开始。
共享锁实现
共享锁 Shared Lock,简称 S 锁,又称读锁。根据操作系统基本知识,如果一个对象上有 S 锁,那其他事务可以继续加 S 锁,但不能加 X 锁。并发性比较好。
- 和排他锁一样,先创建一个节点,假设是
/shared_lock
,然后我们为了方便,在锁节点名称上就能看出是哪个客户端,是 X 锁还是 S 锁,我们将锁节点的命名设置为host-W/R-序号
的形式,这里可以看出是想创建一个顺序节点,为什么要加上序号呢?为了方便我们判断 W 锁和 S 锁的顺序,往下看就知道了。 - 所有客户端根据自身需要,到
/shared_lock
节点下创建临时顺序节点,如:

-
创建完节点后,需要通过 Zookeeper 来判断读写顺序:
- (1)客户端创建完节点,获取
/shared_lock
节点下是所有子节点,并对/shared_lock
的变更注册监听。 - (2)确定自己的节点的顺序
- 对于读请求,如果比自己小的节点都是读节点,可以直接读。如果比自己小的有写节点,不可读。
- 对于写请求,若自己不是序号最小的节点,则等待。
- (3)接受 Watcher 事件通知,重复(1)的步骤。
- (1)客户端创建完节点,获取
-
释放锁,和排他锁的释放流程一样。
羊群效应
上面虽然可以实现共享锁的功能,但是仔细想想,有没有问题?
机智的同学可能想到了,监听通知事件有许多个客户端做了无用功。假设现在的客户端情况如下:

可以看到,在 host1 移除之后,其他所有客户端都会收到通知事件,但其实有影响的只是 host2,能进行加 X 锁写操作。其他的客户端被唤醒后还是要继续等待监听。这就是所谓的羊群效应。
羊群效应会给 Zookeeper 服务器造成巨大的性能影响和网络开销!!!
那上面的共享锁有什么改进办法吗?有,更改几个关键步骤:
- 客户端创建节点后不要在父节点上注册监听了。
- 对于读请求,向比自己序号小的最后一个 W 节点注册 Watcher 监听(exist监听)。
- 对于写请求,向比自己序号小的最后一个节点,无论 R 节点还是 W 节点,注册 Watcher 监听(exist监听)。
这样更改就可以避免羊群效应了(不会通知全部客户端了)。
Zookeeper 和 Redis 分布式锁的区别及应用场景
- 性能不同
- Redis 分布式锁是直接用 key 去 set value,内存中,速度快,性能好。
- Zookeeper 需要在一个目录结构树下创建一个节点。性能不好。
- 锁的释放时机不同
- Reids 分布式锁释放时机:1 执行完成,2 超时释放。
- Zookeeper 分布式锁释放时机:1 执行完成,2 客户端宕机。
- 使用场景不同
- Redis 适合单机,使用简单方便,使用 Redis 集群有可能在主节点获取锁后宕机,又没同步到子节点。
- 如果需要集群的分布式锁则使用 Zookeeper,可以保证强一致性。
总的来说,Zookeeper 分布式锁适合集群,适合需要强一致性的环境。