共计 2342 个字符,预计需要花费 6 分钟才能阅读完成。
前言
默认的加锁逻辑是非偏心的。
在加锁失败时,线程会进入 while 循环,始终尝试取得锁,这时候是多线程进行竞争。就是说谁抢到就是谁的。
Redisson 提供了 偏心锁 机制,应用形式如下:
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的应用办法
fairLock.lock();
上面一起看下偏心锁是如何实现的?
偏心锁
置信小伙伴们看过后面的文章,曾经驾轻就熟了,间接定位到源码办法:RedissonFairLock#tryLockInnerAsync
。
好家伙,这一大块代码,我截图也截不完,咱们间接剖析 lua 脚本。
PS:尽管咱不懂 lua,然而这一堆堆的 if else 咱们大略还是能看懂的。
因为 debug 发现 command == RedisCommands.EVAL_LONG
,所以间接看上面一部分。
这么长,连呼好几声好家伙!
先来看看参数都有啥?
- KEYS[1]:加锁的名字,
anyLock
; - KEYS[2]:加锁期待队列,
redisson_lock_queue:{anyLock}
; - KEYS[3]:期待队列中线程锁工夫的 set 汇合,
redisson_lock_timeout:{anyLock}
,是依照锁的工夫戳寄存到汇合中的; - ARGV[1]:锁超时工夫 30000;
- ARGV[2]:UUID:ThreadId 组合
a3da2c83-b084-425c-a70f-5d9a08b37f31:1
; - ARGV[3]:threadWaitTime 默认 300000;
- ARGV[4]:currentTime 以后工夫戳。
加锁队列和汇合是含有大括号的字符串。{XXXX} 是指这个 key 仅应用 XXXX 用来计算 slot 的地位。
Lua 脚本剖析
下面的 lua 脚本是分为几块的,咱们别离从不同的角度看下下面代码的执行。
首次加锁(Thread1)
第一局部,因为是首次加锁,所以期待队列为空,间接 跳出循环。这一部分执行完结。
第二局部:
- 当锁不存在,期待队列为空或队首是以后线程,两个条件都满足时,进入外部逻辑;
- 从期待队列和超时汇合中删除以后线程,这时候期待队列和超时汇合都是空的,不须要任何操作;
- 缩小队列中所有期待线程的超时工夫,也不须要任何操作;
- 加锁并设置超时工夫。
执行完这里就 return
了。所以前面几局部就临时不看了。
相当于上面两个命令(整个 lua 脚本都是原子的!):
> hset anyLock a3da2c83-b084-425c-a70f-5d9a08b37f31:1 1
> pexpire anyLock 30000
Thread2 加锁
当 Thread1 加锁实现之后,此时 Thread2 来加锁。
Thread2 能够是本实例其余线程,也能够是其余实例的线程。
第一局部,尽管锁被 Thread1 占用了,然而期待队列是空的,间接跳出循环。
第二局部,锁存在,间接跳过。
第三局部,线程是否持锁,没有持锁,间接跳过。
第四局部,线程是否在期待队列中,Thread2 才来加锁,不在外面,间接跳过。
Thread2 最初会来到这里:
- 从线程期待队列
redisson_lock_queue:{anyLock}
中获取最初一个线程; - 因为期待队列是空的,所以间接获取以后锁的剩余时间
ttl anyLock
; - 组装超时工夫 timeout = ttl + 300000 + 以后工夫戳,这个 300000 是默认
60000*5
; - 应用 zadd 将 Thread2 放到期待线程有序汇合,而后应用 rpush 将 Thread2 再放到期待队列中。
zadd KEYS[3] timeout ARGV[2]
这里应用 zadd 命令别离搁置的是,redisson_lock_timeout:{anyLock}
,超时工夫戳(1624612689520),线程(UUID2:Thread2)。
其中超时工夫戳当分数,用来在有序汇合中排序,示意加锁的程序。
Thread3 加锁
Thread1 占有了锁,Thread2 在期待,此时线程 3 来了。
获取 firstThreadId2 此时队列是有线程的是 UUID2:Thread2。
判断 firstThreadId2 的分数(超时工夫戳)是不是小于以后工夫戳:
- 小于等于则阐明超时了,移除 firstThreadId2;
- 大于,则会进入后续判断。
第二、三、四局部都不满足条件。
Thread3 最初也会来到这里:
- 从线程期待队列
redisson_lock_queue:{anyLock}
中获取最初一个线程; - 最初一个线程存在,且不是本人,则 ttl = lastThreadId 超时工夫戳 – 以后工夫戳,就是看最初一个线程还有多久超时;
- 组装超时工夫 timeout = ttl + 300000 + 以后工夫戳,这个 300000 是默认
60000*5
,在最初一个线程的超时工夫上加上 300000 以及以后工夫戳,就是 Thread3 的超时工夫戳。 - 应用 zadd 将 Thread3 放到期待线程有序汇合,而后应用 rpush 将 Thread3 再放到期待队列中。
总结
本文次要总结了偏心锁的加锁逻辑,这波及到比拟多的 Redis 操作,做一下简要总结:
- Redis Hash 数据结构:寄存以后锁,Redis Key 就是锁,Hash 的 field 是加锁线程,Hash 的 value 是 重入次数;
- Redis List 数据结构:充当线程期待队列,新的期待线程会应用 rpush 命令放在队列左边;
- Redis sorted set 有序汇合数据结构:寄存期待线程的程序,分数 score 用来是期待线程的超时工夫戳。
须要了解的就是这里会额定增加一个期待队列,以及有序汇合。
对照着 Java 偏心锁源码浏览,了解起来成果更好。
相干举荐
- Redisson 分布式锁源码 04:可重入锁开释
- Redisson 分布式锁源码 03:可重入锁互斥
- Redisson 分布式锁源码 02:看门狗