Redisson(2-2)分佈式鎖實現對比 VS Java的ReentrantLock之帶超時時間的tryLock

Redisson實現了一整套JDK中ReentrantLock的功能,這裏對比一下實現的差異和核心的思想。

unfair模式的帶超時時間的tryLock(超時時間)

ReentrantLock

這裏上來會直接先試下能不能try成功,如果不成功,進入等待並開始競爭等邏輯。

整個鎖的核心是通過LockSupport的park方法來實現的,這是調用底層UNSAFE的park方法來實現的。如果在被等待獲取的鎖釋放的時候,該線程會重新被喚醒,然後和其它線程一起競爭,如果沒有競爭成功,那麼繼續park,循環往復,直到獲取到鎖或者等待超時(這裏park的時間是當前要獲取鎖的線程等待獲取鎖的剩餘時間,當前線程要麼因爲鎖釋放被喚醒,要麼等到超時)。

這裏有個小細節是,如果剩餘的等待時間小於spinForTimeoutThreshold這個值,那麼就不會再park然後等待喚醒了。這是爲了防止park然後喚醒這種線程調度操作,因爲剩下的等待時間已經不多,即使一直自旋也不會消耗太多的CPU性能,反而比頻繁掛起釋放要節省性能,這是一個折衷處理。

RedissonLock

RedissonLock沒有LockSupport這種底層級別的工具來幫忙,因爲它是分佈式的,所以它也需要藉助這樣一個東西,來實現類似的功能。

我們一般在自己用Redis實現分佈式鎖的時候,經常設計的操作是輪詢Redis去查詢該鎖的狀態,輪詢之間會設置一個休眠時間,休眠時間越短,當鎖釋放的時候響應的就越及時,但是對Redis的壓力就越大,因爲你單位時間內輪詢Redis的次數會增加,所以這是一個痛點。那麼如果我們用通知來代替輪詢,是不是就可以仿照ReentrantLock那樣,通過喚醒操作(藉助通知)來喚醒本地的sleep操作,這樣就不必定時輪詢檢查狀態了。

而這個功能就要利用Redis的訂閱功能,下面看代碼:

這裏和ReentrantLock一樣,先try一下,如果獲得了鎖就返回,如果沒有獲得,看下是否已經超過等待獲取鎖設置的超時時間,如果已經超時,獲取鎖失敗,否則,就要進入等待競爭的過程。

競爭的過程中,第一個操作是subcribe,即上面我們提到的訂閱功能。訂閱的超時時間即當前要獲取鎖的線程的剩餘等待時間,如果在這個時間範圍內沒有響應,訂閱失敗。

訂閱的相關操作可以參考:https://my.oschina.net/u/2313177/blog/1925237

“訂閱監聽Redis消息,並且創建RedissonLockEntry,其中RedissonLockEntry中比較關鍵的是一個Semaphore屬性對象,用來控制本地的鎖請求的信號量同步,返回的是netty框架的Future實現”。

如果這個await成功了,代表在等待鎖的超時時間之內鎖就被釋放了,接下來要進入競態部分了。

這裏我理解有兩層維度,一層是應用維度,不同的虛擬機之間通過訂閱方式競爭鎖,某一臺業務服務器的java虛擬機會最終成功訂閱到這個鎖。二層是虛擬機內的線程維度,這裏機器內的競爭通過一個共享鎖來減少對Redis的壓力,因爲當前虛擬機的訂閱下面掛着多個競爭該鎖的不同線程,這些線程中也只有一個會成功獲得共享鎖,繼而再去競爭真正的鎖。沒有獲得共享鎖的線程睡眠一段時間,或者被喚醒繼續獲取鎖,或者超時,獲取鎖失敗,而只有獲得共享鎖成功的線程纔有機會和其它虛擬機中同樣獲取共享鎖的線程共同競爭真正的鎖。

所以同虛擬機內要競爭真正鎖的所有線程先競爭一個共享鎖(Semaphore),成功的線程才獲取機會和其它虛擬機內同樣獲取共享鎖(Semaphore)的線程競爭真正的鎖——跨虛擬機的鎖競爭,即分佈式鎖競爭,這裏的關鍵是共享鎖降低了靜態,同時又利用訂閱機制(通知機制)解決了睡眠時間無法合理設置的問題。

其實https://www.cnblogs.com/ASPNET2008/p/6385249.html文章也說到了這個問題:

所以,如果訂閱成功,說明當前機器在虛擬機層面首先搶佔到了資源,然後再在當前虛擬機內進行不同線程間的競爭。

像上面說的那樣,這裏藉助信號量來替代了ReentrantLock中的LockSupport的park功能,“通過信號量(共享鎖)阻塞,等待解鎖消息(這一點設計的非常精妙:減少了其他分佈式節點的等待或者空轉等無效鎖申請的操作,整體提高了性能)。如果剩餘時間(ttl)小於wait time ,就在 ttl 時間內,從Entry的信號量獲取一個許可(除非被中斷或者一直沒有可用的許可)。否則就在wait time時間範圍內等待”。

這裏的共享鎖和上面subscribe的是同一把鎖。如果本虛擬機的某個線程獲取鎖之後,當它釋放鎖的時候,會發布一條取消訂閱的消息(這個後面具體會說),這樣其它虛擬機纔能有平等的機會再次和本虛擬機的其他線程競爭鎖,而不是一直在本虛擬機的範圍內進行競爭,那樣其他虛擬機就會一直處於飢餓狀態。

那麼什麼時候這個訂閱的消息會解除當前線程的休眠操作呢?和subscribe對應的當然就是publish了,當執獲得鎖的線程進行解鎖操作的時候(解鎖後面會單獨說),在lua中會執行‘publish’操作,而publish的消息類型是LockPubSub.unlockMessage,這個消息會觸發訂閱消息的共享鎖(Semaphore)的喚醒操作,然後發起對該鎖的新一輪競爭。

而最後無論獲取鎖成功與否,都要解除當前線程對該鎖的訂閱消息。

關於等待時間(Semaphore的休眠時間),上面ReentrantLock的park的時間是當前線程獲取鎖的等待剩餘時間。這裏本地等待鎖的睡眠時間略有不同,使用的是Redis中鎖的生命週期剩餘時間,當然在這個睡眠過程中,可能會因爲鎖釋放而喚醒,因爲有對當前鎖的訂閱操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章