Redisson 分布式鎖源碼之一:可重入鎖加鎖
前言
相信小伙伴都是使用分布式服務(wù),那一定繞不開(kāi)分布式服務(wù)中數(shù)據(jù)并發(fā)更新問(wèn)題!
單系統(tǒng)很容易想到 Java 的各種鎖,像 synchronize、ReentrantLock 等等等,那分布式系統(tǒng)如何處理?
當(dāng)然是使用分布式鎖。
如果小伙伴不知道什么是分布式鎖,那推薦看看石杉老師的突擊課或者在網(wǎng)上搜一搜相關(guān)資料。
當(dāng)使用 Redis 作為分布式鎖時(shí),當(dāng)前使用較多的框架就是 Redisson。
當(dāng)然 Redisson 也不僅僅只能當(dāng)做鎖來(lái)使用,也有很多其他的功能,小伙伴們可以看一看官方文檔,自己多動(dòng)手實(shí)踐一下。
下面就開(kāi)始記錄 Redisson 的相關(guān)筆記!錯(cuò)誤之處,歡迎指正。
1、環(huán)境配置
- 本地環(huán)境搭建的偽集群:
- redisson 3.15.6
不同版本可能會(huì)有所不同,但是核心思想不會(huì)發(fā)生太大變化,如果變化很大,希望可以留言。
- <dependency>
- <groupId>org.redisson</groupId>
- <artifactId>redisson</artifactId>
- <version>3.15.6</version>
- </dependency>
- 項(xiàng)目準(zhǔn)備
一個(gè)簡(jiǎn)單的 maven 項(xiàng)目,只需要一個(gè) Main 方法即可。
2、可重入鎖加鎖
在 lock.lock() 斷點(diǎn),作為源碼入口。
默認(rèn)加鎖,什么參數(shù)也沒(méi)有傳遞。但是這里會(huì)設(shè)置 leaseTime = -1。這個(gè) leaseTime 的含義是加鎖的時(shí)間。
剩下的一路挺進(jìn)即可。
在調(diào)用 tryAcquire 方法之前,多了一個(gè)參數(shù) threadId,是當(dāng)前線程的 id,long 型正數(shù)。
異步加鎖
直接來(lái)到 tryAcquireAsync 異步加鎖方法。
tryAcquireAsync
前面已經(jīng)說(shuō)了 leaseTime 是 -1,所以這里會(huì)走到下面的方法中。
至此幾個(gè)參數(shù)已經(jīng)清楚:
- waitTime:-1;
- internalLockLeaseTime:使用默認(rèn)時(shí)間 30000 毫秒;
- TimeUnit.MILLISECONDS:?jiǎn)挝缓撩?
- threadId:線程 id;
- RedisCommands.EVAL_LONG:eval。
Redis eval 命令的相關(guān)文檔可以閱讀:https://redis.io/commands/eval
加鎖邏輯
真正的加鎖,其實(shí)就是這么一段 lua 腳本。
先說(shuō)明一下 lua 腳本的參數(shù)信息:
- KEYS[1]:getRawName(),加鎖的 key ,比如 anyLock;
- ARGV[1]:unit.toMillis(leaseTime),鎖的毫秒時(shí)間,比如 30000;
- ARGV[2]:getLockName(threadId),是 UUID 和線程 id 拼接起來(lái)的字符串,比如 931573de-903e-42fd-baa7-428ebb7eda80:1。
因?yàn)槭褂玫氖?lua 腳本,可以保證這一塊 lua 腳本的原子性。
首次加鎖分析:
- exists 命令判斷 redis anyLock 是否存在;
- 不存在,使用 hincrby 命令,創(chuàng)建 anyLock 數(shù)據(jù);
- 對(duì) anyLock 設(shè)置過(guò)期時(shí)間。
加鎖后 Redis 內(nèi)的數(shù)據(jù)格式是:
關(guān)于 Redis 的 Hash 數(shù)據(jù)結(jié)構(gòu)可以閱讀:https://redis.io/topics/data-types#hashes
抽象一點(diǎn)可以理解為 anyLock 下面掛著一個(gè) K-V 結(jié)構(gòu)的數(shù)據(jù):
- "anyLock":{
- "f400aad5-4b1f-4246-a81e-80c2717c3afb:1":"1"
- }
執(zhí)行腳本
后續(xù)的內(nèi)容就是進(jìn)行請(qǐng)求執(zhí)行 lua 腳本,唯一需要注意的地方就是有個(gè)哈希槽路由。
這塊代碼是在 CommandAsyncService#evalWriteAsync 方法處調(diào)用的,是為了獲取一個(gè) NodeSource。
當(dāng)然這個(gè) NodeSource 里面只存放了一個(gè) slot(哈希槽值)。
這個(gè) slot 值是對(duì)加鎖的 key 使用 CRC16 算法計(jì)算出來(lái)的。
- // MAX_SLOT 默認(rèn) 16384
- int result = CRC16.crc16(key.getBytes()) % MAX_SLOT;
這塊計(jì)算一個(gè) slot 到底有什么用呢?
繼續(xù)追蹤!
BaseRedisBatchExecutor#addBatchCommandData 在這里會(huì)從 source 里面獲取到 solt,然后獲得 MasterSlaveEntry。
大概可以理解為這里是獲取到這個(gè) Redis key 對(duì)應(yīng)的節(jié)點(diǎn)。
可重入
既然是可重入鎖,這塊是支持可重入的,來(lái)看下可重入是如何保證的。
- exists 命令判斷 redis key field 是否存在;
- 存在 則通過(guò) hincrby 命令對(duì) key 的 field 對(duì)應(yīng) value 自增;
- 為當(dāng)前 redis key 設(shè)置過(guò)期時(shí)間。
加鎖互斥
上面已經(jīng)驗(yàn)證了兩種情況:
- redis key 不存在;
- redis key 和 key 的 field 存在。
剩下的情況就是 key 存在的情況下,但是 field 不存在。
要知道 key 的 field 放的是 UUID:ThreadId,說(shuō)明加鎖的不是當(dāng)前線程。這時(shí)候直接返回當(dāng)前鎖的剩余時(shí)間。
3、總結(jié)
本文主要介紹了 Redisson 可重入鎖的加鎖、鎖重入、鎖互斥邏輯。
核心重點(diǎn)在 lua 腳本。同時(shí)需要理解 Redis 的 Hash 數(shù)據(jù)結(jié)構(gòu)。
同時(shí)需要記住,在未指定加鎖時(shí)間時(shí),默認(rèn)使用的是 30s。
最后,一張圖介紹本文加鎖邏輯。