📖 开场:银行排队叫号
想象你去银行办业务 🏦:
没有叫号系统(抢锁):
大家挤在柜台前 👥👥👥
谁抢到谁办业务
↓
秩序混乱!😱
不公平(后来的可能先办)❌
有叫号系统(Zookeeper):
1. 小明进门 → 取号001 🎫
2. 小红进门 → 取号002 🎫
3. 小张进门 → 取号003 🎫
↓
叫号:001号请到1号窗口 📢
↓
小明办业务 ✅
↓
小明办完 → 叫号:002号请到1号窗口 📢
↓
小红办业务 ✅
这就是Zookeeper分布式锁的原理:排队叫号!
特点:
- 顺序号:按顺序分配
- 公平:先来先得
- 自动删除:离开自动取消号码
🤔 Zookeeper基础
Zookeeper是什么?
Zookeeper = 分布式协调服务
核心功能:
- 配置管理:统一配置中心
- 命名服务:分布式命名服务
- 分布式锁:分布式同步 ⭐
- 集群管理:选举、监控
Zookeeper的数据模型
类似文件系统的树形结构:
/
├── app
│ ├── config
│ └── locks ← ⭐ 分布式锁节点
│ ├── stock-lock
│ │ ├── _c_001 (临时顺序节点)
│ │ ├── _c_002 (临时顺序节点)
│ │ └── _c_003 (临时顺序节点)
│ └── order-lock
│ ├── _c_001
│ └── _c_002
└── service
├── server1
└── server2
Zookeeper的节点类型
| 节点类型 | 说明 | 特点 |
|---|---|---|
| 持久节点(PERSISTENT) | 创建后一直存在 | 需要手动删除 |
| 临时节点(EPHEMERAL) | 会话结束后自动删除 | ⭐ 分布式锁的关键 |
| 持久顺序节点(PERSISTENT_SEQUENTIAL) | 持久 + 自动编号 | 编号递增 |
| 临时顺序节点(EPHEMERAL_SEQUENTIAL) | 临时 + 自动编号 | ⭐⭐⭐ 分布式锁使用 |
Watch机制
Watch = 监听机制
Client注册Watch → 监听节点变化
↓
节点发生变化(删除、修改)
↓
Zookeeper通知Client 📢
↓
Client收到通知,采取行动
特点:
- 一次性触发(触发后失效)
- 异步通知
- 分布式锁的核心机制 ⭐
🎯 Zookeeper分布式锁原理
基本原理
锁目录:/locks/stock-lock
步骤1:Client1创建临时顺序节点
/locks/stock-lock/_c_0000000001
步骤2:Client2创建临时顺序节点
/locks/stock-lock/_c_0000000002
步骤3:Client3创建临时顺序节点
/locks/stock-lock/_c_0000000003
步骤4:各Client获取/locks/stock-lock下的所有子节点
[_c_0000000001, _c_0000000002, _c_0000000003]
步骤5:判断自己是否是序号最小的节点
- Client1:我是0000000001(最小)→ 获取锁成功 ✅
- Client2:我是0000000002(不是最小)→ 监听0000000001
- Client3:我是0000000003(不是最小)→ 监听0000000002
步骤6:Client1处理完业务,删除节点
/locks/stock-lock/_c_0000000001(删除)
步骤7:Client2收到通知(0000000001被删除)
重新判断 → 我是最小的了 → 获取锁成功 ✅
步骤8:Client2处理完业务,删除节点
/locks/stock-lock/_c_0000000002(删除)
步骤9:Client3收到通知
重新判断 → 我是最小的了 → 获取锁成功 ✅
为什么监听前一个节点?
错误方案:所有Client监听锁目录 ❌
/locks/stock-lock
├── _c_001 (Client1)
├── _c_002 (Client2) → 监听/locks/stock-lock
├── _c_003 (Client3) → 监听/locks/stock-lock
├── _c_004 (Client4) → 监听/locks/stock-lock
└── ... (100个Client都监听)
问题:
Client1删除节点 → 通知所有100个Client 📢📢📢
↓
100个Client同时醒来 → 竞争锁
↓
只有Client2获取成功,其他99个继续等待
↓
惊群效应(Herd Effect)!😱
正确方案:每个Client监听前一个节点 ✅
/locks/stock-lock
├── _c_001 (Client1)
├── _c_002 (Client2) → 监听_c_001
├── _c_003 (Client3) → 监听_c_002
├── _c_004 (Client4) → 监听_c_003
└── ...
优点:
Client1删除节点 → 只通知Client2 📢
↓
Client2获取锁
↓
Client2删除节点 → 只通知Client3 📢
↓
避免惊群效应!✅
🔧 Curator实现
Curator简介
Curator = Apache开源的Zookeeper客户端框架
优势:
- 封装了Zookeeper的复杂API
- 提供了分布式锁的实现
- 久经考验,生产级别
依赖配置
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.1</version>
</dependency>
配置Curator
@Configuration
public class ZookeeperConfig {
@Bean
public CuratorFramework curatorFramework() {
// ⭐ 重试策略:最多重试3次,每次间隔1秒
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
// ⭐ 创建Curator客户端
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("localhost:2181") // Zookeeper地址
.sessionTimeoutMs(5000) // 会话超时时间
.connectionTimeoutMs(5000) // 连接超时时间
.retryPolicy(retryPolicy)
.namespace("app") // 根节点(可选)
.build();
// ⭐ 启动客户端
client.start();
return client;
}
}
使用InterProcessMutex(可重入锁)
@Service
@Slf4j
public class OrderService {
@Autowired
private CuratorFramework curatorFramework;
/**
* 扣减库存
*/
public boolean deductStock(Long productId, int quantity) {
String lockPath = "/locks/stock/" + productId;
// ⭐ 创建可重入锁
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
try {
// ⭐ 获取锁(阻塞等待)
lock.acquire();
log.info("获取锁成功,开始扣减库存");
// ⭐ 业务逻辑
int stock = getStock(productId);
if (stock < quantity) {
log.warn("库存不足");
return false;
}
setStock(productId, stock - quantity);
log.info("扣减库存成功");
return true;
} catch (Exception e) {
log.error("处理失败", e);
return false;
} finally {
// ⭐ 释放锁
try {
lock.release();
log.info("释放锁成功");
} catch (Exception e) {
log.error("释放锁失败", e);
}
}
}
/**
* 扣减库存(带超时)
*/
public boolean deductStockWithTimeout(Long productId, int quantity) {
String lockPath = "/locks/stock/" + productId;
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
try {
// ⭐ 尝试获取锁(最多等待3秒)
if (!lock.acquire(3, TimeUnit.SECONDS)) {
log.warn("获取锁超时");
return false;
}
// 业务逻辑...
return true;
} catch (Exception e) {
log.error("处理失败", e);
return false;
} finally {
try {
lock.release();
} catch (Exception e) {
log.error("释放锁失败", e);
}
}
}
private int getStock(Long productId) {
// 从数据库读取库存
return 100;
}
private void setStock(Long productId, int stock) {
// 写入数据库
}
}
InterProcessMutex源码分析
public class InterProcessMutex implements InterProcessLock {
private final CuratorFramework client;
private final String basePath; // 锁的根路径,如 /locks/stock/1
/**
* ⭐ 获取锁
*/
@Override
public void acquire() throws Exception {
// 调用internalLock,阻塞等待
internalLock(-1, null);
}
/**
* ⭐ 尝试获取锁(带超时)
*/
@Override
public boolean acquire(long time, TimeUnit unit) throws Exception {
return internalLock(time, unit);
}
/**
* ⭐ 核心方法:获取锁
*/
private boolean internalLock(long time, TimeUnit unit) throws Exception {
Thread currentThread = Thread.currentThread();
// ⭐ 1. 检查是否可重入(同一线程)
LockData lockData = threadData.get(currentThread);
if (lockData != null) {
// 可重入,计数器+1
lockData.lockCount.incrementAndGet();
return true;
}
// ⭐ 2. 创建临时顺序节点
String ourPath = client.create()
.creatingParentsIfNeeded() // 创建父节点(如果不存在)
.withProtection() // 添加GUID前缀(防止网络问题导致重复创建)
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL) // 临时顺序节点
.forPath(basePath + "/lock-");
// ⭐ 3. 尝试获取锁
boolean hasTheLock = false;
boolean isDone = false;
while (!isDone) {
isDone = true;
try {
// ⭐ 4. 获取所有子节点并排序
List<String> children = getSortedChildren();
// ⭐ 5. 判断是否是序号最小的节点
String sequenceNodeName = ourPath.substring(basePath.length() + 1);
int ourIndex = children.indexOf(sequenceNodeName);
if (ourIndex == 0) {
// ⭐ 我是最小的,获取锁成功 ✅
hasTheLock = true;
} else {
// ⭐ 不是最小的,监听前一个节点
String previousNodeName = children.get(ourIndex - 1);
String previousNodePath = basePath + "/" + previousNodeName;
// ⭐ 6. Watch前一个节点
Stat stat = client.checkExists()
.usingWatcher(watcher) // 设置监听器
.forPath(previousNodePath);
if (stat != null) {
// 前一个节点还在,等待通知
if (time < 0) {
// 无限等待
wait();
} else {
// 超时等待
wait(unit.toMillis(time));
}
// 被唤醒,重新检查
isDone = false;
}
// 如果stat == null,说明前一个节点已删除,重新循环
}
} catch (KeeperException.NoNodeException e) {
// 节点不存在,重新尝试
isDone = false;
}
}
if (hasTheLock) {
// ⭐ 获取锁成功,记录线程信息
LockData newLockData = new LockData(currentThread, ourPath);
threadData.put(currentThread, newLockData);
}
return hasTheLock;
}
/**
* ⭐ 释放锁
*/
@Override
public void release() throws Exception {
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if (lockData == null) {
throw new IllegalMonitorStateException("当前线程未持有锁");
}
// ⭐ 可重入计数器-1
int newLockCount = lockData.lockCount.decrementAndGet();
if (newLockCount > 0) {
// 还有重入,不释放锁
return;
}
if (newLockCount < 0) {
throw new IllegalMonitorStateException("锁计数器异常");
}
// ⭐ 删除临时顺序节点(释放锁)
try {
client.delete().guaranteed().forPath(lockData.lockPath);
} finally {
threadData.remove(currentThread);
}
}
}
临时节点的自动删除
关键特性:客户端断开连接,临时节点自动删除 ⭐⭐⭐
场景:服务器宕机
Client1获取锁 → 创建临时节点 /locks/stock/1/_c_001
↓
Client1处理业务中...
↓
【Client1所在服务器宕机】💀
↓
Zookeeper检测到会话断开
↓
自动删除 /locks/stock/1/_c_001 ✅
↓
Client2收到通知 → 获取锁成功 ✅
优势:
- 不会出现死锁(Redis需要设置过期时间)
- 自动故障恢复
📊 Zookeeper vs Redis 分布式锁
| 特性 | Zookeeper | Redis |
|---|---|---|
| 可靠性 | ⭐⭐⭐ 非常高(CP) | ⭐⭐ 较高(AP) |
| 性能 | ⭐⭐ 中等(磁盘IO) | ⭐⭐⭐ 高(内存) |
| 实现复杂度 | ⭐⭐⭐ 较复杂 | ⭐⭐ 中等 |
| 公平性 | ✅ 公平锁(先来先得) | ❌ 非公平锁 |
| 自动删除 | ✅ 临时节点自动删除 | ⚠️ 需要设置过期时间 |
| 死锁风险 | ❌ 不会死锁 | ⚠️ 可能死锁(过期时间设置不当) |
| 主从切换 | ✅ 强一致性,不会丢锁 | ⚠️ 可能丢锁 |
| 阻塞等待 | ✅ Watch机制,不浪费CPU | ⚠️ 轮询或Pub/Sub |
| 运维成本 | ⭐⭐⭐ 高(需要Zookeeper集群) | ⭐⭐ 中(Redis常用) |
🎯 Zookeeper分布式锁的优势
1️⃣ 强一致性(CP)
Zookeeper的CAP选择:
- C:一致性(Consistency)
- P:分区容错性(Partition tolerance)
- 牺牲:A(可用性)
优势:
Client写入锁节点 → 必须写入大多数节点(Quorum)
↓
写入成功,返回 ✅
↓
保证所有Client看到的都是最新数据 ✅
对比Redis:
Redis主从架构:
Client写入Master → 异步复制到Slave
↓
Master宕机 → Slave升级为Master
↓
可能丢失还没复制的数据 ❌
2️⃣ 公平锁
Zookeeper:
Client按顺序获取锁
Client1(10:00:00)→ 001号 → 先获取 ✅
Client2(10:00:01)→ 002号 → 后获取 ✅
Client3(10:00:02)→ 003号 → 最后获取 ✅
公平:先来先得 ✅
Redis:
Client同时竞争锁
Client1(10:00:00)→ 竞争
Client2(10:00:01)→ 竞争
Client3(10:00:02)→ 竞争
↓
谁先SETNX成功谁获取锁
↓
可能Client3先获取 ⚠️(不公平)
3️⃣ 自动故障恢复
临时节点自动删除:
Client1获取锁 → 临时节点
↓
Client1宕机 💀
↓
会话断开 → 临时节点自动删除 ✅
↓
Client2自动获取锁 ✅
对比Redis:
Client1获取锁 → 设置过期时间10秒
↓
Client1宕机(没有释放锁)💀
↓
等待10秒 → 锁自动过期 ⏰
↓
Client2才能获取锁 ⚠️(延迟10秒)
4️⃣ Watch机制(不浪费CPU)
Zookeeper:
Client2监听前一个节点
↓
Client1删除节点 → Zookeeper通知Client2 📢
↓
Client2立即获取锁 ✅
不需要轮询,不浪费CPU ✅
Redis(自己实现):
while (true) {
if (tryLock()) {
break; // 获取成功
}
Thread.sleep(100); // 轮询,浪费CPU ❌
}
🎓 面试题速答
Q1: Zookeeper分布式锁的原理是什么?
A: 核心原理:临时顺序节点 + Watch机制
1. Client创建临时顺序节点(如 _c_001, _c_002)
2. 获取所有子节点并排序
3. 判断自己是否是序号最小的:
- 是 → 获取锁成功 ✅
- 否 → 监听前一个节点(Watch)
4. 前一个节点删除 → 收到通知 → 重新判断
5. 业务完成 → 删除自己的节点(释放锁)
比喻:银行排队叫号,先来先得,公平有序
Q2: 为什么要监听前一个节点而不是监听锁目录?
A: 避免惊群效应(Herd Effect)!
监听锁目录:
100个Client监听锁目录
↓
锁释放 → 通知100个Client 📢📢📢
↓
100个Client同时醒来竞争
↓
只有1个成功,99个继续等待
↓
浪费资源!❌
监听前一个节点:
Client1(001)→ Client2(002)→ Client3(003)
↓监听 ↓监听 ↓监听
001删除 → 只通知002 📢
↓
002获取锁 ✅
Q3: Zookeeper分布式锁如何避免死锁?
A: 临时节点自动删除!
Client获取锁 → 创建临时节点
↓
Client宕机/断开连接 💀
↓
Zookeeper检测到会话断开
↓
自动删除临时节点 ✅
↓
下一个Client自动获取锁 ✅
对比Redis:需要设置过期时间,如果时间设置不当可能死锁
Q4: Zookeeper分布式锁是公平锁还是非公平锁?
A: 公平锁!
Client按照创建节点的顺序获取锁
Client1(10:00:00)→ _c_001 → 先获取 ✅
Client2(10:00:01)→ _c_002 → 后获取 ✅
Client3(10:00:02)→ _c_003 → 最后获取 ✅
先来先得,绝对公平 ✅
对比Redis:非公平锁,谁先SETNX成功谁获取
Q5: Zookeeper分布式锁有什么缺点?
A: 三个主要缺点:
-
性能较差:
- 磁盘IO(Redis是内存)
- 吞吐量较低
-
实现复杂:
- 需要理解Zookeeper机制
- 代码比Redis复杂
-
运维成本高:
- 需要部署和维护Zookeeper集群
- 如果没有Zookeeper,需要额外引入
适用场景:
- 对可靠性要求极高(金融、交易)
- 需要公平锁
- 已有Zookeeper集群
Q6: 什么时候选择Zookeeper而不是Redis?
A: 三种场景:
-
对可靠性要求极高:
- 金融交易、支付
- 绝对不能容忍丢锁
-
需要公平锁:
- 按顺序处理任务
- 先来先得的场景
-
已有Zookeeper集群:
- 不需要额外引入中间件
- 充分利用现有资源
其他场景推荐Redis:
- 高并发、高性能场景(秒杀、抢购)
- 对可靠性要求不是极高
- 没有Zookeeper集群
🎬 总结
Zookeeper分布式锁完整流程
┌─────────────────────────────────────────┐
│ Client1: 创建 /locks/stock/1/_c_001 │
│ Client2: 创建 /locks/stock/1/_c_002 │
│ Client3: 创建 /locks/stock/1/_c_003 │
└─────────────┬───────────────────────────┘
│
↓ 判断序号
┌─────────────────────────────────────────┐
│ Client1: 001最小 → 获取锁 ✅ │
│ Client2: 002不是最小 → 监听001 │
│ Client3: 003不是最小 → 监听002 │
└─────────────┬───────────────────────────┘
│
↓ Client1业务完成
┌─────────────────────────────────────────┐
│ Client1: 删除001 → 通知Client2 📢 │
└─────────────┬───────────────────────────┘
│
↓ Client2醒来
┌─────────────────────────────────────────┐
│ Client2: 002最小 → 获取锁 ✅ │
│ Client3: 继续监听002 │
└─────────────────────────────────────────┘
公平 + 可靠 + 自动故障恢复 ✅
🎉 恭喜你!
你已经完全掌握了Zookeeper分布式锁的原理!🎊
核心要点:
- 临时顺序节点:自动编号,自动删除
- 监听前一个节点:避免惊群效应
- 公平锁:先来先得
- 强一致性:不会丢锁
- 自动故障恢复:临时节点自动删除
下次面试,这样回答:
"Zookeeper分布式锁基于临时顺序节点和Watch机制实现。
Client创建临时顺序节点,判断自己是否是序号最小的,是则获取锁,否则监听前一个节点。前一个节点删除时收到通知,重新判断。
它有三个主要优势:一是强一致性,不会像Redis主从切换时丢锁;二是公平锁,严格按照先来先得的顺序;三是自动故障恢复,Client宕机时临时节点自动删除,下一个Client自动获取锁。
但Zookeeper性能较Redis差,因为是磁盘IO。我们项目中秒杀系统使用Redis锁,金融交易系统使用Zookeeper锁,根据场景选择。"
面试官:👍 "很好!你对Zookeeper分布式锁理解很透彻!"
本文完 🎬
上一篇: 196-Redis实现分布式锁的细节和问题.md
下一篇: 198-分布式事务的解决方案.md
作者注:写完这篇,我都想去银行当叫号员了!🎫
如果这篇文章对你有帮助,请给我一个Star⭐!