redisson实现可重入锁原理
可重入锁
我们很多情况下会使用redis来实现分布式锁,但利用redis实现基础的分布式锁在稍微复杂情况下会存在一些问题。
-
不可重入
指同一线程无法多次获取同一把锁。比如在获取完锁之后,执行完另一个方法又要再一次获取这把锁,是不允许拿到的。
-
不可重试
一个请求如果第一次获取锁失败,就立即返回,不再进行重试获取。
详细可参考:如何实现可重试锁 -
锁超时释放
业务还没执行完,锁超时释放了,造成并发问题。应该可以根据业务动态延长超时时间。
详细可参考:如何解决锁超时问题
这三点其实有很多工具可以帮忙实现,其原理是一致的,在这以redisson为例深入研究。
redisson是什么?
redisson
就是一个操作redis的一个工具,更多是用来上分布式锁,他帮你写了很多固定的上锁步骤,你直接调用即可。
本篇文章主要研究redisson解决不可重入问题
1 那要实现可重入锁,其原理是什么?
在保存锁时会记录获取锁的次数。如果同一个线程递进3次获取同一把锁,那次数就是3,释放锁时,不能直接删除,而是释放一次锁,标志位就
减1
,直到减为0才删除锁。所以每次释放锁时,需要再判断一下标志位是否为0
需要注意的是只有判断所以存在并且是自己上的时候,才能再次将标识值+1,如果是其他线程获取的则直接获取锁失败。
redisson是基于redis实现了,那在redis里这些数据怎么保存呢?
上锁时向redis里存入hash结构的数据,hash的key为线程值,value为上锁的次数。
2 流程图
redisson实现可重入的原理上面已经大概讲述了,主要是多了个标识位判断次数
,下面看一下整体的流程图:
为保证上锁的原子性同样采用lua脚本实现
3 redisson快速使用
下面介绍怎么快速在项目中使用redisson
-
添加依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.23.2</version> </dependency>
-
注入redisson客户端和它自带的锁操作类
//redisson提供的客户端 @Resource private RedissonClient redissonClient; //redisson提供的上锁工具 @Resource private RLock rLock; public AjaxResult redissonTest(){ boolean isLock = rLock.tryLock(); if (!isLock){ return AjaxResult.error("获取锁失败,有其他线程占用锁"); } try { //执行业务逻辑 } finally { rLock.unlock(); } return AjaxResult.success(); }
4 可重入实现源码
整体的流程和代码示例上面已经介绍,下面我们进入源码看下redisson是怎么实现可重入的。
我们进入rLock.tryLock()
方法,选择最常用的实现类RedissonLock
进入this.tryLockAsync()
方法,会自己获取当前线程的id,仍然选择RedissonLock
实现类
public boolean tryLock() {
return (Boolean)this.get(this.tryLockAsync());
}
public RFuture<Boolean> tryLockAsync() {
return this.tryLockAsync(Thread.currentThread().getId());
}
进入this.tryLockAsync(Thread.currentThread().getId())
方法,此处会异步执行上锁的方法
public RFuture<Boolean> tryLockAsync(long threadId) {
return this.getServiceManager().execute(() -> {
return this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId);
});
}
进入this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId)
方法,会判断一下是否传入过期时间,没有则给默认的过期时间
private RFuture<Boolean> tryAcquireOnceAsync(lon
RFuture acquiredFuture;
if (leaseTime > 0L) {
acquiredFuture = this.tryLockInnerAsync(
} else {
acquiredFuture = this.tryLockInnerAsync(
}
CompletionStage<Boolean> acquiredFuture = th
CompletionStage<Boolean> f = acquiredFuture.
if (acquired) {
if (leaseTime > 0L) {
this.internalLockLeaseTime = uni
} else {
this.scheduleExpirationRenewal(t
}
}
return acquired;
});
return new CompletableFutureWrapper(f);
}
进入tryLockInnerAsync
方法,可以看到再次方法中执行lua脚本,脚本逻辑和上面截图的逻辑一致
//waitTime是重试持续时间
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.commandExecutor.syncedEval(
this.getRawName()
, LongCodec.INSTANCE
, command
, "if ((redis.call('exists', KEYS[1]) == 0) or (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]);"
, Collections.singletonList(this.getRawName())
, new Object[]{unit.toMillis(leaseTime)
, this.getLockName(threadId)});
}
解锁步骤和上锁方式差不多,获取锁判断 —> 如果是自己上的就counter - 1,减后如果counter标识值不大于0了,则删除锁
。进入unlockInnerAsync()
方法,源码如下
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.evalWriteAsync(this.getRawName()
, LongCodec.INSTANCE
, RedisCommands.EVAL_BOOLEAN
, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call(ARGV[4], KEYS[2], ARGV[1]); return 1; end; return nil;"
, Arrays.asList(this.getRawName()
, this.getChannelName())
, new Object[]{LockPubSub.UNLOCK_MESSAGE
, this.internalLockLeaseTime
, this.getLockName(threadId)
, this.getSubscribeService().getPublishCommand()}
);
}
由此可见,可重入锁就是增加了个计数功能,它不会限制你获取锁的次数,但每次获取都会将计数加一。释放锁时要一层层释放知道计数为0。