【深度复盘】Redis 分布式锁:从 SETNX 到 Redisson 看门狗的架构权衡
标签: Redis 分布式锁 Redisson 并发编程 架构设计
复盘时间: 2026-01-25
一、 为什么需要分布式锁?(演进之路)
在单机多线程环境下,我们使用 synchronized 或 ReentrantLock;但在微服务/集群环境下,本地锁无法锁住其他 JVM 进程,因此需要引入第三方组件(Redis/Zookeeper)来实现全局锁。
1. 原始阶段:SETNX
- 命令:
SETNX lock:key value(Set if Not Exists) - 致命缺陷: 如果机器宕机或程序崩溃,锁未释放(死锁)。
- 补丁: 引入过期时间(TTL)。
2. 进阶阶段:原子性与误删
- 原子性问题:
SETNX和EXPIRE是两步操作,非原子。- 解决: 使用 Redis 2.6+ 复合命令:
SET key value EX 10 NX。
- 解决: 使用 Redis 2.6+ 复合命令:
- 误删问题: 线程 A 卡顿,锁过期自动释放;线程 B 上锁;线程 A 醒来执行
DEL,把线程 B 的锁删了。- 解决: UUID + Lua 脚本。
- Value 存入唯一 ID(如 UUID),删除前判断
if (get(key) == uuid) { del(key) },必须通过 Lua 保证判断和删除的原子性。
3. 终极阶段:续期问题
- 痛点: 业务没跑完,锁过期了怎么办?
- 解决: 引入 守护线程(Daemon Thread) 自动续期。这就是 Redisson 的核心价值。
二、 Redisson 核心机制深度解析
1. 看门狗 (WatchDog) 机制
- 触发条件: 加锁时不指定 LeaseTime(租约时间)。
- 原理:
- 默认租约时间 (
lockWatchdogTimeout) 为 30秒。 - Redisson 启动一个后台线程,每隔 10秒 (1/3 租约时间) 检查一次,如果主线程还持有锁,就重置过期时间为 30秒。
- 默认租约时间 (
- 停止条件: 显式解锁 (
unlock) 或 客户端宕机(看门狗线程随之死亡,30秒后锁自动过期)。
2. 架构哲学:Safety vs Liveness(面试杀手锏)
问题: 如果业务代码死循环,看门狗一直续期导致锁无法释放(死锁),是不是不如"固定过期时间"好?
架构决策:
- 场景 A:固定过期时间(无看门狗)
- 业务死循环 -> 锁过期释放 -> 其他线程进入 -> 并发修改同一数据。
- 后果: 脏写 (Dirty Write) 。数据被污染,账平不上了。(Safety 丢失)
- 场景 B:看门狗自动续期
- 业务死循环 -> 锁一直存在 -> 其他线程拿不到锁(阻塞)。
- 后果: 服务停摆 (Deadlock) 。业务卡住,但数据是安全的。(Liveness 丢失)
结论: 对于金融/交易等核心业务,数据一致性(Safety)高于可用性(Liveness) 。宁可人工介入重启服务解决死锁,也不能容忍数据被脏写。因此,看门狗机制是核心业务的"保护神"。
3. 锁等待机制:Pub/Sub vs 自旋
- SETNX: 失败返回 0,客户端通常需要
while(true)自旋重试,消耗 CPU。 - Redisson: 利用 Redis 的 Pub/Sub(发布订阅) 。
- 获取锁失败时,订阅锁的释放频道,然后挂起当前线程(
Semaphore)。 - 收到释放消息后,唤醒线程再次尝试。
- 优势: 无效等待时不耗 CPU,性能更优。
- 获取锁失败时,订阅锁的释放频道,然后挂起当前线程(
三、 Redisson API 实战指南
在代码中,根据业务场景选择不同的锁策略:
1. 死磕模式(核心业务)
适用于必须执行且不能并发的任务(如每日跑批、资金结算)。
java
RLock lock = redisson.getLock("myLock");
lock.lock(); // 激活看门狗,默认30s,自动续期
try {
// 业务逻辑(哪怕跑1小时也不怕)
} finally {
lock.unlock();
}
2. 优雅等待模式(高并发 Web 业务)
适用于大多数互联网请求,防止请求堆积。
java
// 尝试等待10秒,拿到锁后,锁在30秒后强制过期(无看门狗!)
// 参数:waitTime, leaseTime, unit
boolean res = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
try {
// 业务处理
} finally {
lock.unlock();
}
} else {
throw new RuntimeException("系统繁忙,请稍后再试"); // 快速失败
}
注意: 一旦指定了 leaseTime(第二个参数),看门狗机制失效。
3. 快速失败模式(防抖/非核心)
适用于防止用户重复点击。
java
if (lock.tryLock()) { // 不等待,拿不到立刻返回 false
try { ... } finally { lock.unlock(); }
} else {
return;
}
四、 总结(记忆钩子)
- 原子性: 加锁用
SET ... NX EX,解锁用 Lua 脚本。 - 误删: Value 必须存 UUID 做身份验证。
- 看门狗: 解决"业务没跑完锁过期"的问题。不传
leaseTime时触发。 - 取舍: 核心业务选看门狗(保数据),高并发业务选固定过期时间(保服务)。