Skip to content

分布式锁实现详解


单机锁(单 JVM):
synchronized / ReentrantLock
分布式锁(多 JVM):
ZooKeeper / Redis / 数据库

场景

  • 库存扣减
  • 唯一 ID 生成
  • 定时任务执行
  • 配置文件修改

ZooKeeper 结构:
/locks
└── /exclusive
└── lock-0000000001 # 获得锁的客户端
实现思路:
1. 所有客户端尝试创建 /locks/exclusive/lock 节点
2. 创建成功者获得锁
3. 其他客户端 Watch 该节点
4. 锁释放(节点删除)→ 重新竞争
public class ZooKeeperLock {
private static final String LOCK_PATH = "/locks/exclusive";
private final ZooKeeper zk;
private final String lockName;
private String currentLock;
public ZooKeeperLock(ZooKeeper zk, String lockName) {
this.zk = zk;
this.lockName = lockName;
}
// 获取锁
public boolean tryLock(long timeout) throws Exception {
// 创建临时顺序节点
currentLock = zk.create(
LOCK_PATH + "/" + lockName + "-",
"locked".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL
);
// 获取所有锁节点
List<String> locks = zk.getChildren(LOCK_PATH, false);
locks.sort(Comparator.naturalOrder());
// 如果自己是最小序号,获得锁
String nodeName = currentLock.substring(currentLock.lastIndexOf("/") + 1);
if (locks.get(0).equals(nodeName)) {
return true;
}
// 否则 Watch 前一个节点
int index = locks.indexOf(nodeName);
String prevLock = locks.get(index - 1);
CountDownLatch latch = new CountDownLatch(1);
// 异步监听前一个节点删除
zk.exists(LOCK_PATH + "/" + prevLock, event -> {
if (event.getType() == Event.EventType.NodeDeleted) {
latch.countDown();
}
});
// 等待锁释放或超时
return latch.await(timeout, TimeUnit.MILLISECONDS);
}
// 释放锁
public void unlock() throws Exception {
if (currentLock != null) {
zk.delete(currentLock, -1);
}
}
}
ZooKeeper 结构:
/locks/read-write
├── read-0000000001 # 读锁 1
├── read-0000000002 # 读锁 2
├── write-0000000003 # 写锁
└── read-0000000004 # 读锁 4
读锁规则:
- 读操作可以并发获取读锁
- 写锁需要等待所有读锁释放
写锁规则:
- 写锁是排他的
- 需要等待所有锁释放
public class ZooKeeperReadWriteLock {
private static final String LOCK_PATH = "/locks/read-write";
private final ZooKeeper zk;
private final String lockName;
private final boolean isRead;
private String currentLock;
public ZooKeeperReadWriteLock(ZooKeeper zk, String lockName, boolean isRead) {
this.zk = zk;
this.lockName = lockName;
this.isRead = isRead;
}
public boolean tryLock(long timeout) throws Exception {
// 创建锁节点
String lockType = isRead ? "read-" : "write-";
currentLock = zk.create(
LOCK_PATH + "/" + lockType + lockName + "-",
"locked".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL
);
// 获取所有锁节点
List<String> locks = zk.getChildren(LOCK_PATH, false);
locks.sort(Comparator.naturalOrder());
// 检查是否可以获取锁
return canAcquireLock(locks, timeout);
}
private boolean canAcquireLock(List<String> locks, long timeout)
throws Exception {
String nodeName = currentLock.substring(
currentLock.lastIndexOf("/") + 1
);
int index = locks.indexOf(nodeName);
if (isRead) {
// 读锁:前面没有写锁即可获取
for (int i = 0; i < index; i++) {
if (locks.get(i).startsWith("write-")) {
return watchNode(locks.get(i), timeout);
}
}
return true;
} else {
// 写锁:前面没有任何锁即可获取
if (index == 0) {
return true;
}
return watchNode(locks.get(index - 1), timeout);
}
}
private boolean watchNode(String prevNode, long timeout)
throws Exception {
CountDownLatch latch = new CountDownLatch(1);
zk.exists(LOCK_PATH + "/" + prevNode, event -> {
if (event.getType() == Event.EventType.NodeDeleted) {
latch.countDown();
}
});
return latch.await(timeout, TimeUnit.MILLISECONDS);
}
public void unlock() throws Exception {
if (currentLock != null) {
zk.delete(currentLock, -1);
}
}
}

问题:
所有等待锁的客户端都 Watch 同一个节点
锁释放时,所有客户端同时竞争 → 惊群现象
例如:1000 个客户端等待锁
锁释放 → 1000 个客户端同时收到通知
→ 1000 次网络请求
→ 999 次徒劳
优化前(羊群效应):
1000 客户端 Watch 同一个节点
锁释放 → 1000 次通知
优化后(链式 Watch):
Client1 Watch Client2
Client2 Watch Client3
...
锁释放 → 1 次通知 → 链式传递
// 只 Watch 序号最小的节点
public boolean tryLockOptimized(long timeout) throws Exception {
currentLock = zk.create(LOCK_PATH + "/lock-",
"locked".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
List<String> locks = zk.getChildren(LOCK_PATH, false);
locks.sort(Comparator.naturalOrder());
String nodeName = currentLock.substring(
currentLock.lastIndexOf("/") + 1
);
// 只 Watch 序号最小的节点
if (locks.get(0).equals(nodeName)) {
return true;
}
int index = locks.indexOf(nodeName);
// 只 Watch 前一个节点,而不是 Watch 最小节点
String prevNode = locks.get(index - 1);
CountDownLatch latch = new CountDownLatch(1);
zk.exists(LOCK_PATH + "/" + prevNode, event -> {
if (event.getType() == Event.EventType.NodeDeleted) {
latch.countDown();
}
});
return latch.await(timeout, TimeUnit.MILLISECONDS);
}

Redis 分布式锁的缺陷:

  • 单点问题:单机 Redis 不可靠
  • 主从切换:可能丢锁
假设有 N 个 Redis 实例(N >= 5)
1. 获取当前时间 T1
2. 依次尝试在 N 个实例获取锁
- 设置锁过期时间为 100ms
- 获取失败立即尝试下一个
3. 计算获取锁的耗时 T2 = T1 - now
4. 如果在 N/2 + 1 个实例上获取成功
且总耗时 < 锁过期时间,则成功
5. 否则,释放所有实例的锁
public class RedLock {
private List<Jedis> jedisInstances;
public boolean tryLock(String lockName, String value, long expireMs) {
int n = jedisInstances.size();
int successCount = 0;
long startTime = System.currentTimeMillis();
// 依次获取锁
for (Jedis jedis : jedisInstances) {
try {
String result = jedis.set(
lockName,
value,
"NX", "PX",
expireMs
);
if ("OK".equals(result)) {
successCount++;
}
} catch (Exception e) {
// 单个实例失败,继续尝试其他
}
}
// 检查是否获得多数锁
long elapsed = System.currentTimeMillis() - startTime;
if (successCount >= n / 2 + 1 && elapsed < expireMs) {
return true;
}
// 失败,释放所有锁
for (Jedis jedis : jedisInstances) {
try {
if (jedis.get(lockName).equals(value)) {
jedis.del(lockName);
}
} catch (Exception e) {
// 忽略
}
}
return false;
}
}

特性ZooKeeperRedis数据库
可靠性
性能
实现复杂度
羊群效应可优化存在存在
锁类型排他/读写排他排他
故障恢复自动需配置依赖 DB

Q1: ZooKeeper 分布式锁的原理是什么?

Section titled “Q1: ZooKeeper 分布式锁的原理是什么?”

参考答案

  1. 创建临时顺序节点
  2. 判断是否为最小序号
  3. 是 → 获得锁
  4. 否 → Watch 前一个节点
  5. 前一个节点删除 → 重新判断

参考答案

  1. 只 Watch 序号最小的节点
  2. 锁释放时只通知一个客户端
  3. 该客户端获取锁后再通知下一个

Q3: RedLock 是什么?有什么问题?

Section titled “Q3: RedLock 是什么?有什么问题?”

参考答案: RedLock 是 Redis 的分布式锁算法,通过在多个 Redis 实例上获取锁来提高可靠性。问题:

  • 假设时钟漂移可能导致锁失效
  • 需要维护多个 Redis 实例
  • 性能开销大