分布式锁实现详解
分布式锁实现详解
Section titled “分布式锁实现详解”分布式锁概述
Section titled “分布式锁概述”为什么需要分布式锁?
Section titled “为什么需要分布式锁?”单机锁(单 JVM): synchronized / ReentrantLock
分布式锁(多 JVM): ZooKeeper / Redis / 数据库场景:
- 库存扣减
- 唯一 ID 生成
- 定时任务执行
- 配置文件修改
基于 ZooKeeper 的分布式锁
Section titled “基于 ZooKeeper 的分布式锁”1. 排他锁(Exclusive Lock)
Section titled “1. 排他锁(Exclusive Lock)”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); } }}2. 读写锁(Read/Write Lock)
Section titled “2. 读写锁(Read/Write Lock)”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); } }}羊群效应优化
Section titled “羊群效应优化”什么是羊群效应?
Section titled “什么是羊群效应?”问题: 所有等待锁的客户端都 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);}RedLock 算法
Section titled “RedLock 算法”Redis 分布式锁的缺陷:
- 单点问题:单机 Redis 不可靠
- 主从切换:可能丢锁
RedLock 原理
Section titled “RedLock 原理”假设有 N 个 Redis 实例(N >= 5)
1. 获取当前时间 T12. 依次尝试在 N 个实例获取锁 - 设置锁过期时间为 100ms - 获取失败立即尝试下一个3. 计算获取锁的耗时 T2 = T1 - now4. 如果在 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; }}分布式锁对比
Section titled “分布式锁对比”| 特性 | ZooKeeper | Redis | 数据库 |
|---|---|---|---|
| 可靠性 | 高 | 中 | 低 |
| 性能 | 中 | 高 | 低 |
| 实现复杂度 | 中 | 低 | 低 |
| 羊群效应 | 可优化 | 存在 | 存在 |
| 锁类型 | 排他/读写 | 排他 | 排他 |
| 故障恢复 | 自动 | 需配置 | 依赖 DB |
面试高频问题
Section titled “面试高频问题”Q1: ZooKeeper 分布式锁的原理是什么?
Section titled “Q1: ZooKeeper 分布式锁的原理是什么?”参考答案:
- 创建临时顺序节点
- 判断是否为最小序号
- 是 → 获得锁
- 否 → Watch 前一个节点
- 前一个节点删除 → 重新判断
Q2: 如何解决羊群效应?
Section titled “Q2: 如何解决羊群效应?”参考答案:
- 只 Watch 序号最小的节点
- 锁释放时只通知一个客户端
- 该客户端获取锁后再通知下一个
Q3: RedLock 是什么?有什么问题?
Section titled “Q3: RedLock 是什么?有什么问题?”参考答案: RedLock 是 Redis 的分布式锁算法,通过在多个 Redis 实例上获取锁来提高可靠性。问题:
- 假设时钟漂移可能导致锁失效
- 需要维护多个 Redis 实例
- 性能开销大