記一次 Redisson 線上問題 → 你怎麼能釋放別人的鎖

青石路發表於2024-07-22

開心一刻

今天,我的又一個好哥們脫單了,只剩下我自己單身了

我向一個我喜歡的女生吐苦水

我:我這輩子是找不到女朋友了

她:怎麼可能,你很優秀的,會有很多女孩子願意當你女朋友的

我內心竊喜,問道:那你願意當我女朋友嗎

她:我都在開導你了,你不要恩將仇報!

不要恩將仇報

線上問題

生產環境突然告警,告警資訊:

attempt to unlock lock, not locked by current thread by node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52

檢視日誌,找到對應的堆疊資訊

Exception in thread "thread0" java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52
	at org.redisson.RedissonLock.lambda$unlockAsync$4(RedissonLock.java:616)
	at org.redisson.misc.RedissonPromise.lambda$onComplete$0(RedissonPromise.java:187)
	at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:578)
	at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:552)
	at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:491)
	at io.netty.util.concurrent.DefaultPromise.addListener(DefaultPromise.java:184)
	at org.redisson.misc.RedissonPromise.onComplete(RedissonPromise.java:181)
	at org.redisson.RedissonLock.unlockAsync(RedissonLock.java:607)
	at org.redisson.RedissonLock.unlock(RedissonLock.java:492)
	at com.qsl.ResissonTest.testLock(ResissonTest.java:41)
	at java.lang.Thread.run(Thread.java:748)

翻譯過來就是

企圖去釋放鎖,不被當前執行緒(node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52)鎖住

也就是:當前執行緒企圖去釋放別的執行緒的鎖

怎麼能釋放別人的鎖?

指指點點

基礎回顧

在排查問題之前,我們先弄清楚

node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52

node idthread-id 是什麼

關於 thread-id,我相信大家都理解,就是拋異常的執行緒的 id,沒問題吧?那 node id 呢?

我用八股文引導下你們

問:redisson 用的 redis 的什麼資料型別來實現鎖的

答:hash

問:那 hash 中的 keyfieldvalue 的值分別是什麼

答:key 的值是鎖名,field 的值是 執行緒idvalue 的值是重入次數

問:如果多個服務同時去獲取一把鎖,field 的值是不是有可能相同,比如服務A獲取鎖的執行緒的 thread-id 是 52,服務B獲取鎖的執行緒的的 thread-id 也是 52

此時你是不是有點慌了,但依舊嘴硬的回答:有可能相同

問:那沒問題嗎,A服務的執行緒(thread-id=52)拿到鎖後,正在執行業務處理,B服務的執行緒(thread-id=52)也能拿到鎖,這不是鎖了個寂寞?

答:呃...嗯...

很顯然漏了個細節,那就是 field,其值不是 執行緒id,而是 node id:thread-id,例如:b9df1975-5595-42eb-beae-bdc5d67bce49:52 ,而這個 node id 就是 redisson例項id,用以區分分散式下的 redisson 例項

Redisson 分散式鎖實現之原始碼篇 → 為什麼推薦用 Redisson 客戶端 有很詳細的介紹,值得你們看看

釋放別人的鎖

talk is sheap show me the code

testLock

這程式碼,我相信大家都能看懂,但我還是說明下

  1. 構造鎖
  2. 嘗試獲取鎖,等待時間1s,持鎖3s
  3. 如果獲取到鎖,則進行業務處理,沒獲取到鎖,則列印 鎖獲取失敗
  4. finally 保證異常和非異常情況下,鎖都能釋放

是不是很正常,但真的沒 bug

640 (15)

我們調整下程式碼

testLock_異常

執行 multiThreadLock,異常就來了

異常資訊

從列印資訊,我們應該能分析出問題出在哪

  1. 執行緒52獲取到鎖,執行業務中
  2. 執行緒53嘗試獲取鎖,但鎖被執行緒52持有
  3. 執行緒53 1s內獲取鎖失敗
  4. 執行緒53 來到 finally,判斷鎖是否被持有,發現是被持有的,釋放鎖
  5. redisson 釋放鎖的時候,發現鎖的持有執行緒並非當前執行緒,丟擲異常

執行緒53,你怎麼回事,怎麼能釋放別人的鎖?可不能怪執行緒53,程式碼可是我們寫的,看看提交記錄,非得把這個二臂揪出來!!!

哪個二臂寫的

算了算了,還是別揪了,我們繼續看如何修復

問題修復

既然找到問題了,修復問題就很簡單了,方式有以下幾種

提高等待時長

將獲取鎖的等待時長提高,但這種方式只能減少異常,並不是完全修復異常;因為會有多個執行緒同時競爭鎖,等待時長設定成多少都不合適,除非設定成不超時,但是設定成不超時,可能會導致等待的執行緒太多,造成執行緒不夠用的情況。不推薦該方式

自動釋放

去掉 finally,相當於把產生異常的源頭給幹掉了,那肯定就不會有異常了嘛,這不就是我們常提到的

解決不了問題,那就把提出問題的人解決掉

不主動釋放鎖,讓鎖自動到期釋放,因為我們設定了鎖持有時長是 3s,3s 後就自動到期釋放了。但在實際業務中,我們往往會把鎖持有時長設定的比較大(遠大於業務執行的平均時長),保證業務不會併發執行,如果業務執行完了不主動釋放鎖,就會導致很長時間內鎖被無效佔用,後面的執行緒獲取鎖也只能白白等待。不推薦該方式

記錄獲取狀態

直接看程式碼,你們就懂了

記錄獲取狀態_1

如果業務執行時間超過 3s,會怎麼樣,我們把睡眠時間改成 5s,執行下 testLock,你會發現同樣的異常又出現了!!!

記錄獲取狀態_異常

我們來分析下,鎖持有時長是 3s,而業務執行時長是 5s,也就說業務還沒執行完,鎖已到期,redis 自動釋放了,業務執行完之後我們再去釋放鎖,鎖都沒了,怎麼釋放?所以 redisson 丟擲異常了;所以釋放鎖的時候,還需要加一個條件

if (acquired && lock.isLocked())

acquired 表示當前執行緒是否獲取到鎖了,而 lock.isLocked() 表示是否有執行緒持有鎖,如果都為 true,那就說明是當前執行緒持有鎖,釋放就沒問題了。可以用,但不推薦,因為有更優雅的處理方式

判斷持有者

這種寫法更優雅

判斷持有者

就直接判斷鎖是不是當前執行緒持有,是就可以釋放;就不用去管鎖是別的執行緒持有,還是到期自動釋放了。推薦該方式

總結

  1. 示例程式碼地址:redisson-spring-boot-demo
  2. 加鎖的目的就是為了保證業務單執行緒執行,所以鎖的持有時長一定要設定大一點,不然極端情況下,業務還在執行中,鎖卻到期了,就違背了加鎖的初衷(設定不過期,Redisson會啟用看門狗來續期,能夠很好的解決業務還在執行中,鎖卻到期的問題)
  3. 鎖一定要主動釋放、一定要主動釋放、一定要主動釋放,與業務無關
  4. 釋放鎖的時候,要判斷是否是當前執行緒持有,都不是你的鎖,你憑什麼釋放

相關文章