Redisson 是如何实现分布式锁的
从 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();
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 原子检查
掌握这四步,你就拥有了 分布式锁的终极武器!