分布式锁的几种实现(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 释放代码
  • 能解释看门狗的工作原理