分布式锁是协调多节点并发访问共享资源的基础设施。从简单的 Redis SETNX 到工业级的 Redisson、Curator,每种方案都在一致性、可用性与性能间做着不同的取舍。
1. 分布式锁核心要求
| 特性 | 说明 |
|---|---|
| 互斥性 | 任意时刻只有一个客户端持有锁 |
| 防死锁 | 客户端崩溃后锁自动释放(TTL / Session) |
| 可重入 | 同一线程可重复获取同一锁 |
| 容错性 | 部分节点故障不影响整体 |
| 锁续期 | 业务未完成时自动延长持有时间 |
2. Redis 分布式锁演进
2.1 阶段一:SETNX + EXPIRE(初级)
// 问题:SETNX 与 EXPIRE 非原子,中间崩溃导致死锁
Boolean acquired = jedis.setnx(lockKey, clientId);
if (acquired) jedis.expire(lockKey, 30); // 非原子!
2.2 阶段二:SET key value NX PX(改进)
SET lock:order:123 my-client-id NX PX 30000
// Lua 释放锁(原子性检查 + 删除)
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
jedis.eval(lua, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
2.3 阶段三:RedLock(Redis 作者提出)
假设 5 个独立的 Redis Master 节点(无主从,无 Sentinel)
1. 记录当前时间 T1
2. 依次向 5 个节点请求加锁,超时时间设为锁 TTL 的很小比例(如 10ms)
3. 计算总耗时 = 当前时间 T2 - T1
4. 若成功加锁的节点数 ≥ N/2 + 1(即 3 个),且总耗时 < TTL
5. 则加锁成功,有效时间 = TTL - 总耗时
6. 若失败,向所有节点发送解锁请求
争议:Redisson 作者认为 RedLock 在时钟跳跃、网络分区下仍不完美;推荐直接使用 Redlock-based 实现即可。
2.4 Redisson:工业级实现
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://node1:7000", "redis://node2:7000");
RedissonClient redisson = Redisson.create(config);
// 可重入锁 + 看门狗自动续期
RLock lock = redisson.getLock("myLock");
try {
// 尝试加锁,最多等待 10 秒,锁 30 秒后自动释放
// 看门狗:持有锁的线程存活时,每 10 秒续期 TTL 到 30 秒
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
// 执行业务
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
看门狗原理:
加锁成功 → 启动 Watchdog 线程(Netty HashedWheelTimer)
│
├─ 每 lockWatchdogTimeout / 3 续期一次(默认 10s)
│ └─ 延长锁 TTL = lockWatchdogTimeout(默认 30s)
│
└─ 业务线程结束 / 解锁 → Watchdog 取消
公平锁:
RLock fairLock = redisson.getFairLock("fairLock");
// 按请求锁的先后顺序获取,避免饥饿
读写锁:
RReadWriteLock rwLock = redisson.getReadWriteLock("myLock");
RLock readLock = rwLock.readLock(); // 共享
RLock writeLock = rwLock.writeLock(); // 排他
Redisson 锁类型矩阵:
| 类型 | 特点 |
|---|---|
RLock | 可重入非公平锁 |
RSemaphore | 信号量,限并发数 |
RCountDownLatch | 计数等待 |
RLock fair | FIFO 公平锁 |
3. ZooKeeper 分布式锁
3.1 ZK 的优势
- 强一致性:ZAB 协议保证写入顺序一致
- 临时节点:会话断开自动删除,天然防死锁
- Watcher 通知:事件驱动,无需阻塞轮询
3.2 基于临时顺序节点的锁
/locks/order
├── lock-0000000001 ← 最小序号,获取锁
├── lock-0000000002 ← 监听前一个节点 lock-0001
└── lock-0000000003 ← 监听前一个节点 lock-0002
3.3 Curator 框架实现
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("zk1:2181,zk2:2181", retryPolicy);
client.start();
// 可重入互斥锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.release();
}
Curator 锁对比:
| 实现类 | 特点 |
|---|---|
InterProcessMutex | 可重入互斥锁 |
InterProcessSemaphoreMutex | 不可重入互斥锁 |
InterProcessReadWriteLock | 读写锁 |
InterProcessMultiLock | 多锁原子获取 |
3.4 羊群效应 vs 监听链优化
| 方案 | 行为 | 风险 |
|---|---|---|
| 监听父节点 | 所有等待者收到通知,同时竞争 | 羊群效应 |
| 监听前一个节点 | 只有紧邻的下一个收到通知 | 无羊群效应,但监听链过长增加 ZK 负担 |
Curator 使用 监听前一个节点 方案。
4. Etcd 分布式锁
4.1 Etcd 优势
- Raft 共识:强一致性
- TTL + Lease:租约机制,自动过期
- Revision / Event:全局递增版本号,天然 FIFO
4.2 基于 Etcd 的锁实现(jetcd)
Client etcdClient = Client.builder().endpoints("http://etcd:2379").build();
KV kvClient = etcdClient.getKVClient();
Lease leaseClient = etcdClient.getLeaseClient();
// 创建租约(10 秒 TTL)
LeaseGrantResponse lease = leaseClient.grant(10).get();
long leaseId = lease.getID();
// 原子写入:key = lock:resource, value = node-id, 绑定租约
ByteSequence key = ByteSequence.from("lock:order:123", StandardCharsets.UTF_8);
ByteSequence value = ByteSequence.from("node-1", StandardCharsets.UTF_8);
PutOption putOpt = PutOption.newBuilder().withLeaseId(leaseId).build();
kvClient.put(key, value, putOpt).get();
// 租约续期(心跳)
ew KeepAliveListener keepAlive = leaseClient.keepAlive(leaseId);
// 释放:删除 key(原子)
kvClient.delete(key).get();
4.3 Etcd 分布式锁的租约续期
// 持续续期直到业务完成
Executors.newSingleThreadExecutor().submit(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
leaseClient.keepAliveOnce(leaseId).get();
Thread.sleep(5000);
}
} catch (Exception e) {
// 续期失败,锁可能已释放
}
});
5. 三种方案对比
| 维度 | Redis (Redisson) | ZooKeeper (Curator) | Etcd (jetcd) |
|---|---|---|---|
| 一致性 | 最终一致(主从异步) | 强一致(ZAB) | 强一致(Raft) |
| 可用性 | AP(主从可切换) | CP(过半存活) | CP(过半存活) |
| 性能 | 极高(单节点 10K+ QPS) | 中等(写需 leader) | 中等 |
| 死锁防护 | TTL + 看门狗 | 临时节点 + 会话 | Lease TTL |
| 可重入 | ✅ | ✅ | 自行实现 |
| 公平锁 | ✅ | ✅ | 自行实现 |
| 适用场景 | 高并发、短期锁 | 协调服务、长期锁 | K8s 生态、服务发现 |
6. 锁的最佳实践
6.1 锁粒度控制
// ❌ 粗粒度:锁住整个表
lock.lock("products");
// ✅ 细粒度:按业务维度拆分
lock.lock("product:" + productId);
lock.lock("inventory:" + sku);
6.2 锁 + 业务事务
// 错误:锁在事务之外
lock.lock(); // 锁 A
transactionService.execute(); // 事务开始 → 锁可能已超时释放!
lock.unlock();
// 正确:锁在事务内部(存储过程 / AOP 环绕)
transactionService.execute(() -> {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
});
6.3 防御性编程
try {
if (!lock.tryLock(5, TimeUnit.SECONDS)) {
throw new LockAcquisitionException("获取锁超时");
}
// 双重检查
if (alreadyProcessed(orderId)) return;
processOrder();
} catch (Exception e) {
log.error("处理异常", e);
throw e;
} finally {
// 只释放自己持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
延伸阅读
- Java 高并发编程精要 — JUC 本地锁与 CAS
- Java 缓存策略与 Redis 集成 — Redisson 分布式对象
- 系统架构设计 — 限流与并发控制架构
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。