Redis分散式鎖實戰
什麼是分散式鎖
在單機部署的情況下,要想保證特定業務在順序執行,通過JDK提供的synchronized關鍵字、Semaphore、ReentrantLock,或者我們也可以基於AQS定製化鎖。單機部署的情況下,鎖是在多執行緒之間共享的,但是分散式部署的情況下,鎖是多程序之間共享的。那麼分散式鎖要保證鎖資源的唯一性,可以在多程序之間共享。
分散式鎖特性
- 保證同一個方法在某一時刻只能在一臺機器裡一個程序中一個執行緒執行;
- 要保證是可重入鎖(避免死鎖);
- 要保證獲取鎖和釋放鎖的高可用;
分散式鎖實現
- 鎖釋放(finally);
- 鎖超時設定;
- 鎖重新整理(定時任務,每2/3的鎖生命週期執行);
- 如果鎖超時了,防止刪除其他執行緒的鎖(其他執行緒會拿到鎖),考慮 value值用執行緒id標識,當前執行緒釋放鎖的時候要判斷是否為當前執行緒的執行緒id;
- 可重入;
Redis分散式鎖
RedisLockRegistry
RedisLockRegistry是spring-integration-redis中提供redis分散式鎖實現類。主要是通過redis鎖+本地鎖雙重鎖的方式實現的一個比較好的鎖。
OBTAIN_LOCK_SCRIPT是一個上鎖的lua指令碼。KEYS[1]代表當前鎖的key值,ARGV[1]代表當前的客戶端標識,ARGV[2]代表過期時間。
基本邏輯是:根據KEYS[1]從redis中拿到對應的客戶端標識,如已存在的客戶端標識和ARGV[1]相等,那麼重置過期時間為ARGV[2];如果值不存在,設定KEYS[1]對應的值為ARGV[1],並且過期時間是ARGV[2]。
獲取鎖的過程也很簡單,首先通過本地鎖(localLock,對應的是ReentrantLock例項)獲取鎖,然後通過RedisTemplate執行OBTAIN_LOCK_SCRIPT指令碼獲取redis鎖。
為什麼要使用本地鎖呢,首先是為了鎖的可重入,其次是減輕redis服務壓力。
釋放鎖的過程也比較簡單,第一步通過本地鎖判斷當前執行緒是否持有鎖,第二步通過本地鎖判斷當前執行緒持有鎖的計數。
如果當前執行緒持有鎖的計數 > 1,說明本地鎖被當前執行緒多次獲取,這時只釋放本地鎖(釋放之後當前執行緒持有鎖的計數-1)。
如果當前執行緒持有鎖的計數 = 1,釋放本地鎖和redis鎖。
RedisLockRegistry使用如上所示。
首先定義RedisLockRegistry對應的Bean,需要依賴redis的ConnectionFactory。
然後在服務層中注入RedisLockRegistry例項。
通過lock方法和unlock方法將業務邏輯包起來,需要注意的是unlock方法要寫在finally程式碼塊中。
Redisson
Redisson是架設在Redis基礎上的一個Java駐記憶體資料網格(In-Memory Data Grid)。
充分的利用了Redis鍵值資料庫提供的一系列優勢,基於Java實用工具包中常用介面,為使用者提供了一系列具有分散式特性的常用工具類。
使得原本作為協調單機多執行緒併發程式的工具包獲得了協調分散式多機多執行緒併發系統的能力,大大降低了設計和研發大規模分散式系統的難度。
同時結合各富特色的分散式服務,更進一步簡化了分散式環境中程式相互之間的協作。
首先感受一下通過Redisson Api使用redis分散式鎖。
定義RedissonBuilder,通過redis叢集地址構建RedissonClient。
定義RedissonClient型別的Bean。
業務程式碼裡,通過RedissonClient獲取分散式鎖。
由於對Redisson分散式鎖實現原理了解的也不是很透徹,這裡推薦一篇文章:Redisson 分散式鎖實現分析。
Redisson和RedisLockRegistry對比
- RedisLockRegistry通過本地鎖(ReentrantLock)和redis鎖,雙重鎖實現,Redission通過Netty Future機制、Semaphore (jdk訊號量)、redis鎖實現。
- RedisLockRegistry和Redssion都是實現的可重入鎖。
- RedisLockRegistry對鎖的重新整理沒有處理,Redisson通過Netty的TimerTask、Timeout 工具完成鎖的定期重新整理任務。
- RedisLockRegistry僅僅是實現了分散式鎖,而Redisson處理分散式鎖,還提供了了佇列、集合、列表等豐富的API。
動手實現分散式鎖
實現原理
本地鎖(ReentrantLock)+ redis鎖
獲取鎖lua指令碼
鎖重新整理lua指令碼
鎖釋放lua指令碼
本地鎖定義
每一個lock key對應唯一的一個本地鎖
執行緒標識定義
分散式環境下,每一個執行緒對應一個唯一標識
鎖重新整理定時任務定義
通過JDK ConcurrentTaskScheduler完成定時任務執行,ScheduledFuture完成定時任務銷燬。其中taskId對應執行緒標識。
定義分散式鎖註解
分散式鎖切面
通過RedisLock註解例項lockInfo獲取到鎖key值、鎖過期時間資訊。
獲取鎖過程
- 通過lockInfo.key()方法獲取到鎖key值,通過鎖key值拿到對應的本地鎖(ReentrantLock)
- 本地鎖獲取鎖物件
- 進入獲取redis鎖的迴圈
- 通過快取服務元件執行獲取鎖的lua指令碼
- 如果獲取到redis鎖,判斷當前執行緒是否第一次獲取到鎖並且開啟了鎖重新整理,相應的註冊鎖重新整理定時任務
- 如果沒有獲取到redis鎖,休眠lockInfo.sleep()毫秒的時間,再次重試
釋放鎖過程
- 獲取到當前鎖key值對應的本地鎖
- 判斷當前執行緒是否為本地鎖鎖的持有者
- 如果本地鎖的重入次數大於1,則只釋放本地鎖
- 如果本地鎖的重入次數等於1,釋放本地鎖和redis鎖
分散式鎖測試
定義測試類,測試方法註上@RedisLock註解,制定鎖的key值為 "redis-lock-test",測試方法內隨機休眠。
開啟20個執行緒,同時呼叫測試方法。
多執行緒redis分散式鎖測試結果如下。
定義可重入測試類,方法內獲取當前代理物件,遞迴呼叫測試方法。
測試方法中,呼叫可重入測試類注有@RedisLock的測試方法。
分散式鎖可重入測試結果如下。
&n