Skip to content

Redis 中如何实现分布式锁? ​

  • 标签:后端、Redis

🏛 业务架构师 ​

Redis 分布式锁的核心命令是满足四个条件的 SET:

SET lock:order:1001 {唯一值} NX EX 30

NX 保证互斥(已有锁则失败)、EX 设过期防死锁、唯一值(UUID+线程 ID)防误删别人的锁、释放时用 Lua 脚本保证"判断是自己的锁 + 删除"两步原子执行。

但业务架构视角我更关注三条原则:

  • 锁粒度按业务对象设计:锁 lock:order:{orderId} 而不是全局锁。粒度越细并发越高,锁的是业务临界资源,不是代码段;
  • 锁不是业务正确性的全部:拿锁失败的重试策略(快速失败 vs 自旋等待)、锁内逻辑的执行时长上限(超时时间必须大于 P99 执行时间,否则锁过期被别人抢走、两个线程同时进临界区)——超时时间要按压测数据定,不是拍 30 秒;
  • 必须配兜底:即使加锁成功,关键写操作仍有唯一约束或乐观锁保底,锁是性能优化(挡住无效并发),不是正确性依赖。

生产实践我直接用 Redisson(看门狗自动续期、可重入、Lua 原子释放),自研锁是重复造轮子。最后一句大实话:先问自己能不能不用锁——数据库乐观锁、队列串行化往往更简单可靠,Redis 锁是选项之一,不是默认答案。

🏗 系统架构师 ​

单节点 Redis 分布式锁的核心是一条原子命令:

SET lock_key unique_value NX PX 30000
  • NX:不存在才设置,保证互斥;
  • PX 30000:过期时间,防止持有者宕机死锁;
  • unique_value:随机唯一标识(如 UUID),标识持有者。

释放必须原子:不能直接 DEL(可能删掉别人的锁),要用 Lua 脚本先比对 value 再删除:

lua
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else return 0 end

架构要点:

  • 锁过期但业务没执行完怎么办:看门狗续期(Redisson 默认 30 秒锁、每 10 秒续)或业务幂等兜底;
  • 主从切换丢锁问题:主写入锁后未同步到从即宕机,新主上锁消失——Redisson 红锁(RedLock)是官方方案,但争议大(时钟漂移、多数派假设),我的实践是多数场景用普通 Redisson 锁 + 业务幂等兜底,真正强互斥(钱)走 DB 唯一约束或 ZooKeeper/etcd 的共识锁;
  • 锁粒度尽量细(锁订单 ID 而不是锁整个表),锁等待要有超时和重试上限。

💻 全栈开发专家 ​

核心是一条命令:SET 的 NX + EX 必须原子(老代码 setnx + expire 两步是错的,中间宕机就是死锁):

bash
SET lock:order:1001 <唯一值> NX EX 10
# NX:不存在才设置;EX:10 秒过期;value 放 UUID+线程ID,标识持有者

释放锁必须校验是自己的锁再删,否则会把别人的锁误删,这要用 Lua 保证原子:

lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

生产实践我直接用 Redisson,它把这套全包了:

java
RLock lock = redisson.getLock("lock:order:" + orderId);
if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {   // 等待3s,持有30s(有看门狗续期)
    try { doBiz(); } finally { lock.unlock(); }
}

必须能答出的问题链:锁过期但业务没执行完怎么办(看门狗自动续期);主从切换锁丢失怎么办(RedLock 或接受概率、下沉 ZK);为什么删除要 Lua(判断+删除两步间锁可能易主)。能沿这条链讲到底,这题就过了。