Redisson 总结

322 阅读3分钟

可重入锁的实现原理

Redisson 加锁的核心其实是通过执行 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]);

这段 lua 脚本也比较好理解,首先用 exists 去判断 key 存不存在,也就是锁是否存在,不存在就去获取锁并添加线程标识并设置锁的过期时间,锁是通过 Redis 的 Hash 结构存储的,key 是资源名,field 是 uuid:threadId,value 是获取锁的次数。锁存在的话,通过 hexists 去判断锁标识是否是自己的,是自己的就锁计数器+1,并重置过期时间。如果以上两种情况都不满足,就说明锁被别的线程占用了,也就获取锁失败,返回锁的过期时间。这里相当于实现了锁的重入。

Redisson 释放锁也是通过 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;

KEYS[1] = 锁资源,KEYS[2] = 锁频道

ARGV[1] = 锁频道消息类型,ARGV[2] = 过期时间,ARGV[3] = uuid + : + threadId

如果当前线程未持有锁,直接返回 nil。如果持有该锁,就将锁计数器-1,并判断锁计数器的值,如果大于0,说明锁重入了,重置锁的过期时间。如果小于等于 0,就删除锁,并向锁频道发布释放锁的消息,频道的订阅者就可以收到消息,然后唤醒线程去获取锁。如果以上情况都不满足,直接返回 nil。

锁重试的实现原理

tryLock 方法的第一参数是最长等待时间,如果设置了该参数,获取锁失败后并不会立即返回,而是在等待时间内不断进行尝试获取锁,若时间结束还没有获取成功才会返回 false。获取锁重试,并不是立即重试,在代码里有一个 subscribe 方法,这个方法会去订阅释放锁的消息,我们刚才在释放锁时提到释放锁之后会使用 publish 命令发布消息通知,subscribe 方法就是订阅它发布的通知。消息通知是一个 CompletableFuture,它调用了 get() 方法阻塞等待结果返回,等待时间为锁剩余的最大等待时间,如果在剩余时间内收到订阅通知,会继续计算剩余等待时间,如果此时还有剩余时间,就可以再次尝试获取锁,如果获取成功又失败了,继续等待下一个订阅消息到来,直到无剩余等待时间。

锁自动续期的实现原理

锁自动续期是通过 WatchDog 机制去实现的,但是不是所有获取锁成功都会开启看门狗机制,只有我们在不指定 leaseTime 的情况下,才会开启。我们在获取锁成功以后,会开启一个定时任务,并且这个定时任务会在 this.internalLockLeaseTime / 3L = 10 s 后才去执行,后续的执行其实也是通过 lua 脚本去实现锁的续期,先去判断这个锁是不是该线程持有的,如果是就重置锁的有效期。执行完 lua 脚本之后,又会去调用自己,又会去建立一个定时任务,所以这个锁的有效期就会不断的去重置,实现锁的自动续期。

在锁释放的时候会关闭锁的自动续期,先从缓存里将锁取出删除,再取出定时任务取消掉。