✅Redisson里面的锁是如何实现可重入的?

✅Redisson里面的锁是如何实现可重入的?

典型回答

所谓可重入,就是同一个线程在运行过程中,如果多次拿同一把锁,需要让他可以获取成功。可重入的锁可以有效的避免死锁的问题。

这种题,就是讲源码最有说服力了。(这句话好像在哪见过,确实,Redisson讲不误删的也有这句话,而且下面的代码也都一样的。因为这俩问题是一段代码解决的。https://www.yuque.com/hollis666/sza8tg/ucarmoyv1belggn0

先来看Redisson加锁的核心lua相关的代码,在RedissonLock#tryLockInnerAsync中。

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return evalWriteSyncedNoRetryAsync(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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

这段lua脚本执行之后,最终会在Redis中存一个Hash结构,key就是我们指定的加锁的key的名字,比如Hollis666,然后存储的hash的key是一个真正的lockName(不展开了,在介绍误删的文章中有),存储的值是这个锁被重入的次数。

那么也就是说,如果同一个线程,多次加锁的话,lockName是一样的,那么这段lua都能执行成功,每次执行都会给hash中存的value的值+1。这样就实现了可重入。

而每一次都+1了,什么时候可以真正的解锁,也就是把和这个key彻底删除呢。看一下解锁的代码,在RedissonLock#unlockInnerAsync中。


protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {
      return evalWriteSyncedNoRetryAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
          "local val = redis.call('get', KEYS[3]); " +
                "if val ~= false then " +
                    "return tonumber(val);" +
                "end; " +

                "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]); " +
                    "redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +
                    "return 0; " +
                "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call(ARGV[4], KEYS[2], ARGV[1]); " +
                    "redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +
                    "return 1; " +
                "end; ",
            Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),
            LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,
            getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
  }

可以看到,这里面也把getLockName(threadId)的结果传到lua脚本中了,作为ARGV[3]

可以看到,上面的代码中,Lua 脚本首先会检查 Redis 中锁 Key 对应的 Hash 结构中,是否存在一个 Field 等于 ARGV[3](即当前客户端的 UUID:threadId)。

  • 如果不存在:说明在当前客户端准备解锁时,这把锁已经不属于它了。此时,脚本直接返回 nil,而不是执行删除操作。这直接防止了误删他人锁的行为。不存在可能的原因有:
    • 锁早已过期被自动删除(设置了超时时间,就不自动续期了),然后被其他客户端获取。
    • 客户端试图释放一个它从未持有的锁。
  • 如果存在:
    • 说明是锁的所有者, then 将重入次数减 1。
    • 如果减 1 后重入次数大于 0,说明当前线程多次锁(可重入),还没有完全释放。此时只刷新过期时间,不删除 Key。
    • 只有重入次数减到 0 时,才执行 del KEYS[1] 删除整个锁 Key。

所以,总结一下,就是在加锁的时候,Redisson并没有单纯用一个string类型来存储一个固定值,比如和key相同,或者存个固定数字,而是存储了一个hash结构,然后针对这个hash结构中的元素的的key设置为lockName,lockName其中包含了当前的线程id,value为加锁次数。从0开始,第一次加锁就设置为1。每一次加锁的时候如果对应的lockName已经存在了,则次数+1,并且在解锁的时候也会通过同样的这个lockName来做解锁判断,如果已经有值了,则-1,然后再判断剩余的次数是否大于0,如果不大于0了,则删除对应的key-value。