Redission可重入锁原理

Redission可重入锁原理

Redission

​ Redission是一个在 Redis 的基础上实现的 java 驻内存数据网络,它提供了一系列分布式 Java 对象和许多分布式服务。

可重入:

​ 同一个线程可以多次获取同一把锁

问题引出:

​ 调用一个方法时,它首先给自己上了锁,然后执行业务逻辑过程中,它会调用另一个方法,而另一个方法也会先去获取同一把锁,而此时前一个方法正拿着这把锁不放,这就会造成死锁问题,而为了能够让后执行的方法得到锁,这就需要使用可重入锁。

原理图:

image-20231129163732884

在使用trylock的时候,Redisson会在Redis中创建一个Hash结构的键值对,它的键是我们自定义的键,而值是随着我们调用tryLock的次数递增的,一个方法获取了锁,先创建key,然后让value+1,如果方法内又调用了其他方法,同时该方法也去获取锁,在判断完锁是当前线程的之后,它也会让value+1,如果锁不是当前线程的锁,那么就会直接返回。如果要释放锁,它也会先判断锁是否是当前线程的,若是,则让value-1(注意,这里没有使用del key来删除锁),若不是,则直接返回,一旦value=0之后,就说明全部的锁被释放,那么就可以正式地释放锁,也就是调用del key 来释放锁。

获取锁源码

image-20231129155038582

在 Redission 中,其通过 trylock 方法 来获取锁,其底层是通过调用一个 tryLockAsync() 实现:

public boolean tryLock() {
    return (Boolean)this.get(this.tryLockAsync());
}

追踪 tryLockAsync() 方法

我们可以看到,它获取了当前线程ID然后执行另一个tryLockAsync(long threadId) 方法:

image-20231129155156577

这里它调用了 this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId) 方法,并设置了默认的等待时间和释放时间为 -1L:

image-20231129155250291

这是 tryAcquireOnceAsync 的具体实现:

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1L) {
        return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    } else {
        RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e == null) {
                if (ttlRemaining) {
                    this.scheduleExpirationRenewal(threadId);
                }

            }
        });
        return ttlRemainingFuture;
    }
}

方法定义:

  • 这是一个私有方法,返回一个RFuture<Boolean>对象,表示尝试获取锁的异步结果。
  • 方法接受四个参数:waitTime表示等待时间,leaseTime表示锁的持有时间,unit表示时间单位,threadId表示线程的ID。

对于条件判断的内容:

  • 首先,检查leaseTime是否等于-1L。如果不等于-1L,表示有指定的锁超时时间,将调用tryLockInnerAsync方法来尝试获取锁,传递相应的参数。
  • 如果leaseTime等于-1L,表示没有指定锁的持有时间,将调用tryLockInnerAsync方法来尝试获取锁,但是将锁的超时时间设置为Redisson配置中的lockWatchdogTimeout,并传递相应的参数。

image-20231129160409696

  • 默认的超时时间为 30000 毫秒
  • 无论前面的哪种情况,都会返回一个ttlRemainingFuture对象,表示锁的剩余生存时间。
  • ttlRemainingFuture对象的完成回调方法中,判断如果没有发生异常且ttlRemainingtrue,则调用scheduleExpirationRenewal方法来安排锁的过期续约。

由于我们设置的超时释放时间为 -1L,所以第一个判断语句不会执行。

第二行它执行了 tryLockInnerAsync 的方法

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    this.internalLockLeaseTime = unit.toMillis(leaseTime);
    return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', 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]);", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
}

咋们来看一下中间的这一串 lua 脚本:

if (redis.call('exists', KEYS[1]) == 0) 
    then redis.call('hincrby', 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]);

以及它对应的三个参数:

  • Collections.singletonList(this.getName()) 这个参数表示一个只包含一个元素的不可变列表,元素是通过this.getName()方法获取的锁的名称
  • this.internalLockLeaseTime :锁的超时释放时间,以毫秒为单位
  • this.getLockName(threadId) :表示通过threadId获取的锁的名称

我们来解释一下上面的 lua 脚本的含义:

  • 首先,判断锁是否存在,如果不存在,则通过hincrby命令将锁的计数器加一,并刷新锁的过期时间,然后返回nil
  • 如果锁已存在,且当前线程已经持有该锁,则同样通过hincrby命令将锁的计数器加一,并刷新锁的过期时间,然后返回nil
  • 如果锁已存在,但当前线程未持有该锁(说明锁不是当前线程的),则返回锁的剩余生存时间。

这就是 Redission 可重入锁获取锁的实现,它通过判断当前获取锁的线程是否和 Redis 保存的锁的线程信息一致。若是,则获取锁成功,可以继续往下执行业务,若不是,则直接返回,无法获取锁。

释放锁源码

通过追踪 Redission 的 unlock() 方法:

可以看到,其执行了 unlockAsync 方法,并传入了当前线程ID:

image-20231129162442047

进入 unlockAsync(long threadId) 方法,可以看到类似于获取锁,它也执行了一个 unlockInnerAsync(threadId) 的方法:

image-20231129162706606

该方法的实现也是通过一个 lua 脚本实现的释放锁逻辑:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return this.evalWriteAsync(this.getName(), 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('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId));
}

lua脚本内容:

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('publish', KEYS[2], ARGV[1]); 
    return 1; 
end; 
return nil
  • 首先,判断锁是否存在,如果不存在,则直接返回nil
  • 如果锁存在,则通过 hincrby 命令将锁的计数器减一,并获取计数器的值。
    • 如果计数器大于0,则表示仍有其他线程持有该锁,通过pexpire命令续约锁的过期时间,并返回0。
    • 如果计数器等于0,则表示当前线程是最后一个持有锁的线程,通过del命令删除锁,并通过publish命令发布一个解锁消息,返回1。
  • 如果锁不存在或其他异常情况,则返回nil

rby` 命令将锁的计数器减一,并获取计数器的值。

  • 如果计数器大于0,则表示仍有其他线程持有该锁,通过pexpire命令续约锁的过期时间,并返回0。
  • 如果计数器等于0,则表示当前线程是最后一个持有锁的线程,通过del命令删除锁,并通过publish命令发布一个解锁消息,返回1。
  • 如果锁不存在或其他异常情况,则返回nil

总之,释放锁是通过减少锁的计数器来实现锁的释放,并根据计数器的值判断是否需要续约或者删除锁。如果当前线程是最后一个持有锁的线程,则会发布一个解锁消息。

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值