Redisson分布式鎖學習總結:讀鎖 RedissonReadLock#unLock 釋放鎖源碼分析

2022-01-04 不送花的程序猿

收錄於話題 #分布式鎖 12個

一、RedissonReadLock#unlock 源碼分析

上一篇已經簡單介紹了,redisson 提供的讀寫鎖 RReadWriteLock 的使用demo、使用場景、和RedissonLock 的關係;也深入分析了讀鎖 RedissonReadLock 加鎖 lua 腳本的執行邏輯、watchdog 機制 lua 腳本的執行邏輯。

下面,我們將繼續分析讀鎖 RedissonReadLock 釋放鎖時,lua 腳本是怎麼執行的。

1、RedissonReadLock 釋放鎖 lua 腳本分析

分析前,我們定一下加鎖的key:

RReadWriteLock readWriteLock = client.getReadWriteLock("myLock");

RedissonReadLock#unlockInnerAsync:

@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);

return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
"if (lockExists == 0) then " +
"return nil;" +
"end; " +

"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
"if (counter == 0) then " +
"redis.call('hdel', KEYS[1], ARGV[2]); " +
"end;" +
"redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +

"if (redis.call('hlen', KEYS[1]) > 1) then " +
"local maxRemainTime = -3; " +
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
"if type(counter) == 'number' then " +
"for i=counter, 1, -1 do " +
"local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " +
"maxRemainTime = math.max(remainTime, maxRemainTime);" +
"end; " +
"end; " +
"end; " +

"if maxRemainTime > 0 then " +
"redis.call('pexpire', KEYS[1], maxRemainTime); " +
"return 0; " +
"end;" +

"if mode == 'write' then " +
"return 0;" +
"end; " +
"end; " +

"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; ",
Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix),
LockPubSub.UNLOCK_MESSAGE, getLockName(threadId));
}

我們可以看到,這讀鎖釋放鎖的lua腳本還是比較長的,但是我們也不用著急,一步一步分析就可以了。

1.1、KEYS

Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix):

getName(): 鎖key

getChannelName():prefixName("redisson_rwlock", getName()) -> redisson_rwlock:{myLock}

timeoutPrefix:getReadWriteTimeoutNamePrefix(threadId) -> suffixName(getName(), getLockName(threadId)) + ":rwlock_timeout" -> {myLock}:UUID-1:threadId-1:rwlock_timeout

keyPrefix:getKeyPrefix(threadId, timeoutPrefix) -> timeoutPrefix.split(":" + getLockName(threadId))[0] -> {myLock}

KEYS:["myLock","redisson_rwlock:{myLock}","{myLock}:UUID-1:threadId-1:rwlock_timeout","{myLock}"]

1.2、ARGVS

LockPubSub.UNLOCK_MESSAGE, getLockName(threadId)

ARGVS:[0L,"UUID:threadId"]

1.3、lua 腳本分析1、分支一:鎖模式不存在,往鎖對應的channel發送消息

場景:

如果鎖模式不存在,那麼證明沒有線程持有讀寫鎖

當前線程即使沒有持有鎖,但還是調用了釋放鎖的方法

lua腳本:

"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; "

分析:

利用 hget 命令獲取讀寫鎖的模式

hget myLock mode

如果鎖模式為空,往讀寫鎖對應的channel發送釋放鎖的消息,然後返回1,lua腳本執行完畢

publish redisson_rwlock:{myLock} 0

channel 發布鎖釋放消息的用處:

其實在 Redisson 提供的各種分布式鎖中,不管是可重入鎖、公平鎖,還是到現在的讀寫鎖,都會利用 redis 的pub/sub機制來做下面的通知機制。

在線程獲取鎖失敗的時候,在等待前會先訂閱鎖對應的 channel,然後進入等待狀態。

如果當前線程成功釋放鎖,那麼會在鎖對應的 channel 發布釋放鎖的消息;假設此時有其他線程在等待獲取鎖,那麼就會接收到 channel 裡釋放鎖的消息,提前跳出等待狀態,去獲取鎖。

關於鎖channel,要注意的是:可重入鎖和讀寫鎖,等待線程都是訂閱同一個channel;而公平鎖不是,公平鎖是每個等待線程都訂閱自己指定的channel,從而做到公平。

2、分支二:鎖存在,但當前線程沒有持有鎖

場景:

lua腳本:

"local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
"if (lockExists == 0) then " +
"return nil;" +
"end; "

分析:

利用 hexists 命令判斷當前線程是否持有鎖

hexists myLock UUID-1:threadId-1

如果不存在直接返回null,表示釋放鎖失敗

3、分支三:鎖存在且當前線程持有鎖

場景:

lua腳本:

"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " + 
"if (counter == 0) then " +
"redis.call('hdel', KEYS[1], ARGV[2]); " +
"end;" +
"redis.call('del', KEYS[3] .. ':' .. (counter+1)); "

分析: 分析前,我們假設線程 UUID-1:threadId-1 持有兩個讀鎖,那麼 redis 中鎖相關數據如下:

myLock:{
"mode":"read",
"UUID-1:threadId-1":2
}
{myLock}:UUID-1:threadId-1:rwlock_timeout:1 1
{myLock}:UUID-1:threadId-1:rwlock_timeout:2 1

利用 hincrby 命令,給當前線程持有鎖數量減1

hincrby myLock UUID-1:threadId-1 -1

如果持有鎖數量減1後等於0,證明當前線程不再持有鎖,那麼利用 hdel 命令將鎖map中加鎖次數記錄刪掉

hdel myLock UUID:threadId

刪除線程持有鎖對應的加鎖超時記錄

del {myLock}:UUID-1:threadId-1:rwlock_timeout:count+1

分析後,由於我們假設了當前線程持有鎖兩次,所以只會執行步驟1和步驟3,執行後redis 中鎖相關數據如下:

myLock:{
"mode":"read",
"UUID-1:threadId-1":1
}
{myLock}:UUID-1:threadId-1:rwlock_timeout:1 1

到這裡,我們可能第一時間會有點疑惑:為什麼給讀鎖扣減不需要先判斷鎖的模式?

其實在前一篇文章中,我們就提及到兩點:

在鎖map中記錄加鎖次數時,讀鎖的key是UUID:threadId,即客戶端ID:線程ID;而寫鎖的key是UUID:threadId:write,即客戶端ID:線程ID:write,那麼就是說讀鎖的key和寫鎖的key是不一樣的。所以解鎖的時候,直接使用對應key來扣減持有鎖次數即可。

還有一點很重要的是,相同線程,如果獲取了寫鎖後,還是可以繼續獲取讀鎖的。所以只需要判斷鎖map有讀鎖加鎖次數記錄即可,就可以判斷當前線程是持有讀鎖的,並不需要關心當前鎖的模式。

4、分支四:給當前鎖刷新過期時間

場景:

lua腳本:

"if (redis.call('hlen', KEYS[1]) > 1) then " +
"local maxRemainTime = -3; " +
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
"if type(counter) == 'number' then " +
"for i=counter, 1, -1 do " +
"local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " +
"maxRemainTime = math.max(remainTime, maxRemainTime);" +
"end; " +
"end; " +
"end; " +

"if maxRemainTime > 0 then " +
"redis.call('pexpire', KEYS[1], maxRemainTime); " +
"return 0; " +
"end;" +

"if mode == 'write' then " +
"return 0;" +
"end; " +
"end; "

分析:

利用 hlen 獲取鎖map中key的數量

hlen myLock

如果鎖map中 key 的數量還是大於1,那麼證明還有線程持有鎖,遍歷鎖map集合中的加鎖次數key,根據加鎖超時記錄獲取最大的超時時間

hkeys myLock

拼接 key 對應的加鎖記錄對應的超時時間,利用 pttl 獲取超時時間

與 maxRemainTime 對比,獲取當前最大的超時時間,賦值給 maxRemainTime

利用 hget 命令獲取key對應的加鎖次數

遍歷步驟2獲取到的keys

hget myLock key

遍歷加鎖次數

pttl {myLock}:UUID-1:threadId-1:rwlock_timeout:num -> remainTime

maxRemainTime = math.max(remainTime, maxRemainTime)

設置 maxRemainTime 為 -3

利用 hkeys 命令獲取鎖map中所有key

判斷 maxRemainTime 是否大於0,如果大於0,給鎖重新設置過期時間為 maxRemainTime,然後返回0結束lua腳本的執行

pexpire myLock maxRemainTime

這裡我們會思考一個問題,為什麼鎖map中的key都大於1了,證明肯定還有線程持有鎖,那為什麼還會存在 maxRemainTime 最後小於0的情況呢?

有一個點我們還沒學到,那就是其實讀寫鎖中,如果是獲取寫鎖,並不會新增一條寫鎖的超時記錄,因為讀寫鎖中,寫鎖和寫鎖是互斥的,寫鎖和讀鎖也是互斥的,即使支持當前線程先獲取寫鎖再獲取讀鎖,其實也不需要增加一條寫鎖的超時時間,因為讀寫鎖 key 的超時時間就等於寫鎖的超時時間。

如果當前讀寫鎖的鎖模式是寫鎖,直接返回0結束lua腳本的執行

5、最後操作

場景:

lua腳本:

"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "

分析:

利用 del 命令刪除讀寫鎖對應的 key

del myLock

往讀寫鎖對應的channel發布鎖釋放消息

publish redisson_rwlock:{myLock} 0

二、最後

到此,關於讀寫鎖 RReadWriteLock 中讀鎖 RedissonReadLock 釋放鎖的原理已經分析完了,至於釋放鎖後續的停止 watchdog 的執行等操作,還是和 RedissLock 保持一致,我們就不再分析了。

同時我們可以發現,其實分析讀鎖時很多地方還是需要同時知道寫鎖的部分原理,我們這裡只是提前透露了一些,用於支撐整個讀鎖釋放鎖到原理分析。不過接下來,也將會對寫鎖 RedissonWriteLock 加鎖和釋放鎖的原理進行分析,在梳理過程中,也會帶上一定的場景來分析,儘量做到完全理解為啥 Redisson 要這麼做~

相關焦點

  • Redisson分布式鎖學習總結:寫鎖 RedissonWriteLock#unLock 釋放鎖源碼分析
    釋放鎖的源碼,我們這裡也是不再做分析了,因為 RedissonWriteLock 也是基於 RedissonLock 做的擴展,所以釋放鎖的源碼也是和 RedissonLock 保持一致的,我們這裡只需要分析 lua 腳本是如何執行即可。
  • redisson分布式讀寫鎖-讀鎖watchDog原理
    redisson分布式鎖文章回顧redisson分布式可重入鎖加鎖原理
  • Redisson 如何實現分布式鎖
    針對項目中使用的分布式鎖進行簡單的示例配置以及源碼解析,並列舉源碼中使用到的一些基礎知識點,但是沒有對redisson中使用到的netty知識進行解析
  • Redisson 分布式鎖實戰與watch dog機制解讀
    :互斥:在分布式高並發的條件下,需要保證,同一時刻只能有一個線程獲得鎖,這是最最基本的一點。防止死鎖:在分布式高並發的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無法獲得鎖,造成死鎖。可重入:我們知道ReentrantLock是可重入鎖,那它的特點就是同一個線程可以重複拿到同一個資源的鎖。
  • 基於Redisson分布式鎖解決秒殺系統的「超賣」問題
    Redis基礎上的一款綜合中間件,除了擁有Redis本身提供的強大功能外,還提供了諸如分布式鎖、分布式服務、延遲隊列、遠程調用等強大的功能。:@Autowiredprivate RedissonClient redissonClient;//秒殺核心業務邏輯的處理-redisson分布式鎖@Overridepublic Boolean killItem (Integer killId, Integer userId) throws Exception {    Boolean
  • Redisson分布式鎖學習總結:寫鎖 RedissonWriteLock#lock 獲取鎖源碼分析
    收錄於話題 #分布式鎖+ ARGV[1]); " + "return nil; " + "end; " + "end;" + "return redis.call
  • 分布式鎖中的王者方案 - Redisson
    分布式鎖:Redisson還實現了Redis文檔中提到像分布式鎖Lock這樣的更高階應用場景。("Finally,釋放鎖成功。由此可以得出結論,Redisson 的可重入鎖(lock)是阻塞其他線程的,需要等待其他線程釋放的。3.1.2 驗證二:服務停了,鎖會釋放嗎?如果線程 A 在等待的過程中,服務突然停了,那麼鎖會釋放嗎?如果不釋放的話,就會成為死鎖,阻塞了其他線程獲取鎖。
  • 實戰:Redis集群環境下的-RedLock(真分布式鎖)
    為什麼基於故障切換的方案不夠好為了理解我們想要提高的到底是什麼,我們先看下當前大多數基於Redis的分布式鎖三方庫的現狀。 用Redis來實現分布式鎖最簡單的方式就是在實例裡創建一個鍵值,創建出來的鍵值一般都是有一個超時時間的(這個是Redis自帶的超時特性),所以每個鎖最終都會釋放。而當一個客戶端想要釋放鎖時,它只需要刪除這個鍵值即可。
  • Redisson實現Redis分布式鎖的N種姿勢
    Redis幾種架構Redis發展到現在,幾種常見的部署架構有:單機模式;主從模式;哨兵模式;集群模式;我們首先基於這些架構講解Redisson普通分布式鎖實現,需要注意的是,只有充分了解普通分布式鎖是如何實現的,才能更好的了解Redlock分布式鎖的實現,因為Redlock分布式鎖的實現完全基於普通分布式鎖
  • 分布式鎖(Redisson)-從零開始,深入理解與不斷優化
    作者:大程子的技術成長路連結:https://www.jianshu.com/p/bc4ff4694cf3分布式鎖場景案例1如下代碼模擬了下單減庫存的場景,我們分析下在高並發場景下會存在什麼問題package com.wangcp.redisson;import org.springframework.beans.factory.annotation.Autowired
  • 分布式鎖解決方案-Redis
    ## 為什麼要學習分布式鎖解決方案為了解決分布式架構帶來的數據準確性問題!我們用synchronized或者 ReentrantLock(瑞恩吹特) 能解決問題嗎?真實生產環境我們採用集群的方式去訪問秒殺商品(nginx為我們做了負載均衡)。
  • 造了一個 Redis 分布鎖的輪子,沒想到還學到這麼多東西!!!
    那就先寫下最近在鼓搗一個東西,使用 Redis 實現可重入分布鎖。看到這裡,有的朋友可能會提出來使用 redisson 不香嗎,為什麼還要自己實現?哎,redisson 真的很香,但是現有項目中沒辦法使用,只好自己手擼一個可重入的分布式鎖了。
  • 一文掌握 Redisson 分布式鎖原理(值得收藏)
    「了解到這裡就差不多了, 就不向下擴展了, 想要了解詳細用途的, 翻一下上面的目錄Redisson 重入鎖由於 Redisson 太過於複雜, 設計的 API 調用大多用 Netty 相關, 所以這裡只對 如何加鎖、如何實現重入鎖進行分析以及如何鎖續時進行分析創建鎖
  • Redisson 分布式鎖詳解與可視化監控方案
    點擊上方「陶陶技術筆記」關注我回復「資料」獲取作者整理的大量學習資料!
  • 一文弄懂最複雜並發工具類讀寫鎖源碼
    前面幾篇文章分析了AQS下實現類的使用,今天講最後一個也是最複雜的一個ReentrantReadWriteLock。而ReentrantReadWriteLock讀寫鎖這個類支持多個讀鎖同時訪問,當一個線程在持有寫鎖的時候,其他所有線程都不能訪問,寫鎖釋放後所有線程又能同時訪問。
  • Qt的讀寫鎖QReadWriteLock要怎麼玩?
    QReadWriteLock從名字看就知道是讀寫鎖的意思。和QMutex一樣,QReadWriteLock也是線程同步的一種工具。那麼它有什麼用呢?和QMutex又有什麼區別呢?寫個例子瞧一瞧。在寫例子前,先看看要用到的函數:lockForRead、lockForWrite和unlock。比QMutex的例子多一個,從名字上可以看得出來是把lock分為了readlock和writelock。unlock和QMutex裡的是一樣的,有lock就要unlock。
  • Redlock分布式鎖
    這篇文章主要是對 Redis 官方網站刊登的 Distributed locks with Redis 部分內容的總結和翻譯。
  • Java都為我們提供了各種鎖,為什麼還需要分布式鎖?
    這篇文章主要是針對為什麼需要使用分布式鎖這個話題來展開討論的。前一段時間在群裡有個兄弟問,既然分布式鎖能解決大部分生產問題,那麼java為我們提供的那些鎖有什麼用呢?直接使用分布式鎖不就結了嘛。針對這個問題我想了很多,一開始是在網上找找看看有沒有類似的回答。後來想了想。想要解決這個問題,還需要從本質上來分析。OK,開始上車出發。
  • 帶你研究Redis分布式鎖,源碼走起
    它不僅提供了一系列的分布式的Java常用對象,還實現了可重入鎖(Reentrant Lock)、公平鎖(Fair Lock、聯鎖(MultiLock)、 紅鎖(RedLock)、 讀寫鎖(ReadWriteLock)等,還提供了許多分布式服務。Redisson提供了使用Redis的最簡單和最便捷的方法。