文章目录
- 一、基于Redis实现分布式锁的问题
- 二、Redisson的使用
- 三、Redisson可重入的原理
- 四、Redisson的可重试原理(PubSub机制)
- 五、Redisson的超时续约原理(WatchDog机制)
- [六、Redisson 的主从一致性原理(MultiLock)](#六、Redisson 的主从一致性原理(MultiLock))
- 七、Redisson分布式锁原理
一、基于Redis实现分布式锁的问题
用 SET NX EX + Lua 脚本实现的分布式锁,能满足基本的互斥需求,但在生产环境中仍存在四个典型缺陷:
- 不可重入:同一个线程无法多次获取同一把锁。
- 不可重试:获取锁失败后,只能立刻返回 false,无法自旋等待。
- 超时释放:业务执行时间如果超过锁的过期时间,锁会被自动释放,导致并发安全问题。
- 主从一致性:如果 Redis 主节点宕机,锁数据尚未同步到从节点,其他线程可能重新获取到锁。
Redisson 作为成熟的 Redis 分布式锁框架,针对这些问题提供了完善的解决方案。
二、Redisson的使用
- 引入依赖:
html
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
- 配置Redisson客户端:
java
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
// 配置类
Config config = new Config();
// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
// 创建客户端
return Redisson.create(config);
}
}
- 使用 Redission 的分布式锁
java
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
// 获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
// 判断释放获取成功
if(isLock){
try {
System.out.println("执行业务");
}finally {
// 释放锁
lock.unlock();
}
}
}
三、Redisson可重入的原理
存储数据结构变为Hash,field存储线程唯一标识,value存储重入次数
Redisson 的可重入锁参考了 JUC 中 ReentrantLock 的设计,通过 state 计数器 记录锁的重入次数:state = 0 表示锁空闲,state ≥ 1 表示已被线程持有,state > 1 表示线程多次重入。
Redis 中使用 Hash 数据结构来存储锁信息;key 表示锁的名称;field 表示线程唯一标识(UUID + 线程ID);Value表示重入次数

- 获取锁的流程:若锁不存在,则说明当前锁空闲,直接获取锁。锁已存在,则说明有线程正在持有锁,于是判断锁是否被自己持有,是自己的锁则重入,不是则获取锁失败。
- 释放锁的流程:判断锁是否被自己持有,若是自己的锁,重入次数减 1,再判断重入次数是否为 0,若value> 0说明线程仍有未完成的重入层次;若 value== 0 说明所有重入层次都已执行完毕,可以完全释放锁,删除锁del key。

获取锁的 Lua 脚本
lua
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) then
-- 不存在,获取锁
redis.call('hset', key, threadId, '1');
-- 设置有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
-- 不存在,获取锁,重入次数+1
redis.call('hincrby', key, threadId, '1');
-- 设置有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
释放锁的 Lua 脚本
lua
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
return nil; -- 如果已经不是自己,则直接返回
end
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断是否重入次数是否已经为0
if (count > 0) then
-- 大于0说明不能释放锁,重置有效期然后返回
redis.call('EXPIRE', key, releaseTime);
return nil;
else -- 等于0说明可以释放锁,直接删除
redis.call('DEL', key);
return nil;
end
四、Redisson的可重试原理(PubSub机制)
获取锁失败的时候会调用 subscribe 方法订阅频道,释放锁的时候会 publish
手动实现的分布式锁,在获取失败时直接返回 false,不具备自旋重试能力。Redisson 的 tryLock 方法支持设置最大等待时间,获取锁失败时不会立即返回,而是利用 Redis 的 Pub/Sub(发布订阅) 机制等待唤醒。
- 线程 A 尝试获取锁失败后,会订阅一个与该锁相关的频道(channel),然后进入阻塞等待。
- 当持有锁的线程 B 释放锁时,会向该频道发布一条消息,通知所有等待的线程锁已经被释放。
- 线程 A 收到通知后,被唤醒,立即再次尝试获取锁;如果仍然失败,则继续等待,直到超时。
RedissonLock 的 tryAcquireAsync 返回 Future<Boolean>,如果获取失败,会调用 subscribe 方法订阅频道,并将当前线程挂起在 Semaphore 上。一旦收到释放锁的消息,信号量释放,线程被唤醒重试。
五、Redisson的超时续约原理(WatchDog机制)
未指定超时时间则会开启一个后台定时任务(看门狗),每隔 10 秒检查锁是否仍被当前线程持有,若是则自动将锁的过期时间重置为 30 秒,直到线程主动释放锁或进程终止
锁的超时时间设得太长,可能导致资源长时间被占用;设得太短,业务还没执行完锁就过期。Redisson 通过 Watch Dog(看门狗) 机制,自动为锁延长有效期。
- 当线程成功获取锁后,如果未显式指定锁的释放时间(即 leaseTime = -1),Redisson 会启动一个定时任务(Watch Dog)。
- 定时任务每隔 internalLockLeaseTime / 3 秒(默认 internalLockLeaseTime = 30 秒,所以每 10 秒)执行一次。
- 它执行一段 Lua 脚本,检查当前线程是否仍然持有该锁,如果是,则将锁的过期时间重新设置为 30 秒。
- 当线程主动释放锁或进程崩溃,Watch Dog 自动停止。
调用 unlock() 、客户端进程崩溃、与 Redis 断连,或线程被中断/终止都会导致Watch Dog 停止,锁会在 30 秒后自动释放
用户无需担心业务执行时间过长导致锁过期;同时,一旦进程意外退出,Watch Dog 停止续期,锁会在 30 秒后自动释放
只要业务线程未执行完毕且未主动释放锁,Watch Dog 每隔 10 秒会用 Lua 脚本检查锁是否属于自己的线程,然后重新设置过期时间为 30 秒,理论上可以无限续期

六、Redisson 的主从一致性原理(MultiLock)
在 Redis 主从架构中,如果主节点写入锁数据后未来得及同步到从节点就宕机,从节点升级为主后,锁数据会丢失,其他线程可能重新获取到锁,造成并发安全问题。
例如下面的例子,Java 应用向 Redis Master 成功写入了锁,此时 Master 突然宕机,而这条锁数据还未同步到 Slave,Sentinel(哨兵)会从 Slave 中选举一个新的 Master,但新的 Master 中并没有 lock 这条数据。另一个 线程向新 Master 申请同一把锁会成功,因为锁数据根本不存在。

Redisson 的 MultiLock 方案正是为了解决这一问题。它不依赖主从复制,而是要求客户端在多个完全独立的 Redis 节点上依次成功获取锁,才算真正获取到锁。
如下图所示,部署多个独立的 Redis 节点(通常为奇数,如 3 个或 5 个),没有主从关系(或者为主节点独立设置从节点以保证高可用)。加锁时,客户端按顺序向每个节点发送 SET NX EX 请求。当其中一个Redis宕机了,新线程请求锁,在第一个Redis Master上请求成功,但当在其他节点上获取失败。

七、Redisson分布式锁原理
- 可重入:利用 Redis Hash 结构存储线程标识和重入次数,通过 Lua 脚本原子加减。
- 可重试:利用 Redis Pub/Sub 发布订阅 + Java Semaphore 实现等待唤醒。
- 超时续约:Watch Dog 机制,后台定时任务每隔 10 秒延长锁过期时间(默认总时长 30 秒)。
- 主从一致性:MultiLock机制,在多个独立 Redis 节点上同时加锁,打包成一个"组合锁"。