Redisson分布式事务锁
Redisson分布式事务锁
Redisson 实现分布式锁的原理主要依赖于 Redis 的原子操作和其自身的复杂封装。核心目标是在分布式环境下,以互斥、安全、可靠的方式控制并发访问共享资源。
以下是 Redisson 分布式锁(通常是 RLock 实现类)的关键工作原理:
1.基于 Redis Hash 结构存储锁信息:
-
Redisson 使用 Redis 的一个 Hash 数据结构(Hash Key 对应锁的名称)来存储一把锁的状态。
-
这个 Hash 结构通常包含以下关键字段:
-
UUID:threadID: 一个唯一标识,由客户端 UUID(Redisson 客户端实例唯一)和当前获取锁的线程 ID 拼接而成。用于标识持有锁的客户端和线程,实现可重入性。
-
counter: 一个整数值,表示锁被当前持有者重入的次数。初始为 1,重入一次加 1。用于支持可重入锁。
-
expiration: 锁的过期时间(Unix 时间戳,毫秒)。这是确保锁最终能被释放的关键机制。
2.获取锁(加锁) - Lua 脚本的原子性操作: -
当客户端线程调用 lock.lock() 或 lock.tryLock(…) 方法请求锁时,Redisson 底层会向 Redis 发送一个 Lua 脚本。
-
Lua 脚本的核心逻辑如下:
i.检查锁是否存在(HEXISTS lock_name UUID:threadID):
-
如果不存在(锁是空闲的,或者持有者不是自己),尝试获取:用 HSETNX lock_name UUID:threadID 1 设置初始计数器为 1,并设置 pexpire lock_name ttl 给锁设置一个 TTL(生存时间)。
-
如果设置成功,返回 nil(表示成功获取)。
-
如果设置失败(锁已被其他线程获取),继续后续检查。
ii.检查锁的持有者是否是自己(HEXIST lock_name UUID:threadID): -
如果是(当前线程重入锁),则使用 HINCRBY lock_name UUID:threadID 1 将计数器增加 1,并更新过期时间 pexpire lock_name ttl。返回 nil(表示重入成功)。
iii.如果既不是空闲锁,持有者也不是自己: -
获取锁失败。此时脚本会返回当前锁的剩余生存时间 (TTL)。
-
使用 Lua 脚本执行这些步骤是原子性的关键。Redis 保证 Lua 脚本在执行期间不会被其他命令中断,从而避免了并发判断时的竞态条件。
-
如果尝试加锁失败(步骤3),客户端会根据配置(例如tryLock中的waitTime)进行等待和重试(通常基于 Redis 的 Pub/Sub 通道监听锁释放通知)。
3.锁续期(WatchDog 看门狗机制): -
为了防止客户端在持有锁期间因为 GC 暂停、网络阻塞或线程阻塞等意外情况导致业务逻辑执行时间超过锁的 TTL 而导致锁被 Redis 自动释放(可能引发其他线程获得锁并修改共享资源,造成数据不一致),Redisson 引入了 WatchDog 机制。
-
当一个线程成功获取锁(非设置 leaseTime 的锁)时,Redisson 会在持有锁的客户端 JVM 中启动一个后台守护线程 (WatchDog)。
-
这个线程会定期(默认为 TTL 的 1/3 时间,例如锁 TTL 30 秒,则每 10 秒一次) 向 Redis 发送命令(也是 Lua 脚本)来检查:
i.锁是否仍然存在(HEXISTS lock_name UUID:threadID)。
ii.如果锁存在且持有者仍然是当前客户端线程(uuid:threadId匹配),则使用 pexpire lock_name new_ttl 重置锁的 TTL 为初始值(例如 30 秒)。
- 意义: 只要持有锁的 JVM 进程是存活状态且持有锁的线程没有显式释放锁,WatchDog 就会一直续期,确保业务逻辑在执行期间锁不会因为超时而被自动释放。注意: 在使用 tryLock(…, leaseTime, …) 指定 leaseTime 时,WatchDog 不会生效,锁会在 leaseTime 后自动释放。
4.释放锁 - Lua 脚本的原子性操作: - 当持有锁的线程调用 lock.unlock() 时,Redisson 会向 Redis 发送一个释放锁的 Lua 脚本。
- Lua 脚本的核心逻辑如下:
i.检查锁的持有者是否是当前线程(HEXISTS lock_name UUID:threadID):
-
如果不是,说明试图释放锁的线程不是持有者,直接返回 nil(或报错)。这确保了只有持有者才能释放锁。
ii.如果是持有者,则将计数器减 1 (HINCRBY lock_name UUID:threadID -1)。
iii.检查计数器的新值: -
如果 counter > 0: 表示线程还在重入状态(未完全释放),更新锁的过期时间 (pexpire lock_name ttl),然后返回 0。
-
如果 counter <= 0: 表示锁已经完全释放(重入计数归零),则删除锁的 Hash Key (DEL lock_name), 并通过 Pub/Sub 广播一条锁已被释放的消息给其他等待的客户端,最后返回 1。
-
原子性保证: 整个释放过程(检查->减计数->判断->删除/广播)在脚本中完成,保证了操作的原子性。
-
广播通知: 释放锁后通过 Redis 的 Pub/Sub 发布消息,通知其他在等待该锁的客户端(这些客户端在尝试加锁失败后订阅了该锁名称对应的频道),让它们能立即“醒来”尝试再次争抢锁,减少无效等待时间。
5.高可用考量: -
单点 Redis: 最基本的锁实现依赖于单 Redis 实例。如果该实例宕机,锁服务会中断。
-
Redis Sentinel: Redisson 支持通过 Sentinel 监控主从切换。在主节点故障时,Sentinel 会选举新的主节点,Redisson 客户端能感知到并连接到新主节点,锁信息在切换过程中可能会丢失(新主节点上没有原主节点的锁数据)。Redisson 提供 RLock lock = redisson.getLock(“myLock”); 接口在这种模式下工作,但存在短暂不可用或锁丢失的风险。
-
Redis Cluster: Redisson 支持 Redis 集群。锁会存储在与锁名称 myLock 对应的 CRC16 槽位(Slot)所属的主节点上。集群模式下锁依赖于单个主节点,如果该主节点宕机但尚未完成故障转移(切换从节点),锁服务在该分片上中断。如果故障转移完成,新主节点可能没有之前的锁信息(取决于复制方式)。问题与 Sentinel 模式类似。
-
RedLock 算法: 为了在 Redis 部署具有一定容错能力时(比如 N 个独立的 Redis 主节点部署,非哨兵/集群)提供更强的安全性,Redisson 实现了 RedLock (或 MultiLock) 算法。原理是在多个独立的 Redis 实例上顺序获取锁(lock),只有获取到 超过一半节点(N/2 + 1) 的锁才算成功。释放锁时也需要在所有节点上释放。它牺牲了性能和吞吐量换取了更高的容错性。但 RedLock 本身在业界存在争议,其安全性依赖于精确的时钟和网络假设。
总结关键特性: -
互斥性: 使用 Redis 的 SETNX (在 Hash 中用 HSETNX/计数器逻辑模拟) 保证同一时刻只有一个客户端能成功设置锁信息。
-
可重入性: 通过 Hash 结构中的 counter 字段记录持有线程的重入次数。
-
避免死锁: TTL(锁的生存时间)是核心保障。无论客户端是否显式释放锁,锁最终都会在过期后被 Redis 自动删除。WatchDog 机制进一步确保了在持有者存活且有进展时锁不会因业务执行时间长而过期。
-
释放锁的安全性: 释放锁的 Lua 脚本会严格校验释放者身份(uuid:threadId)和重入计数(counter),确保只有持有锁的线程能释放锁,并且是在最后一次释放时(counter降为0)才真正删除锁的Key。
-
等待锁的效率: 利用 Redis 的 Pub/Sub 机制通知等待的客户端锁已被释放,使其能立即尝试获取锁,而不是仅仅依靠轮询,降低了等待延迟和 Redis 压力。
-
高可用选项: 通过 Sentinel、Cluster 或 RedLock (MultiLock) 可以在一定程度上提高锁服务的可用性,但需要权衡性能、复杂度和实际安全性。
使用要点:
1.务必在 finally 块中调用 unlock() 来确保锁被释放。
2.合理评估业务操作耗时,设置合适的 leaseTime(如果使用带租期的 tryLock)或依赖默认配置下的 WatchDog。避免因超时时间过短导致锁意外失效。
3.理解不同高可用部署模式(单机、Sentinel、Cluster、MultiLock/RedLock)下锁服务的保证和潜在风险。
总的来说,Redisson 的分布式锁通过巧妙地运用 Redis 的原子性、数据结构和发布订阅功能,并结合自身的线程管理(WatchDog)和集群支持(MultiLock),提供了一个在分布式环境下功能强大且相对健壮的分布式互斥锁解决方案。
我们用图文结合的方式来直观理解 Redisson 分布式锁的核心原理:
核心图解概述:
1.锁在 Redis 中的存储结构 (使用 Hash)
2.获取锁流程 (加锁 Lua 脚本)
3.锁续期流程 (WatchDog 机制)
4.释放锁流程 (解锁 Lua 脚本)
5.等待通知机制 (Pub/Sub)
6.高可用选项概览
1. Redis 锁存储结构 (Hash)
±------------------------+ ±--------------------------+
| Lock Key (String) | | “my_lock” (Hash结构) |
| (e.g., “my_lock”) | | |
±------------------------+ ±–±----------------------±------------------+
| | Field Key | Field Value |
| +=======+=+
|关联存储数据 | “c8b5f1a0-2d…:123” | 5 (counter) | <-- UUID:ThreadID 对应计数器 (重入次数)
| ±------------------------±------------------+
| | “expiration” | 1733422800000 | <-- 过期时间戳 (Unix时间, ms)
V ±------------------------±------------------+
- 关键点: 一把锁对应一个 Hash Key。Field Key 是 UUID:ThreadID (唯一标识持有者),Field Value 是该持有者的重入计数器。还有一个特殊的 expiration 字段存储绝对过期时间。
2. 获取锁流程 (加锁 Lua 脚本 - 核心步骤)
graph TD
A[线程调用 lock.lock 或 tryLock] --> B[执行 Lua 脚本到 Redis]
B --> C1{锁 Hash 存在?且 UUID:ThreadID 匹配?}
C1 – 是, 锁被本线程持有 --> C2[计数器 +1 HINCRBY…]
C2 --> C3[更新过期时间 PEXPIRE…]
C3 --> D[返回 null, 加锁成功 (重入)]
C1 – 否 --> C4{锁 Hash 不存在?}
C4 – 是 --> C5[设置 UUID:ThreadID 计数=1 HSETNX…]
C5 --> C6[设置过期时间 PEXPIRE…]
C6 --> C7{设置成功?}
C7 – 是 --> D[返回 null, 加锁成功]
C7 – 否 --> E[返回 TTL 或错误]
C4 – 否, 锁被其他线程持有 --> E[返回当前锁的剩余 TTL]
E --> F{tryLock 能等待?}
F – 是 --> G[订阅锁的 Pub/Sub 频道等待通知]
F – 否 --> H[立即返回 false, 加锁失败]
G – 收到通知或超时 --> B
- 关键点: Lua 脚本在 Redis 单线程中原子执行,确保判断和操作的连续性。脚本依次检查“是否重入”?“锁是否空闲”?并据此决定是设置新锁(计数器初始1)还是增加重入计数(计数器+1),或者返回失败/剩余时间。tryLock 失败后会利用 Pub/Sub 订阅通知,避免无效轮询。
3. 锁续期流程 (WatchDog 机制)
sequenceDiagram
participant C as 客户端JVM (持有锁的线程)
participant R as Redis
participant W as WatchDog线程
C ->>+ R: 成功获取锁 (无leaseTime)
R -->>- C: OK
C ->> W: 启动WatchDog线程
loop 每隔 TTL/3 (e.g., 10s)W ->>+ R: HEXISTS my_lock UUID:ThreadID?R -->> W: 1 (存在且持有者是本线程)W ->> R: PEXPIRE my_lock new_ttlR -->> W: 1 (续期成功)
end
alt 业务逻辑完成 || 进程崩溃/线程阻塞太久C ->> W: 停止WatchDog (正常解锁)W -->> C: OK
else WatchDog轮询时发现锁不存在或持有者改变 || Redis异常W -->> C: 日志警告/停止续期 (锁可能已丢失)
end
- 关键点: WatchDog 是 客户端 JVM 中的一个后台守护线程。它定期检查锁的存在性和持有者身份。如果锁有效,则重置过期时间(续期)。这保证了只要客户端 JVM 和持有线程存活且在“工作”(未僵死),锁就不会因超时而被 Redis 自动删除。如果设置了 leaseTime 参数,则没有 WatchDog。
4. 释放锁流程 (解锁 Lua 脚本 - 核心步骤)
graph TD
A[线程调用 lock.unlock] --> B[执行 Lua 脚本到 Redis]
B --> C1{锁 Hash 存在?且 UUID:ThreadID 匹配?}
C1 – 是 --> C2[计数器 -1 HINCRBY -1…]
C2 --> C3{新计数器值 > 0 ?}
C3 – 是 --> C4[更新过期时间 PEXPIRE…]
C4 --> D[返回 0, 释放成功 (部分释放, 仍持有)]
C3 – 否 --> C5[删除整个锁 Hash DEL…]
C5 --> C6[通过 Pub/Sub 广播锁释放消息]
C6 --> D1[返回 1, 释放成功 (完全释放)]
C1 – 否 --> E[返回 null/错误, 试图释放非持有锁]
- 关键点: Lua 脚本原子执行:校验持有者 -> 计数器减1 -> 判断是否完全释放 (计数<=0)。如果完全释放,则删除锁 Key 并 Pub/Sub 广播释放消息。这个广播通知到等待队列中的其他客户端,让他们醒来尝试抢锁。
5. 等待通知机制 (Pub/Sub)
±-------------------+ 锁释放消息 PUBLISH my_lock_channel “unlock”
| Redis Server |<-------------------------------------+
±-------------------+ |
^ ^ |
|SUBSCRIBE |PUBLISH |
±---------------+ ±---------------+ ±---------------+ | (解锁Lua脚本触发)
| 客户端1 (等待锁) | | 客户端2 (持有锁) |------->| 客户端3 (等待锁) |------+
±---------------+ ±---------------+ ±---------------+
| 阻塞等待状态 | ^ 正在执行业务逻辑 | 阻塞等待状态 (刚订阅)
| | | |
+ 收到通知 ±-------> 解锁 + | 收到通知 +
尝试再次抢锁! 尝试抢锁! 尝试再次抢锁!
- 关键点: 当 tryLock 在等待时间内未获取锁时,客户端会订阅锁名称对应的频道。当锁被释放(解锁脚本第6步)时,Redis 自动广播消息。所有等待的客户端会立即收到通知,结束等待并立即再次尝试执行获取锁的 Lua 脚本。这比轮询更高效。
6. 高可用选项概览
graph LR
subgraph 单点Redis
R1[Redis Server] – 宕机 --> 锁服务完全中断
end
subgraph Redis哨兵(Sentinel)
RM[Redis Master] —|主从复制| RS1[Redis Slave1]
RM —|主从复制| RS2[Redis Slave2]
S1[Sentinel] --> RM & RS1 & RS2
S2[Sentinel] --> RM & RS1 & RS2
S3[Sentinel] --> RM & RS1 & RS2
RM – 主宕机, Sentinel选举 --> RS1 --> 新主[新Master (原RS1)]
新主 – 切换过程中丢失锁? --> 可能丢失
end
subgraph Redis集群(Cluster)
M1[Master Node 1] -->|Hash Slot| “my_lock”
M1 --> S1[Slave of M1]
M2[Master Node 2]
M3[Master Node 3]
M1 – 主节点宕机, 故障转移 --> S1 – 提升为新主? --> 新M1
新M1 – 锁状态丢失? --> 可能丢失
end
subgraph RedLock/多锁(MultiLock)
C[Client] -->|lock| R1[Redis Master 1 (独立)]
C -->|lock| R2[Redis Master 2 (独立)]
C -->|lock| R3[Redis Master 3 (独立)]
C -->|lock| R4[Redis Master 4 (独立)]
C -->|lock| R5[Redis Master 5 (独立)]
C – 获取 N/2+1 (如3/5) 成功 --> 成功持有全局锁
C – 任一节点故障 --> 只要获取>=3个锁仍有效
end
-
关键点解释:
-
单点: 简单,但无容错。
-
哨兵/集群: 提供一定的高可用性,但在主节点故障转移期间,新的主节点可能没有原主节点的锁信息(复制是异步的),导致持有者认为自己有锁而实际锁已失效。
-
RedLock (MultiLock): 尝试提高容错性:客户端需在多个独立Redis节点(非集群、非主从)依次获取锁,只有当成功获取超过一半节点的锁时才算成功。释放也需在所有节点释放。牺牲性能换取在少量节点故障时依然安全(但对时钟和网络延迟敏感,存在争议)。
总结图:Redisson 分布式锁生命周期
启动 业务逻辑执行中
±—+ ±-------+ ±-------+ ±-------+ ±-------+ ±-------+
| | tryLock | 执行 | | WatchDog| | 持有者 | | WatchDog| | 持有者 |
| |-------> | 加锁 |------>| 开始轮询 |<----->| 保持 |<----->| 成功续期 |<----->| 未完成 |
| | 成功 | Lua |成功 | | | 锁 | | (循环) | | |
| | | 脚本 | | | | | | | | |
±—+ ±-------+ ±-------+ ±-------+ ±-------+ ±-------+
| ^ ^ | | |
| | | 锁不存在或 | | |
| | ±-不属于自己?| | |
| | | 返回TTL | | |
| | | | | |
| | | ±–> Redis 节点故障/切换? <------------------------+ (可能导致锁意外失效)
| | | | |
| | | | |
| | | | |
| | | 等待队列 (Pub/Sub) <---------+ 解锁 ------------------------+
| | | | ^ | | ±-------+
| | ±—|--------|------+ | -------> | 执行 | 释放锁 Pub/Sub
| | | 等待/被通知 | | | 解锁 |----------> 广播通知
| | ±--------------<-----------+ | Lua | (唤醒等待者)
| ±----------------------<----------成功释放 | 脚本 | (删除锁Key)
| ±-------+
|
|
完成 (无资源访问) 终止/崩溃
(锁因TTL过期被Redis删除)
通过以上图解,可以直观地看到 Redisson 分布式锁是如何利用 Redis 的数据结构、原子操作 Lua 脚本、Pub/Sub 通知以及客户端的 WatchDog 机制协同工作,实现一个安全、可重入、避免死锁且高效的分布式锁。同时也要理解其在高可用部署下的限制和替代方案(如 RedLock)。