開心一刻
今天,我的又一個好哥們脫單了,只剩下我自己單身了
我向一個我喜歡的女生吐苦水
我:我這輩子是找不到女朋友了
她:怎麼可能,你很優秀的,會有很多女孩子願意當你女朋友的
我內心竊喜,問道:那你願意當我女朋友嗎
她:我都在開導你了,你不要恩將仇報!
線上問題
生產環境突然告警,告警資訊:
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 id
和 thread-id
是什麼
關於 thread-id
,我相信大家都理解,就是拋異常的執行緒的 id,沒問題吧?那 node id
呢?
我用八股文引導下你們
問:
redisson
用的redis
的什麼資料型別來實現鎖的答:
hash
問:那
hash
中的key
、field
、value
的值分別是什麼答:
key
的值是鎖名,field
的值是執行緒id
,value
的值是重入次數問:如果多個服務同時去獲取一把鎖,
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
這程式碼,我相信大家都能看懂,但我還是說明下
- 構造鎖
- 嘗試獲取鎖,等待時間1s,持鎖3s
- 如果獲取到鎖,則進行業務處理,沒獲取到鎖,則列印
鎖獲取失敗
finally
保證異常和非異常情況下,鎖都能釋放
是不是很正常,但真的沒 bug
嗎
我們調整下程式碼
執行 multiThreadLock
,異常就來了
從列印資訊,我們應該能分析出問題出在哪
- 執行緒52獲取到鎖,執行業務中
- 執行緒53嘗試獲取鎖,但鎖被執行緒52持有
- 執行緒53 1s內獲取鎖失敗
- 執行緒53 來到
finally
,判斷鎖是否被持有,發現是被持有的,釋放鎖redisson
釋放鎖的時候,發現鎖的持有執行緒並非當前執行緒,丟擲異常
執行緒53,你怎麼回事,怎麼能釋放別人的鎖?可不能怪執行緒53,程式碼可是我們寫的,看看提交記錄,非得把這個二臂揪出來!!!
算了算了,還是別揪了,我們繼續看如何修復
問題修復
既然找到問題了,修復問題就很簡單了,方式有以下幾種
提高等待時長
將獲取鎖的等待時長提高,但這種方式只能減少異常,並不是完全修復異常;因為會有多個執行緒同時競爭鎖,等待時長設定成多少都不合適,除非設定成不超時,但是設定成不超時,可能會導致等待的執行緒太多,造成執行緒不夠用的情況。不推薦該方式
自動釋放
去掉 finally
,相當於把產生異常的源頭給幹掉了,那肯定就不會有異常了嘛,這不就是我們常提到的
解決不了問題,那就把提出問題的人解決掉
不主動釋放鎖,讓鎖自動到期釋放,因為我們設定了鎖持有時長是 3s,3s 後就自動到期釋放了。但在實際業務中,我們往往會把鎖持有時長設定的比較大(遠大於業務執行的平均時長),保證業務不會併發執行,如果業務執行完了不主動釋放鎖,就會導致很長時間內鎖被無效佔用,後面的執行緒獲取鎖也只能白白等待。不推薦該方式
記錄獲取狀態
直接看程式碼,你們就懂了
如果業務執行時間超過 3s,會怎麼樣,我們把睡眠時間改成 5s,執行下 testLock
,你會發現同樣的異常又出現了!!!
我們來分析下,鎖持有時長是 3s,而業務執行時長是 5s,也就說業務還沒執行完,鎖已到期,redis
自動釋放了,業務執行完之後我們再去釋放鎖,鎖都沒了,怎麼釋放?所以 redisson
丟擲異常了;所以釋放鎖的時候,還需要加一個條件
if (acquired && lock.isLocked())
acquired
表示當前執行緒是否獲取到鎖了,而 lock.isLocked()
表示是否有執行緒持有鎖,如果都為 true
,那就說明是當前執行緒持有鎖,釋放就沒問題了。可以用,但不推薦,因為有更優雅的處理方式
判斷持有者
這種寫法更優雅
就直接判斷鎖是不是當前執行緒持有,是就可以釋放;就不用去管鎖是別的執行緒持有,還是到期自動釋放了。推薦該方式
總結
- 示例程式碼地址:redisson-spring-boot-demo
- 加鎖的目的就是為了保證業務單執行緒執行,所以鎖的持有時長一定要設定大一點,不然極端情況下,業務還在執行中,鎖卻到期了,就違背了加鎖的初衷(設定不過期,Redisson會啟用看門狗來續期,能夠很好的解決業務還在執行中,鎖卻到期的問題)
- 鎖一定要主動釋放、一定要主動釋放、一定要主動釋放,與業務無關
- 釋放鎖的時候,要判斷是否是當前執行緒持有,都不是你的鎖,你憑什麼釋放