Redisson同步器底层原理:RLock、Lua、PubSub、WatchDog与故障窗口
Redisson不是让Redis锁变成强一致事务,而是把锁状态、原子Lua、等待通知、自动续期和Java Future封装成分布式同步器。理解它必须区分Redis中的锁状态、客户端等待线程、WatchDog任务和业务数据库四个层次。
一、RLock状态模型
RLock通常使用Redis Hash表达可重入状态:Key标识锁,Hash Field标识Redisson客户端与Java线程,Field Value记录重入次数,Key TTL限制客户端失联后的最长残留时间。
key: order:lock:1001
type: hash
field: <clientId>:<threadId>
value: 2
ttl: 30000ms相同线程再次加锁让计数从1变2;每次unlock减1,降到0才删除Key并发布释放通知。Java线程ID只在客户端进程内有意义,所以还要组合唯一clientId。
二、加锁Lua原理
flowchart TD
A["客户端执行加锁Lua"] --> B{"锁Key是否不存在"}
B -- "是" --> C["HINCRBY线程字段并设置TTL"]
B -- "否" --> D{"Hash是否包含当前线程字段"}
D -- "是" --> E["重入计数加1并刷新TTL"]
D -- "否" --> F["返回锁剩余TTL"]
C --> G["返回成功"]
E --> G判断、写Hash和设置过期时间必须在一个Lua执行中完成,避免“写锁后进程崩溃、尚未设置TTL”形成永久锁。失败时返回TTL,供等待方决定下一次尝试时机;具体脚本和返回值随Redisson版本变化,应以目标版本源码为准。
三、竞争失败后不是固定自旋
flowchart TD
A["加锁失败并得到TTL"] --> B["订阅该锁的释放Channel"]
B --> C{"释放消息或等待时间到达"}
C -- "释放消息" --> D["被唤醒后重新执行加锁Lua"]
C -- "等待到期" --> D
D --> E{"是否获得锁"}
E -- "否" --> C
E -- "是" --> F["取消等待订阅并进入临界区"]Pub/Sub通知只用于减少无效轮询,不是锁正确性的事实源:消息可能在订阅前发布、连接可能断开、多个等待者会同时被唤醒。被唤醒后必须重新执行原子加锁脚本,不能因收到消息就直接进入临界区。
四、WatchDog完整生命周期
调用不带固定leaseTime的lock()成功后,客户端为锁注册续期任务;通常在锁超时时间的一部分到达前执行Lua,只有Hash仍包含当前线程字段才刷新TTL。
flowchart TD
A["无固定leaseTime加锁成功"] --> B["客户端登记ExpirationEntry"]
B --> C["调度下一次续期"]
C --> D{"锁仍属于当前客户端线程"}
D -- "是" --> E["Lua刷新TTL后继续调度"]
E --> C
D -- "否" --> F["停止续期并清理任务"]
G["正常unlock"] --> F指定固定leaseTime通常表示接受到期自动释放,不启用默认自动续期。WatchDog能处理业务耗时不确定,却不能解决:JVM长时间Stop-The-World、客户端网络分区、Redis不可用、业务线程永久卡死和主从切换丢状态。
五、解锁Lua原理
解锁必须校验当前线程字段:不存在说明不是持有者或锁已过期,不能删除;存在则重入计数减1,仍大于0时刷新TTL;减到0才删除Key并Publish通知。
isLocked()只说明某个线程可能持有,不证明当前线程有权解锁。业务代码使用isHeldByCurrentThread()并在finally中解锁,但检查与unlock之间仍可能发生网络/过期变化,最终以unlock原子脚本结果为准。
六、公平锁
公平锁维护等待顺序和等待者超时信息,尽量让先等待者先获得。它减少插队但增加Redis状态、清理和延迟;等待客户端死亡时还要跳过无效节点。公平不等于严格实时顺序,也不能解决业务优先级。
七、读写锁
多个读锁可并发,写锁与读/写互斥。适合读多写少且临界区确实共享同一事实;跨服务缓存读取通常不应为了“读一致”给所有读取加分布式读锁,否则Redis成为热点并降低可用性。
八、Semaphore与PermitExpirableSemaphore
RSemaphore限制跨实例并发数,例如第三方医院接口最多20个在途请求。普通许可若客户端崩溃可能无法归还;可过期许可为每个permit提供ID和租期,便于超时回收。许可过期后旧任务仍可能继续执行,外部写操作仍需幂等和Fencing。
九、CountDownLatch
RCountDownLatch适合跨实例等待若干步骤完成。必须先设置计数,再由参与者幂等countDown;任务重复执行、漏执行和重建Latch都会改变语义。关键流程更推荐持久任务状态表和补偿,Latch只做同步通知。
十、关键失败窗口
| 窗口 | 后果 | 业务防线 |
|---|---|---|
| 锁成功但响应丢失 | 客户端不知道是否持锁 | 同线程调用状态、超时与取消治理 |
| TTL到期后旧线程继续 | 新旧持有者同时执行 | Fencing、状态机、幂等 |
| Master写锁未复制就故障 | 新Master没有锁 | 不把Redis锁当强一致账本 |
| WatchDog长GC未续期 | 锁过期 | 缩短临界区、监控GC、业务约束 |
| unlock请求结果未知 | 锁可能已释放或仍存在 | 等TTL、查询状态,不能误删 |
| Pub/Sub断线 | 等待唤醒延迟 | 超时后重新竞争 |
十一、商业代码Demo
RLock lock = redissonClient.getLock("order:close:" + orderId);
boolean acquired = false;
try {
acquired = lock.tryLock(300, 10, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new BizException("ORDER_BUSY");
}
// 数据库仍用 where status='UNPAID' 条件更新并检查影响行数。
orderService.closeIfUnpaid(orderId, requestId);
} finally {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}这里锁用于减少并发冲突,数据库条件更新和requestId幂等才保证订单不会从PAID错误关闭。示例使用固定leaseTime,业务必须确保最坏临界区小于10秒;耗时不可控时评估WatchDog,但不能无限持锁。
十二、生产Runbook
锁等待暴涨
检查锁Key热点、持有时长、等待线程、Redis延迟、GC和临界区中的远程调用;先限制热点入口,不盲目增加等待时间。
锁提前释放
确认是否指定过短leaseTime、WatchDog是否启用、客户端事件循环是否阻塞、是否发生长GC或Redis故障;同时核对数据库是否出现双执行。
IllegalMonitorStateException
检查加锁和解锁是否为同一client/thread、锁是否已过期、异步线程是否切换、finally是否在未获得锁时执行。不要捕获后无条件DEL。
Redis故障切换后双执行
按业务键检查旧/new Master时间线、锁复制窗口和两次业务流水;立即依赖数据库状态机/唯一约束止损。Redis异步复制不能给关键写提供线性一致锁。
十三、版本与技术边界
JDK 8/Boot 2.7和Java 17+/Boot 3都能使用对应兼容Redisson版本,锁原理不随Boot改变;变化主要在Starter自动配置、Netty依赖、序列化Codec、配置格式和观测接入。必须用项目BOM验证,避免Netty版本冲突。
十四、关联知识点
本章小结
Redisson通过Hash重入计数、Lua原子状态转换、Pub/Sub等待通知和WatchDog续期降低手写锁错误,但Redis复制、租期和客户端暂停决定它不是强一致事务。关键业务必须让事实资源校验版本、状态或Fencing。
