Redisson 分布式锁原理简析(可重入、续期与高可用)
redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
文档
redisson官方文档中文版.md · ychaoyou/redisson-example - Gitee.com
快速入门
配置maven
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
封装配置类
package com.hmdp.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://{redis所在地址:端口号}")
.setPassword("{redis密码}");
return Redisson.create(config);
}
}
例
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws Exception{
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断获取锁成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
业务实现:
RLock lock = redissonClient.getLock("lock:order:" + userId);
//获取锁对象
boolean isLock = lock.tryLock();
//加锁失败
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
redisson可重入锁原理
概述&引入
不可重入锁问题:
不可重入锁缺乏“owner 识别能力”,
无法区分“当前线程”和“其他线程”,
导致同一线程重复加锁时被错误阻塞,从而产生自死锁。
可重入锁允许同一线程在持有锁的情况下多次获得锁而不会产生死锁。当线程请求锁时,如果它已经拥有了该锁,则可以直接获得锁,锁的计数器会增加。在释放锁时,计数器会减少,只有当计数器为零时,锁才会真正释放。
在redisson中,可采用hash结构用来存储锁
设计要素:
锁的标识符(Lock ID):用于标识锁的唯一性。
持有者标识(Owner ID):线程或进程的唯一标识符,通常是线程的 ID 或进程的 ID。
计数器:记录当前持有锁的次数。
过期时间:为了避免死锁,锁需要设定一个合理的超时时间。
源码分析
在redisson的源码中
其中大key表示表示这把锁是否存在,用小key表示当前这把锁被哪个线程持有,并使用lua表达式实现
加锁源码:
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);"
这个地方一共有3个参数
KEYS[1] : 锁名称
ARGV[1]: 锁失效时间
ARGV[2]: id + “:” + threadId; 锁的小key
redis.call(‘exists’, KEYS[1]) == 0: 判断数据 name:lock 是否存在,
- 如果==0,就表示当前这把锁不存在
redis.call(‘hset’, KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ,写成一个hash结构
Lock{
id + **":"** + threadId : 1
}
- 如果!=0,表示这把锁存在,
再判断 redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1
此时需要通过大key+小key判断当前这把锁是否是属于自己的,如果是自己的,则进行
redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)
将当前这个锁的value进行+1
然后再对其设置过期时间redis.call(‘pexpire’, KEYS[1], ARGV[1])
- 如果以上两个条件都不满足,则表示当前这把锁抢锁失败,最后返回pttl,即为当前这把锁的失效时间
删锁源码:
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 0;
else
redis.call('del', KEYS[1]);
return 1;
end;
在释放锁时,首先校验当前线程是否为锁的持有者:
-
若不是持有者,则拒绝释放(防止误删锁)
-
若是持有者:
先将重入计数减一-
若计数 > 0:
说明仍存在重入,刷新过期时间,不释放锁 -
若计数 == 0:
删除锁,完成最终释放
-
流程基本步骤
- 获取锁 (Lock Acquisition)
尝试设置 lock: 的值,如果这个 key 不存在(即没有锁),那么可以创建该 key,并设置 owner 为当前线程 / 进程的唯一 ID,count 设为 1,expires_at 设为当前时间加上锁的过期时间。
如果该 key 已存在且 owner 是当前线程 / 进程的 ID,则增加 count 并更新 expires_at。
- 释放锁 (Lock Release)
检查当前的 owner 是否是当前线程 / 进程的 ID。如果是,减少 count。
如果 count 减少到 0,则删除该 key。
如果在持有锁的情况下,锁已过期,系统会根据 expires_at 检查锁是否可释放,避免死锁。
- 锁续期 (Lock Renewal)
如果当前线程在执行过程中需要继续持有锁,可以在逻辑处理中重新设置 expires_at。
- 超时与故障恢复
可以设置锁的自动超时时间,例如,设定一个最大持锁时间,超时后锁自动释放。这可以在一定程度上防止死锁的情况。

但是,会引发不可重入,不可重试,锁超时失效等问题
因此引出redisson锁重试和WatchDog机制
redisson锁重试和WatchDog机制
重试机制
当线程第一次尝试获取锁失败时,并不会立即返回,而是进入一个“等待 + 再尝试”的过程
基于 **Redis 的发布订阅(Pub/Sub)**机制来实现的
流程:
-
线程先通过 Lua 脚本尝试加锁,如果返回 null 表示成功,
此时流程并不会直接结束,而是会进一步判断是否指定了
leaseTime1、如果调用的是
lock.lock()(即未指定过期时间,leaseTime = -1),那么 Redisson 会引入 WatchDog(看门狗机制),为当前锁设置一个默认 30 秒的过期时间,并启动一个定时续期任务;该任务会每隔 10 秒(即过期时间的 1/3)执行一次,通过 Lua 脚本判断当前线程是否仍然持有锁,如果是则调用pexpire将锁的过期时间重新刷新为 30 秒,从而保证在业务执行期间锁不会因为超时而被释放。2、相反,如果在加锁时显式指定了
leaseTime(如lock.lock(10, TimeUnit.SECONDS)),则不会启用 WatchDog,而是完全依赖 **Redis 自身的过期时间机制,在指定时间到达后自动释放锁,不再进行续期操作。 -
如果返回的是锁的剩余时间(ttl),说明当前锁被其他线程持有,此时会判断调用方传入的 waitTime 是否还有剩余,
-
如果没有则直接返回失败,
-
如果有则订阅该锁对应的释放频道,并进入阻塞等待状态。
等待过程中存在两种唤醒方式,
一种是持锁线程在 unlock 时通过 publish 发送释放锁的消息,从而主动唤醒所有订阅者;
另一种是等待时间(ttl 或剩余 waitTime)到期触发被动唤醒。线程被唤醒后会再次尝试执行加锁 Lua 脚本,
如此循环,直到成功获取锁或者 waitTime 消耗完为止。通过这种“订阅通知 + 超时兜底”的机制,Redisson 避免了高频自旋带来的 CPU 空转和 Redis 压力问题,实现了更加高效的阻塞式重试。

watchdog
Watch Dog( 看门狗),专门用来监控和续期锁,如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
核心源码:
看门狗返回更新后的时间限制:默认30秒
//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;
public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
this.lockWatchdogTimeout = lockWatchdogTimeout;
return this;
}
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
看门狗核心逻辑:
默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。
private void renewExpiration() {
//......
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//......
// 异步续期,基于 Lua 脚本
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
// 无法续期
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// 递归调用实现续期
renewExpiration();
} else {
// 取消续期
cancelExpirationRenewal(null);
}
});
}
// 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期,
而 renewExpirationAsync() 调用 Lua 脚本实现的续期,保证了原子性
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}

但redis宕机时,会导致锁失效,因此引出redisson锁的MutiLock
redisson锁的MutiLock原理
引入
在使用 Redis 主从架构时,由于复制是异步的,存在这样一种风险:客户端在主节点上成功写入锁之后,主节点尚未来得及将数据同步到从节点就发生宕机,此时哨兵将某个从节点提升为新的主节点,但该节点上并不存在这把锁,导致“锁丢失”,进而出现并发安全问题。
为降低这类风险,Redisson 提供了 MultiLock(组合锁)机制,将多个独立的锁实例(通常分布在不同的 Redis 主节点上)组合为一个逻辑整体来使用。
流程
MultiLock 的获取流程可以理解为“全有或全无”:
客户端在一个总的 waitTime 时间窗口内,依次尝试获取每一把子锁;
若某一把锁获取失败(例如已被占用或节点不可用),则会立即触发回滚,将此前已成功获取的子锁全部释放,然后在剩余的等待时间内继续重试。
只有当所有子锁都获取成功时,MultiLock 才算获取成功;否则在 waitTime 耗尽后返回失败。
工程实践中,常见做法是为整体加锁设置一个合理的总等待时间(例如与锁数量成比例的窗口,如近似 N * 1.5s 作为经验值),以在成功率与时延之间取得平衡。
在持锁阶段,如果未显式指定 leaseTime,每一把子锁都会各自启用 WatchDog(看门狗)进行续期,保证组合锁在业务执行期间不会因为某一把子锁提前过期而失效;
在释放阶段,MultiLock 会逐一释放所有子锁,并取消对应的续期任务。
通过这种“多节点一致成功 + 失败回滚 + 有限时间重试”的策略,MultiLock 在一定程度上提升了分布式环境下锁的可靠性,但代价是更高的网络开销与更低的可用性(任一子锁失败都会导致整体失败),因此更适用于对一致性要求较高的场景。

源码分析
核心逻辑:
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long startTime = System.currentTimeMillis();
// 已成功获取的锁集合(用于失败回滚)
List<RLock> acquiredLocks = new ArrayList<>(locks.size());
while (true) {
for (RLock lock : locks) {
long elapsed = System.currentTimeMillis() - startTime;
long remainTime = time - elapsed;
if (remainTime <= 0) {
// 超时:释放已获取的锁
unlockInner(acquiredLocks);
return false;
}
// 尝试获取单个锁(注意这里是逐个尝试)
boolean locked = lock.tryLock(remainTime, leaseTime, TimeUnit.MILLISECONDS);
if (locked) {
acquiredLocks.add(lock);
} else {
// 只要一个失败 → 回滚
unlockInner(acquiredLocks);
// 重新开始下一轮
acquiredLocks.clear();
break;
}
}
// 如果全部锁都成功获取
if (acquiredLocks.size() == locks.size()) {
return true;
}
// 否则继续 while 重试(直到 waitTime 用完)
}
}
使用remaintime进行判断是否有所剩时间
否:直接返回,拿锁失败
是:进行逐一取锁,如果全部锁都成功获取,但只要一个失败 就 回滚,并重新开始下一轮,直到 waitTime 用完
失败回滚:
private void unlockInner(Collection<RLock> locks) {
for (RLock lock : locks) {
try {
lock.unlock();
} catch (Exception e) {
// 忽略异常(保证尽量释放)
}
}
}
edLocks.size() == locks.size()) {
return true;
}
// 否则继续 while 重试(直到 waitTime 用完)
}
}
更多推荐




所有评论(0)