Redis 分布式锁深度实战:Redisson 看门狗、可重入、防死锁完整代码
分布式锁不是 SET NX EX 那么简单。真正踩过生产坑的人都知道:锁过期了业务没跑完怎么办?同一个线程递归调用死锁了怎么办?Redis 宕机锁丢了怎么办?
Redisson 把这些问题全解了。今天从源码级拆透它的三大核心机制:可重入锁、看门狗续期、RedLock 高可用,并给出可直接上生产的完整代码。
一、先搞清楚:分布式锁要解决什么
| 目标 | 含义 | 不做到会怎样 |
|---|---|---|
| 互斥 | 同一时刻只有一个持有者 | 数据并发冲突 |
| 防死锁 | 锁必须有过期时间 | 进程崩溃,锁永远不释放 |
| 安全释放 | 只有持有者才能删自己的锁 | A 删了 B 的锁,全线崩溃 |
| 可重入 | 同一线程可多次获取同一把锁 | 递归调用直接死锁 |
Redis 单条命令做不到以上全部。Redisson 用 Lua 脚本 + Hash 结构 + 看门狗线程 三板斧,一套打完。
二、可重入锁:同一线程拿锁不阻塞自己
核心数据结构
Redis 里存的不是一个 String,而是一个 Hash:
`Key: myLock
Field: uuid:threadId(客户端唯一标识)
Value: 重入次数(整数)
`
同一个线程第二次拿锁,不是去抢,而是把 Value +1。解锁时 -1,减到 0 才真正 DEL。
加锁 Lua 脚本(源码级)
lua
`-- KEYS[1] = 锁名
-- ARGV[1] = 租约时长(ms)
-- ARGV[2] = 线程标识(uuid:threadId)
if (redis.call('exists', KEYS[1]) == 0) or
(redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil -- 加锁成功
end
return redis.call('pttl', KEYS[1]) -- 返回剩余TTL,供客户端等待
`
三行逻辑讲透:
- 锁不存在 → 新建,计数 = 1,设过期时间
- 锁存在且是自己的 → 计数 +1,刷新过期时间(可重入)
- 锁存在但不是自己的 → 返回剩余 TTL,别傻等,去订阅通知
解锁 Lua 脚本
lua
`-- KEYS[1] = 锁名, KEYS[2] = 通知频道
-- ARGV[1] = 线程标识, ARGV[2] = 租约时长
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil -- 不是自己的锁,直接忽略
end
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)
if counter > 0 then
redis.call('pexpire', KEYS[1], ARGV[2])
return 0 -- 还有重入,没真正释放
else
redis.call('del', KEYS[1])
redis.call('publish', KEYS[2], ARGV[1]) -- 通知等待者
return 1 -- 真正释放
end
`
Java 完整代码
java
`@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
return Redisson.create(config);
}
}
@Service
public class OrderService {
@Resource
private RedissonClient redissonClient;
public void createOrder(String orderId) {
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
// 最多等10秒,锁30秒后自动过期(不指定leaseTime则启用看门狗)
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("获取锁失败,请重试");
}
// 执行业务逻辑
processOrder(orderId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 谁加的锁谁释放,防止误删
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void processOrder(String orderId) {
// 模拟业务处理
System.out.println("处理订单: " + orderId);
}
}
`
可重入演示
java
`RLock lock = redissonClient.getLock("reentrant:demo");
lock.lock(); // 第1次,计数 = 1
lock.lock(); // 第2次,计数 = 2(同一线程,不阻塞)
lock.unlock(); // 计数 = 1
lock.unlock(); // 计数 = 0,真正释放
`
三、看门狗机制:锁自动续期,业务跑多久锁活多久
问题场景
锁设了 30 秒过期,但业务跑了 35 秒。第 30 秒锁自动释放,B 线程抢到锁,A 还在执行------数据直接乱套。
看门狗怎么解
当你不指定 leaseTime 时,Redisson 默认启动看门狗:
- 每隔 10 秒(默认 lockWatchdogTimeout = leaseTime / 3)检查一次锁的剩余时间
- 如果剩余时间 < 1/3 totalLeaseTime,自动通过 PEXPIRE 续期到 30 秒
- 业务跑多久,锁就活多久
`时间轴:
0s → 加锁成功,启动看门狗
10s → 看门狗检查:剩余20s > 10s,不续期
20s → 看门狗检查:剩余10s = 1/3,触发续期 → 重置为30s
30s → 业务还在跑,看门狗再次续期
...
业务结束 → unlock() → 看门狗停止
`
关键代码对比
| 写法 | 看门狗 | 适用场景 |
|---|---|---|
lock.lock() |
✅ 启用 | 业务耗时不确定 |
lock.tryLock(10, 30, SECONDS) |
✅ 启用 | 同上 |
lock.lock(10, TimeUnit.SECONDS) |
❌ 禁用 | 明确知道业务10秒内完成 |
避坑:显式传入 leaseTime 时,看门狗不生效。锁到期自动释放,业务必须在租约内完成。
四、RedLock:Redis 宕机也不丢锁
单节点的致命问题
主节点刚写入锁就挂了,数据没同步给从节点。从节点晋升为主节点,锁丢了,其他客户端照样能加锁------互斥性被打破。
RedLock 算法
向 N 个独立 Redis 节点 (通常 5 个)同时申请锁,获取 多数(≥ N/2 + 1) 即算成功。
`5 节点场景:
- 客户端同时向 5 个节点发送加锁请求
- 成功 ≥ 3 个 → 加锁成功
- 成功 < 3 个 → 向所有节点释放已获取的锁
`
RedLock 完整代码
java
`Config config = new Config();
config.useClusterServers()
.addNodeAddress(
"redis://127.0.0.1:6379",
"redis://127.0.0.1:6380",
"redis://127.0.0.1:6381"
);
RedissonClient redisson = Redisson.create(config);
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
redLock.lock(); // 阻塞等待,直到多数节点加锁成功
// 执行业务逻辑
processCriticalTask();
} finally {
redLock.unlock();
}
`
RedLock 的代价
| 优势 | 代价 |
|---|---|
| 抗单节点故障 | 运维成本高(5个独立实例) |
| 强一致性保障 | 依赖系统时钟,时钟跳变会出问题 |
| 高可用 | 加锁需访问多节点,延迟更高 |
五、其他锁类型速查
| 锁类型 | 适用场景 | 核心代码 |
|---|---|---|
| 公平锁 | 防止线程饥饿,先到先得 | RFairLock fairLock = redisson.getFairLock("myFairLock"); |
| 读写锁 | 多读少写 | RReadWriteLock rw = redisson.getReadWriteLock("rw"); |
| 联锁 | 多个锁必须同时成功 | new RedissonMultiLock(lock1, lock2).lock(); |
| 异步锁 | 高并发不阻塞 I/O 线程 | lock.tryLockAsync(10, 30, SECONDS).thenAccept(...) |
六、生产避坑清单
| 坑 | 解法 |
|---|---|
| 锁过期业务没跑完 | 不指定 leaseTime,启用看门狗 |
| 误删别人的锁 | unlock 前必查 isHeldByCurrentThread() |
| 中断异常锁没释放 | finally 块里释放,catch 里 Thread.currentThread().interrupt() |
| 虚拟线程下重入计数不同步 | Redisson 3.23+ 已修复,升级版本 |
| Redis 集群下脚本跨 slot 失败 | Key 用 {segment}myLock 格式,保证同 slot |
| 锁超时时间怎么设 | 压测取平均值 × 3~5 倍,给 GC 和网络抖动留余量 |
总结:一张图记住 Redisson 锁的全貌
`Redisson 分布式锁
├── 可重入:Hash 结构存 (uuid:threadId → 计数)
├── 防死锁:EXPIRE 过期 + 看门狗自动续期
├── 防误删:Lua 脚本原子校验身份后再 DEL
├── 高可用:RedLock 多节点多数派
└── 锁类型:RLock / RFairLock / RReadWriteLock / RedissonMultiLock / RedissonRedLock
`
Redisson 不是银弹,但它是目前 Java 生态里实现 Redis 分布式锁最完整、最成熟的方案。理解这套机制,比背十道面试题有用得多。