Redisson(2-1)分布式锁实现对比 VS Java的ReentrantLock之tryLock

Redisson实现了一整套JDK中ReentrantLock的功能,这里对比一下实现的差异和核心的思想。

unfair模式的tryLock

ReentrantLock

①判断当前的state是否是0(初始状态),并用原子操作设置state,成功说明获取锁,并把当前线程设置为获取锁的线程。

②如果state不是0,检查当前线程是否是持有锁的线程,如果是就按照重入语义,增加计数,当然前提是不能超过最多重入的次数(不能溢出),然后将最终计数设置到state中。

③如果既不是①也不是②,那么tryLock失败。

Redisson

因为这里的锁是用Redis实现的,不在Java虚拟机内,所以只能用线程id来辨识锁,无法通过调用Thread.currentThread()方法来做。无参的加锁方法,leaseTime是-1,所以走的是if下面的逻辑,这个加锁的逻辑是通过lua脚本来完成的。这个和ReentrantLock的加锁方式不同的是,为了防止死锁产生(参考上一篇博客),这里设置了一个看门狗,默认时间是30秒钟,如果业务没有处理完,会每隔10秒钟延长这个锁的时间。

具体加锁的逻辑是通过lua脚本来完成,因为lua脚本会保证原子性。具体的lua脚本内容可以参见博客:https://my.oschina.net/u/2313177/blog/1919810

Lua脚本中的执行分为以下三步:

①'exists', KEYS[1]) == 0 //exists检查redis中是否存在当前要获取的锁

('hset', KEYS[1], ARGV[2], 1) //如果不存在,则获取成功;同时设置锁名称KEYS[1],线程id[ARGV[2],锁重入次数1

('hexpire', KEYS[1], ARGV[1]) 设置锁的过期时间为ARGV[1],返回;

②('hexists', KEYS[1], ARGV[2]) ==1  //如果检查到存在KEYS[1],[ARGV[2],则说明获取成功

('hincrby', KEYS[1], ARGV[2], 1)  //此时会自增对应的value值,记录重入次数

('hexpire', KEYS[1], ARGV[1]) //并更新锁的过期时间

③key不存在

('pttl', KEYS[1] ) //直接返回key的剩余过期时间

上述一系列操作都是原子性的,所以没有线程并发问题。

后面还有一段添加listener的操作,因为上面的lua脚本执行的redis指令是异步的调用,会返回一个future,这个future如果成功放回,同时返回的结果的Boolean值是true,说明获取锁成功了,这个时候会调用一个叫做scheduleExpirationRenewal的方法:

如果该锁已经添加过了,那么返回,否则创建一个延时任务,该任务递归调用该任务本身。最后为了保证竞态,防止多个线程同时写,会将写失败的线程对应的任务cancel掉。这个方法的内容在上一篇博客中已经解释了,就是redisson看门狗的作用。

这里有几个问题需要注意一下:

①为什么说这里的返回值Boolean是true,就说明获取锁成功呢?

这里看下redis指令最后的一个commands参数:

结果的Boolean表示的是,返回值不等于null这个条件是否为真。这里如果Boolean是true,说明返回值是null。那么什么情况返回值是null呢?看上面分析的lua脚本,只有当当前线程id获取锁成功的时候,才会返回null,否则会返回ttl。

②tryAcquireAsync和tryAcquireOnceAsync

仔细看的话,会发现tryAcquireAsync中的判断是否加锁成功的逻辑和上述逻辑正好相反,

原因是因为这里的commands是

所以返回的就是指令本身,那么根据上述lua脚本,只有当获取锁的时候,返回值才是null,所以这里的判断虽然和上面相反,但是逻辑本身是一模一样的。

设计成这样的原因是因为,带once的方法需要返回Boolean,一锤子买卖,如果try失败了再进行其他逻辑。而后面的方法,失败后会进行复杂的锁竞争处理(这个后面会继续说)。

两者实现的功能从接口上也能看出不同,前者是:

失败就失败了,继续按照失败或成功来进行业务逻辑即可。

后者是:

如果没有立刻获取到锁,会在超时时间之内进行重试等复杂操作,直到获取锁或者超时为止。

猜你喜欢

转载自blog.csdn.net/xxcupid/article/details/88183513