从 setnx 到 RedLock,从 Lua 脚本 到 WatchDog,一文彻底讲透
版本:Redisson 3.23 + Redis 7 + JDK 17
目录
- 背景与痛点
- 核心数据结构
- 加锁流程(可重入锁)
- 释放锁流程
- WatchDog 自动续期
- 公平锁实现
- RedLock & MultiLock
- 线程模型与性能
- 最佳实践
- 常见坑 & FAQ
- 总结
背景与痛点
| 原生 SETNX 缺陷 |
Redisson 解决方案 |
| 不可重入 |
Hash 结构 + 重入计数 |
| 无超时释放 |
过期时间 + WatchDog |
| 无重试队列 |
Pub/Sub 等待通知 |
| 主从一致性 |
RedLock / MultiLock |
核心数据结构
Redis Key
lock:{resource}
| 类型 |
字段 |
含义 |
| Hash |
UUID:ThreadId |
持有者 + 重入次数 |
| List |
redisson_lock_queue:{resource} |
公平锁排队 |
| ZSet |
redisson_lock_timeout:{resource} |
公平锁超时 |
加锁流程(可重入锁)
1. 入口方法
RLock lock = redisson.getLock("order:123");
lock.lock(); // 默认 30s 租约
2. Lua 脚本(精简版)
-- KEYS[1] = lockKey, ARGV[1] = 线程标识, ARGV[2] = 过期时间(ms)
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[1], 1);
redis.call('pexpire', KEYS[1], ARGV[2]);
return nil; -- 表示获取成功
end;
if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[1], 1);
redis.call('pexpire', KEYS[1], ARGV[2]);
return nil;
end;
return redis.call('pttl', KEYS[1]); -- 返回剩余时间,客户端重试
3. 重试机制
- 自旋重试:
attempts × sleep(默认 3 次)
- Pub/Sub 等待:客户端订阅
__keyspace@*__:{lockKey},解锁时收到通知,减少 CPU 空转。
释放锁流程
Lua 脚本
-- KEYS[1] = lockKey, 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], 1); -- 通知等待队列
return 1;
end;
WatchDog 自动续期
| 触发时机 |
续期间隔 |
停止条件 |
| 首次加锁成功 |
leaseTime / 3(默认 10s) |
线程主动 unlock() |
| 每次续期 |
重置 leaseTime |
进程崩溃 / 网络分区 |
源码位置:RedissonLock.scheduleExpirationRenewal() → Netty HashedWheelTimer 周期性执行续期 Lua。
公平锁实现
数据结构
- List
redisson_lock_queue:{resource} → 排队顺序
- ZSet
redisson_lock_timeout:{resource} → 超时淘汰
Lua 片段(公平锁加锁)
-- 当前线程必须在队首才能获取锁
if (redis.call('exists', KEYS[1]) == 0 and
(redis.call('exists', KEYS[3]) == 0 or
redis.call('lindex', KEYS[3], 0) == ARGV[1])) then
redis.call('lpop', KEYS[3]);
redis.call('zrem', KEYS[4], ARGV[1]);
redis.call('hset', KEYS[1], ARGV[1], 1);
redis.call('pexpire', KEYS[1], ARGV[2]);
return nil;
end;
RedLock & MultiLock
RedLock(多数派)
RedissonRedLock lock = new RedissonRedLock(
redisson.getLock("lock1"),
redisson.getLock("lock2"),
redisson.getLock("lock3")
);
lock.lock();
- 加锁成功 >
N/2 节点
- 故障恢复 依赖 时钟漂移 容错
MultiLock(全部成功)
RedissonMultiLock lock = new RedissonMultiLock(
redisson.getLock("lock1"),
redisson.getLock("lock2")
);
lock.lock();
线程模型 & 性能
| 维度 |
说明 |
| 内部线程 |
Netty EventLoop 或自定义 ExecutorService |
| 回调线程 |
与加锁线程相同,避免上下文切换 |
| 连接池 |
redisson.nettyThreads = 0 自动 CPU*2 |
最佳实践清单
| 场景 |
推荐锁类型 |
租约时间 |
| 普通互斥 |
RLock |
业务最大耗时 × 2 |
| 高并发公平 |
RFairLock |
30s |
| 跨机房高可用 |
RedLock (≥3 节点) |
30s |
| 业务耗时 > 1s |
业务线程池 + 较长租约 |
|
常见坑 & FAQ
| 坑 |
解决 |
| 误删锁 |
使用 isHeldByCurrentThread() 判断 |
| 时钟漂移 |
RedLock 增加 clock-drift-factor |
| 网络分区 |
设置合理 lockWatchdogTimeout |
总结
- 加锁 = Lua 原子脚本 + Hash 重入
- 续期 = WatchDog 定时任务
- 一致性 = RedLock / MultiLock
- 解锁 = 线程标识 + Lua 原子检查
掌握这四步,你就拥有了 分布式锁的终极武器!