分布式微服务--ZooKeeper作为分布式锁
看这篇博客之前可以先去了解博主的另一篇讲解ZooKeeper的博客:分布式微服务--ZooKeeper的客户端常用命令 & Java API 操作-CSDN博客
1. 为什么需要分布式锁?
在分布式系统中,多个服务节点可能同时访问或修改同一份共享资源(例如数据库记录、缓存数据、文件等)。
如果不加限制,容易出现数据不一致或竞争条件。
因此,需要一种机制来保证:同一时刻只有一个节点能访问某资源。这就是分布式锁的意义。常见分布式锁实现方式:
基于 Redis
基于 ZooKeeper
基于 数据库
其中 ZooKeeper 实现分布式锁更安全可靠,因为它的核心是 强一致性 + 临时顺序节点机制。
2. ZooKeeper 的分布式锁原理
ZooKeeper 提供的 临时节点(Ephemeral) 和 有序节点(Sequential) 是实现分布式锁的关键。
核心思想:
创建锁节点:
所有客户端到某个固定路径(如/lock
)下创建一个 临时顺序节点,例如:/lock/lock_00000001 /lock/lock_00000002 /lock/lock_00000003
临时节点保证客户端宕机或断开时自动删除。
顺序节点保证多个客户端的排队顺序。
获取锁:
客户端判断自己创建的节点是否是 序号最小的节点。
如果是 → 获取锁成功。
如果不是 → 监听比自己小的前一个节点,等待其释放。
释放锁:
客户端执行完业务逻辑后,删除自己的节点 → 唤醒下一个等待者。
3. 分布式锁实现步骤
假设我们要对某个共享资源
order
加锁。1)获取锁
客户端在
/lock/order
下创建临时顺序节点,例如:
/lock/order/lock_00000010
获取
/lock/order
下所有子节点,并排序。如果自己是最小的节点 → 获得锁。
否则 → 监听自己前一个节点。
2)释放锁
客户端完成业务逻辑后,删除自己的节点。
ZooKeeper 会通知下一个等待者。
3)异常情况
如果客户端宕机或与 ZooKeeper 断开连接,ZooKeeper 会自动删除临时节点,从而避免锁“死锁”。
4. ZooKeeper 分布式锁代码示例
基于 Curator(推荐)
Curator 是 ZooKeeper 的一个高级客户端,封装了分布式锁。
![]()
样例代码(本次使用的锁是InterProcessMutex):
xml
<!--curator--><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>4.0.0</version></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>4.0.0</version></dependency>
代码
/*** 模拟12306售票系统 —— 使用 Zookeeper 分布式锁保证并发安全*/ public class Ticket12306 implements Runnable{// 模拟数据库中的票数private int tickets = 10;// 分布式可重入锁对象(Curator 提供)private InterProcessMutex lock ;public Ticket12306(){// 1. 定义重试策略:初始等待时间 3 秒,最多重试 10 次RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);// 2. 通过 builder 模式创建 CuratorFramework 客户端CuratorFramework client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181") // Zookeeper 连接地址.sessionTimeoutMs(60 * 1000) // 会话超时时间.connectionTimeoutMs(15 * 1000) // 连接超时时间.retryPolicy(retryPolicy) // 指定重试策略.build();// 3. 开启连接client.start();// 4. 创建分布式锁对象,指定锁的路径(ZK 节点)// 不同客户端只要路径一样,就能实现互斥lock = new InterProcessMutex(client,"/lock");}@Overridepublic void run() {while(true){try {// 1. 尝试获取锁,最多等待 3 秒lock.acquire(3, TimeUnit.SECONDS);// 2. 拿到锁后执行业务逻辑 —— 卖票if(tickets > 0){System.out.println(Thread.currentThread()+":" + tickets);Thread.sleep(100); // 模拟业务处理耗时tickets--; // 卖出一张票}} catch (Exception e) {e.printStackTrace();} finally {// 3. 无论如何,最后都要释放锁,避免死锁try {lock.release();} catch (Exception e) {e.printStackTrace();}}}} }
测试:
/*** 测试类:模拟多个客户端同时抢票*/ public class LockTest {public static void main(String[] args) {// 创建一个 Ticket12306 对象(共享 10 张票)Ticket12306 ticket12306 = new Ticket12306();// 创建两个线程,模拟两个不同的售票平台(携程、飞猪)Thread t1 = new Thread(ticket12306,"携程");Thread t2 = new Thread(ticket12306,"飞猪");// 启动线程,同时卖票t1.start();t2.start();} }
✅ 总结:
InterProcessMutex
是 Curator 提供的 可重入分布式锁,底层用 Zookeeper 的临时顺序节点实现。
lock.acquire()
获取锁,获取不到会阻塞(或超时返回false
)。
lock.release()
释放锁,必须写在finally
,避免异常导致死锁。这种方式可以模拟 多进程/多机器的并发安全,保证同一时刻只有一个客户端在修改票数。
5. ZooKeeper 分布式锁的优缺点
✅ 优点
强一致性:ZK 的数据一致性保证了锁的可靠性。
自动释放:客户端宕机,临时节点会自动删除,避免死锁。
公平性:顺序节点机制,保证先来先服务。
❌ 缺点
性能较低:每次加锁/解锁都需要与 ZK 通信,适合低并发场景。
部署复杂:需要搭建 ZooKeeper 集群。
羊群效应:节点删除时可能触发大量客户端通知(不过监听前一个节点可以缓解)。
6. 使用场景
订单系统(防止超卖)
分布式定时任务(同一时间只允许一个节点执行)
共享资源访问控制(文件、缓存等)
7. 总结
ZooKeeper 分布式锁依赖 临时顺序节点 + watcher 机制。
Curator 封装了常用的锁(
InterProcessMutex
),开发更方便。适合一致性要求高、并发量不是极高的业务场景。
高并发下更推荐 Redis 分布式锁(性能更好,但需要妥善解决可靠性问题)。