分布式锁的几种实现(Redis / ZK / DB)
一句话速记
Redis(推荐):SET key value NX PX ttl(原子 SETNX+过期),配合 Lua 脚本释放锁,Redisson 提供看门狗自动续期;ZooKeeper:创建临时顺序节点,最小节点持锁,前一节点删除则唤醒,天然公平且无脑裂;DB 乐观锁:WHERE version=old,低频场景可用,高并发下 CAS 重试开销大。互联网高并发首选 Redis,对锁的可靠性(公平、不丢锁)要求极高时选 ZooKeeper。
更详细的 Redis 分布式锁(Redisson 看门狗、SET NX PX、Lua 释放、Redlock 争议)见 Redis 分布式锁超时与 Redisson 看门狗
关键细节
1)Redis 分布式锁
// 加锁(原子操作)
String lockKey = "lock:order:" + orderId;
String lockValue = UUID.randomUUID().toString(); // 唯一值,防止误删他人锁
boolean locked = redis.set(lockKey, lockValue,
SetParams.setParams().nx().px(30_000)); // NX=不存在才设置,PX=毫秒过期
if (!locked) {
throw new BusinessException("系统繁忙,请稍后重试");
}
try {
// 执行业务
doBusinessLogic();
} finally {
// 释放锁(Lua 脚本保证原子)
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
redis.eval(script, List.of(lockKey), List.of(lockValue));
}问题:业务超时,锁自动过期,另一线程拿锁,两个线程同时执行
解决:Redisson 看门狗(自动续期)
加锁后,后台线程每 10s(ttl/3)检查锁是否还持有
持有 → 续期(延长 30s)
业务完成 → 主动释放(watchdog 停止)
JVM 宕机 → watchdog 停止 → 锁自然过期(30s 后释放)
三个核心问题:
问题 1:锁过期但业务未完成(上面的 Redisson 看门狗解决)
问题 2:Redis 主从切换丢锁(主宕机,从未同步锁数据)
→ Redlock 方案(多数 Redis 节点加锁)
→ 但 Redlock 在时钟漂移时也有问题(Martin Kleppmann 的批评)
→ 实际:单 Redis + Redisson 对大多数场景足够
问题 3:误删他人锁(A 的锁过期,B 拿锁,A 业务完成释放 B 的锁)
→ lockValue 用 UUID(每次加锁唯一)
→ 释放时 GET 验证 value 是否是自己的(Lua 原子 GET+DEL)
2)ZooKeeper 分布式锁
原理:
1. 每个竞争者在 /locks/order_123/ 下创建临时顺序节点
如:/locks/order_123/lock-0001, lock-0002, lock-0003
2. 获取所有子节点并排序,判断自己是否最小
3. 自己是最小 → 持锁,执行业务
不是最小 → 监听(watch)前一个节点(lock-0002 监听 lock-0001)
4. 前一节点删除(持锁者释放)→ ZK 通知 → 检查自己是否最小 → 持锁
5. 业务完成 → 删除自己的节点(临时节点,JVM 挂了 Session 超时也自动删除)
特点:
✅ 公平锁(先来先得,顺序保证)
✅ 无脑裂(ZooKeeper 本身是 CP 系统)
✅ 临时节点:宕机自动释放(Session 超时)
❌ 性能比 Redis 差(ZK 写操作要求多数节点确认)
❌ 运维复杂(ZK 集群维护成本高)
❌ 频繁的 watch 通知开销大(集群内部通信)
代码(Curator 框架封装):
CuratorFramework client = CuratorFrameworkFactory.newClient(zkAddress, retryPolicy);
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order-" + orderId);
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
// 持锁,执行业务
doBusinessLogic();
}
} finally {
lock.release();
}3)数据库分布式锁
方式 1:唯一键(悲观锁式):
-- 加锁(INSERT 唯一键,失败则已有锁)
INSERT INTO distributed_lock (lock_key, holder, expire_time)
VALUES ('order:123', 'server-A', NOW() + INTERVAL 30 SECOND);
-- 唯一键约束:重复 INSERT 失败 → 锁已被持有
-- 释放锁
DELETE FROM distributed_lock WHERE lock_key='order:123' AND holder='server-A';
-- 问题:宕机后锁不自动释放 → 需要定时清理过期锁
-- 清理:DELETE WHERE expire_time < NOW()方式 2:SELECT FOR UPDATE(行锁):
BEGIN;
SELECT * FROM distributed_lock WHERE lock_key='order:123' FOR UPDATE;
-- 拿到锁,执行业务
UPDATE distributed_lock SET holder='server-A', expire_time=... WHERE lock_key='order:123';
COMMIT;
-- 问题:需要提前插入记录,数据库连接占用,高并发下连接池耗尽
-- 适用:低并发(几十 QPS),无专用 Redis 的场景4)三种方案对比
方案 性能 可靠性 公平性 适用场景
─────────────────────────────────────────────────────────
Redis 极高 中等 否 高并发互联网业务(推荐)
ZooKeeper 中等 高 是 公平锁/一致性要求高的场景
数据库 低 低 否 低并发/已有 DB/无 Redis
延伸追问
- Q:Redlock 用不用? → Redlock(多节点 Redis 多数派加锁)设计初衷是解决单点 Redis 主从切换的锁丢失问题。但 Martin Kleppmann 指出其在时钟漂移下仍然不安全(进程暂停时锁会过期)。工程实践:大多数场景不需要 Redlock(单 Redis + Redisson 看门狗够用),对安全性极高要求(金融核心)用 ZooKeeper 或数据库悲观锁。
- Q:Redis 分布式锁和本地 synchronized 什么区别? → synchronized 只在单 JVM 内有效,多实例部署时不同实例的 synchronized 各自独立,无法互斥。Redis 分布式锁在 Redis 中持有,所有实例共享同一把锁,可以跨 JVM 互斥。代价:网络 RTT(通常 1-5ms),比本地锁慢 1000x 以上,只在真正需要跨实例互斥时使用。
我的记法
- Redis:SET NX PX(加锁)+ Lua GET+DEL(释放)+ Redisson 看门狗(续期)
- ZooKeeper:临时顺序节点 + watch 前一个,公平但慢
- DB:INSERT 唯一键/SELECT FOR UPDATE,低并发备用方案
- 性能:Redis > ZK > DB;可靠性:ZK ≥ Redis > DB
- 一句话:「互联网用 Redis+Redisson,公平锁用 ZK,别用 DB」
状态
- 已背速记
- 能写 Redis 加锁/Lua 释放代码
- 能解释看门狗的工作原理