摘要:从一次"分布式锁莫名其妙失效"的故障出发,深度剖析Redisson分布式锁的5大核心机制。通过Watch Dog自动续期原理、可重入锁的实现、红锁Redlock的争议、以及公平锁与联锁的使用场景,揭秘Redisson如何解决锁过期、死锁、主从切换等问题。配合源码分析和时序图展示加锁解锁流程,手写简易版Watch Dog机制,给出生产环境的最佳实践和避坑指南。
💥 翻车现场
周五下午,哈吉米收到了一个诡异的bug报告。
测试同学:@哈吉米 秒杀功能有问题!有时候会超卖!
哈吉米:不可能啊,我用了Redisson分布式锁!
测试同学:你自己看日志!
查看日志:
2024-10-07 15:23:45 [Thread-1] 获取锁成功,库存=10
2024-10-07 15:23:45 [Thread-2] 获取锁成功,库存=10 ← 两个线程同时获取到锁?
2024-10-07 15:23:46 [Thread-1] 扣减库存,库存=9
2024-10-07 15:23:46 [Thread-2] 扣减库存,库存=9 ← 都扣成9了,应该是8
哈吉米:"卧槽,两个线程同时获取到锁了?"
查看代码:
RLock lock = redissonClient.getLock("lock:stock:" + productId);
try {
lock.lock(); // 获取锁
// 业务逻辑
Stock stock = stockMapper.selectById(productId);
stock.setNum(stock.getNum() - 1);
stockMapper.updateById(stock);
} finally {
lock.unlock(); // 释放锁
}
哈吉米:"代码没问题啊……"
紧急查看Redis集群配置:
# Redis集群:1主2从
spring:
redis:
cluster:
nodes:
- 192.168.1.10:6379 # 主节点
- 192.168.1.11:6379 # 从节点1
- 192.168.1.12:6379 # 从节点2
南北绿豆和阿西噶阿西赶来了。
南北绿豆:"你遇到了Redis主从异步复制的问题!"
哈吉米:"???"
阿西噶阿西:"来,我给你深度解析Redisson的原理和坑。"
🤔 核心机制1:Redisson的加锁流程
加锁的Lua脚本(源码)
-- Redisson加锁的Lua脚本
-- KEYS[1]: 锁的key
-- ARGV[1]: 锁的过期时间(30秒)
-- ARGV[2]: 锁的value(UUID:ThreadID)
-- 1. 判断锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 锁不存在,获取锁
redis.call('hset', KEYS[1], ARGV[2], 1); -- 设置重入次数=1
redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置过期时间
return nil; -- 获取成功
end;
-- 2. 判断是否是当前线程持有的锁(可重入)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 是当前线程,重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]); -- 刷新过期时间
return nil; -- 重入成功
end;
-- 3. 锁被其他线程持有,返回锁的剩余过期时间
return redis.call('pttl', KEYS[1]);
存储结构:
Redis中的锁(Hash结构):
key: lock:stock:1001
value (Hash):
{
"uuid-123:thread-1": 1 ← 重入次数
}
TTL: 30秒
加锁流程图
sequenceDiagram
participant Thread1 as 线程1
participant Redis
participant Thread2 as 线程2
Thread1->>Redis: 1. 执行Lua脚本(加锁)
Note over Redis: exists(lock) == 0?
Redis->>Redis: 2. hset(lock, uuid1:thread1, 1)
Redis->>Redis: 3. pexpire(lock, 30000ms)
Redis->>Thread1: 返回nil(获取成功)✅
Thread2->>Redis: 4. 执行Lua脚本(加锁)
Note over Redis: exists(lock) == 0? 否
Note over Redis: hexists(lock, uuid2:thread2)? 否
Redis->>Thread2: 返回29500(锁的剩余时间)
Note over Thread2: 等待或重试
Note over Thread1: 执行业务逻辑
Thread1->>Redis: 5. 执行Lua脚本(解锁)
Redis->>Redis: 6. hincrby(lock, uuid1:thread1, -1)
Note over Redis: 重入次数 = 0,删除锁
Redis->>Redis: 7. del(lock)
Redis->>Thread1: 返回1(释放成功)
Thread2->>Redis: 8. 执行Lua脚本(加锁)
Redis->>Thread2: 返回nil(获取成功)✅
南北绿豆:"看到了吗?Redisson用Hash结构存储锁,支持可重入!"
🤔 核心机制2:Watch Dog自动续期
问题场景
场景:
1. 获取锁,过期时间30秒
2. 业务逻辑执行了40秒(比如调用外部接口慢)
3. 30秒时,锁自动过期
4. 其他线程获取到锁
5. 两个线程同时执行业务 ❌
Watch Dog机制
原理:定时任务自动续期
// Redisson源码(简化)
public class RedissonLock {
private long internalLockLeaseTime = 30000; // 默认30秒
public void lock() {
// 1. 获取锁
tryLockInner(internalLockLeaseTime);
// 2. 启动Watch Dog
scheduleExpirationRenewal();
}
private void scheduleExpirationRenewal() {
// 每10秒执行一次(30秒 / 3 = 10秒)
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
// 续期:重置过期时间为30秒
renewExpiration();
// 继续调度下一次
scheduleExpirationRenewal();
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
private void renewExpiration() {
// Lua脚本:如果锁还存在,续期到30秒
redis.call('pexpire', KEYS[1], 30000);
}
}
Watch Dog时序图
sequenceDiagram
participant App as 应用线程
participant Redis
participant WatchDog as Watch Dog定时任务
App->>Redis: 1. 获取锁,TTL=30秒
Redis->>App: 成功 ✅
App->>WatchDog: 2. 启动Watch Dog
par 业务执行
App->>App: 执行业务逻辑(40秒)
and Watch Dog续期
loop 每10秒
Note over WatchDog: 10秒后
WatchDog->>Redis: PEXPIRE lock 30000
Note over Redis: 续期成功,TTL重置为30秒
Note over WatchDog: 20秒后
WatchDog->>Redis: PEXPIRE lock 30000
Note over WatchDog: 30秒后
WatchDog->>Redis: PEXPIRE lock 30000
Note over WatchDog: 40秒后(业务完成)
end
end
App->>Redis: 3. 释放锁(DEL)
App->>WatchDog: 4. 停止Watch Dog
关键点:
Watch Dog续期频率:
过期时间 / 3 = 30秒 / 3 = 10秒
时间轴:
T0: 获取锁,TTL=30秒
T10: Watch Dog续期,TTL=30秒
T20: Watch Dog续期,TTL=30秒
T30: Watch Dog续期,TTL=30秒
T40: 业务完成,释放锁,停止Watch Dog
结果:即使业务执行40秒,锁也不会过期
阿西噶阿西:"这就是Redisson的核心优势——自动续期,不怕业务执行慢!"
如何关闭Watch Dog?
// 方法1:指定leaseTime(手动设置过期时间)
RLock lock = redissonClient.getLock("lock:stock:1001");
lock.lock(30, TimeUnit.SECONDS); // 指定30秒,不会自动续期
// 方法2:tryLock指定leaseTime
lock.tryLock(10, 30, TimeUnit.SECONDS);
// 等待时间10秒,锁过期30秒,不会自动续期
注意:指定leaseTime后,Watch Dog不会启动,锁到期自动释放。
🤔 核心机制3:可重入锁
什么是可重入?
RLock lock = redissonClient.getLock("lock:test");
public void method1() {
lock.lock();
try {
method2(); // 调用method2
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock(); // 同一个线程再次获取锁(可重入)✅
try {
doSomething();
} finally {
lock.unlock();
}
}
如果不可重入:
method1获取锁 → 调用method2 → method2尝试获取锁 → 等待method1释放锁
→ 死锁 ❌
Redisson可重入的实现
Redis存储:
key: lock:test
value (Hash):
{
"uuid-123:thread-1": 2 ← 重入次数=2(method1加锁1次,method2加锁1次)
}
加锁Lua脚本:
-- 如果锁存在,且是当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
解锁Lua脚本:
-- 解锁时,重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then
-- 重入次数 > 0,不删除锁,只刷新过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return 0;
else
-- 重入次数 = 0,删除锁
redis.call('del', KEYS[1]);
return 1;
end;
可重入流程图
graph TD
A[method1: lock.lock] --> B[Hash: uuid:thread-1 = 1]
B --> C[执行method1业务]
C --> D[调用method2]
D --> E[method2: lock.lock]
E --> F{锁是否存在?}
F -->|是| G{是当前线程持有?}
G -->|是| H[重入次数+1: uuid:thread-1 = 2]
H --> I[执行method2业务]
I --> J[method2: lock.unlock]
J --> K[重入次数-1: uuid:thread-1 = 1]
K --> L{重入次数 > 0?}
L -->|是| M[不删除锁,继续]
M --> N[method1: lock.unlock]
N --> O[重入次数-1: uuid:thread-1 = 0]
O --> P[删除锁 ✅]
style H fill:#90EE90
style P fill:#90EE90
哈吉米:"原来可重入锁是通过计数器实现的!"
🤔 核心机制4:红锁(Redlock)的争议
为什么需要Redlock?
问题:Redis主从异步复制导致锁丢失
场景:
T1: 客户端A在主节点获取锁 ✅
T2: 主节点宕机(锁还没同步到从节点)
T3: 从节点升级为主节点(没有锁记录)
T4: 客户端B在新主节点获取锁 ✅
T5: A和B同时持有锁 ❌
时序图:
sequenceDiagram
participant ClientA as 客户端A
participant Master as 主节点
participant Slave as 从节点
participant ClientB as 客户端B
ClientA->>Master: 1. SET lock:key uuid-A NX EX 30
Master->>ClientA: OK(获取锁)✅
Note over Master: 还没同步到从节点
rect rgb(255, 182, 193)
Note over Master: 主节点宕机
end
Note over Slave: 从节点升级为主节点
ClientB->>Slave: 2. SET lock:key uuid-B NX EX 30
Note over Slave: 从节点没有锁记录
Slave->>ClientB: OK(获取锁)✅
Note over ClientA,ClientB: 两个客户端同时持有锁 ❌
Redlock算法
原理:向N个独立的Redis实例(不是主从关系)获取锁,超过半数成功才算成功。
// Redisson的Redlock实现
RLock lock1 = redisson1.getLock("lock:stock:1001");
RLock lock2 = redisson2.getLock("lock:stock:1001");
RLock lock3 = redisson3.getLock("lock:stock:1001");
RLock lock4 = redisson4.getLock("lock:stock:1001");
RLock lock5 = redisson5.getLock("lock:stock:1001");
// 红锁(需要超过半数成功)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
try {
// 尝试获取锁
boolean success = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (success) {
// 业务逻辑
doSeckill();
}
} finally {
redLock.unlock();
}
流程:
1. 向5个Redis实例获取锁
2. 成功3个以上(超过半数) → 获取锁成功
3. 失败或成功不足半数 → 获取锁失败
4. 释放锁时,向所有实例释放
Redlock的争议
支持方(Redis作者 Antirez):
- ✅ 即使部分节点宕机,锁仍然有效
- ✅ 不依赖主从复制
反对方(分布式专家 Martin Kleppmann):
- ❌ 时钟漂移问题(不同服务器时钟不一致)
- ❌ 长时间GC导致锁失效
- ❌ 复杂度高,不如用Zookeeper
阿西噶阿西:"Redlock在工业界用得不多,争议太大,不如用Zookeeper或单Redis + 主从哨兵。"
🤔 核心机制5:公平锁
公平锁 vs 非公平锁
非公平锁(默认):
RLock lock = redissonClient.getLock("lock:test");
lock.lock(); // 非公平,新来的线程可以抢锁
公平锁:
RLock lock = redissonClient.getFairLock("lock:test");
lock.lock(); // 公平,严格按FIFO顺序
公平锁的实现
原理:用Redis的List存储等待队列
Redis存储:
key: redisson_lock_queue:{lock:test}
value (List):
["uuid1:thread-1", "uuid2:thread-2", "uuid3:thread-3"]
↑ 队列头(下一个获取锁)
流程:
1. 线程1加入队列:RPUSH queue uuid1
2. 线程2加入队列:RPUSH queue uuid2
3. 线程3加入队列:RPUSH queue uuid3
4. 检查队列头是否是自己(LINDEX queue 0)
5. 是 → 获取锁
6. 否 → 订阅锁释放事件,等待
性能对比:
| 锁类型 | TPS | 公平性 |
|---|---|---|
| 非公平锁 | 8000 | ❌ |
| 公平锁 | 5000 | ✅ |
性能差距:公平锁慢37%
🎯 Redisson的其他高级功能
功能1:联锁(MultiLock)
场景:需要同时锁住多个资源
RLock lock1 = redissonClient.getLock("lock:user:10086");
RLock lock2 = redissonClient.getLock("lock:user:10087");
// 联锁(同时获取两把锁)
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);
try {
multiLock.lock();
// 同时锁住了两个用户,可以安全地转账
transfer(10086, 10087, 100);
} finally {
multiLock.unlock();
}
优点:
- ✅ 防止死锁(统一加锁顺序)
- ✅ 原子性(要么全部获取,要么全部失败)
功能2:读写锁(ReadWriteLock)
RReadWriteLock rwLock = redissonClient.getReadWriteLock("lock:data");
// 读锁(共享锁,多个线程可以同时持有)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
// 读数据
readData();
} finally {
readLock.unlock();
}
// 写锁(排他锁,只有一个线程能持有)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
// 写数据
writeData();
} finally {
writeLock.unlock();
}
适用场景:读多写少
功能3:信号量(Semaphore)
RSemaphore semaphore = redissonClient.getSemaphore("semaphore:limit");
// 设置许可数量(最多3个线程)
semaphore.trySetPermits(3);
// 获取许可
semaphore.acquire();
try {
// 业务逻辑(最多3个线程同时执行)
doSomething();
} finally {
semaphore.release();
}
适用场景:限流
🛡️ Redisson使用的7个最佳实践
实践1:设置合理的锁过期时间
// ❌ 错误(过期时间太短)
lock.lock(5, TimeUnit.SECONDS); // 5秒,业务可能执行不完
// ✅ 正确(让Watch Dog自动续期)
lock.lock(); // 默认30秒,自动续期
// 或者
lock.tryLock(10, 30, TimeUnit.SECONDS); // 等待10秒,锁30秒
实践2:必须在finally中释放锁
RLock lock = redissonClient.getLock("lock:test");
try {
lock.lock();
doSomething();
} finally {
if (lock.isHeldByCurrentThread()) { // 判断是否是当前线程持有
lock.unlock();
}
}
实践3:避免锁的粒度太大
// ❌ 错误(锁粒度太大)
RLock lock = redissonClient.getLock("lock:order"); // 锁住所有订单
// ✅ 正确(按订单ID锁)
RLock lock = redissonClient.getLock("lock:order:" + orderId);
实践4:监控锁的性能
@Scheduled(fixedDelay = 10000)
public void monitorLock() {
RLock lock = redissonClient.getLock("lock:stock:1001");
log.info("锁是否被持有: {}", lock.isLocked());
log.info("锁的剩余时间: {}ms", lock.remainTimeToLive());
}
实践5:设置获取锁的超时时间
// ❌ 错误(无限等待)
lock.lock(); // 如果锁一直被占用,永远等待
// ✅ 正确(超时返回)
boolean success = lock.tryLock(3, TimeUnit.SECONDS);
if (!success) {
return Result.error("系统繁忙,请稍后再试");
}
实践6-7(快速总结)
- 区分业务场景(秒杀用非公平锁,转账用公平锁)
- Redis集群用哨兵模式(高可用)
🎓 面试标准答案
题目:Redisson分布式锁的核心原理是什么?
答案:
核心机制:
1. Lua脚本保证原子性
- 加锁:SET NX EX + Hash存储重入次数
- 解锁:判断持有者 + 重入次数-1
2. Watch Dog自动续期
- 默认30秒过期
- 每10秒续期一次
- 业务执行完停止续期
3. 可重入
- Hash结构存储重入次数
- 同一线程多次加锁,计数器+1
- 解锁时计数器-1,为0时删除锁
4. UUID标识
- 每个锁有唯一标识(UUID:ThreadID)
- 防止误删别人的锁
5. 公平锁支持
- List存储等待队列
- 严格按FIFO顺序
优点:
- 性能好(8000 TPS)
- 自动续期(不怕业务慢)
- 功能丰富(可重入、公平锁、读写锁)
缺点:
- AP模型(主从切换可能丢锁)
🎉 结束语
晚上10点,哈吉米把代码改成了Redisson。
哈吉米:"用Redisson后,超卖问题解决了!而且Watch Dog自动续期,再也不怕业务执行慢了!"
南北绿豆:"对,Redisson是Redis分布式锁的最佳实践。"
阿西噶阿西:"记住:不要自己实现Redis锁,直接用Redisson,功能完善、久经考验。"
哈吉米:"还有可重入机制,用Hash存储重入次数,很巧妙!"
南北绿豆:"对,理解了Redisson的原理,就理解了分布式锁的最佳实践!"
记忆口诀:
Redisson分布式锁强,Lua脚本保原子
Watch Dog自动续期,业务再慢也不怕
Hash结构存重入,计数加减很巧妙
UUID标识防误删,公平锁队列排
主从切换可能丢,红锁争议不推荐
希望这篇文章能帮你彻底掌握Redisson分布式锁!记住:生产环境直接用Redisson,不要自己造轮子!💪