Redis中是如何實現分散式鎖的?
- 2019 年 10 月 25 日
- 筆記
分散式鎖常見的三種實現方式:
-
資料庫樂觀鎖;
-
基於Redis的分散式鎖;
-
基於ZooKeeper的分散式鎖。
本地面試考點是,你對Redis使用熟悉嗎?Redis中是如何實現分散式鎖的。
要點
Redis要實現分散式鎖,以下條件應該得到滿足
互斥性
-
在任意時刻,只有一個客戶端能持有鎖。
不能死鎖
-
客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
容錯性
-
只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
實現
可以直接通過 set key value px milliseconds nx
命令實現加鎖, 通過Lua腳本實現解鎖。
//獲取鎖(unique_value可以是UUID等) SET resource_name unique_value NX PX 30000 //釋放鎖(lua腳本中,一定要比較value,防止誤解鎖) if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
程式碼解釋
-
set 命令要用
set key value px milliseconds nx
,替代setnx + expire
需要分兩次執行命令的方式,保證了原子性, -
value 要具有唯一性,可以使用
UUID.randomUUID().toString()
方法生成,用來標識這把鎖是屬於哪個請求加的,在解鎖的時候就可以有依據; -
釋放鎖時要驗證 value 值,防止誤解鎖;
-
通過 Lua 腳本來避免 Check And Set 模型的並發問題,因為在釋放鎖的時候因為涉及到多個Redis操作 (利用了eval命令執行Lua腳本的原子性);
加鎖程式碼分析
首先,set()加入了NX參數,可以保證如果已有key存在,則函數不會調用成功,也就是只有一個客戶端能持有鎖,滿足互斥性。其次,由於我們對鎖設置了過期時間,即使鎖的持有者後續發生崩潰而沒有解鎖,鎖也會因為到了過期時間而自動解鎖(即key被刪除),不會發生死鎖。最後,因為我們將value賦值為requestId,用來標識這把鎖是屬於哪個請求加的,那麼在客戶端在解鎖的時候就可以進行校驗是否是同一個客戶端。
解鎖程式碼分析
將Lua程式碼傳到jedis.eval()方法里,並使參數KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。在執行的時候,首先會獲取鎖對應的value值,檢查是否與requestId相等,如果相等則解鎖(刪除key)。
存在的風險
如果存儲鎖對應key的那個節點掛了的話,就可能存在丟失鎖的風險,導致出現多個客戶端持有鎖的情況,這樣就不能實現資源的獨享了。
-
客戶端A從master獲取到鎖
-
在master將鎖同步到slave之前,master宕掉了(Redis的主從同步通常是非同步的)。
主從切換,slave節點被晉級為master節點 -
客戶端B取得了同一個資源被客戶端A已經獲取到的另外一個鎖。導致存在同一時刻存不止一個執行緒獲取到鎖的情況。
redlock演算法出現
這個場景是假設有一個 redis cluster,有 5 個 redis master 實例。然後執行如下步驟獲取一把鎖:
-
獲取當前時間戳,單位是毫秒;
-
跟上面類似,輪流嘗試在每個 master 節點上創建鎖,過期時間較短,一般就幾十毫秒;
-
嘗試在大多數節點上建立一個鎖,比如 5 個節點就要求是 3 個節點 n / 2 + 1;
-
客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了;
-
要是鎖建立失敗了,那麼就依次之前建立過的鎖刪除;
-
只要別人建立了一把分散式鎖,你就得不斷輪詢去嘗試獲取鎖。
Redis 官方給出了以上兩種基於 Redis 實現分散式鎖的方法,詳細說明可以查看:
https://redis.io/topics/distlock 。
Redisson實現
Redisson是一個在Redis的基礎上實現的Java駐記憶體數據網格(In-Memory Data Grid)。它不僅提供了一系列的分散式的Java常用對象,還實現了可重入鎖(Reentrant Lock)、公平鎖(Fair Lock、聯鎖(MultiLock)、 紅鎖(RedLock)、 讀寫鎖(ReadWriteLock)等,還提供了許多分散式服務。
Redisson提供了使用Redis的最簡單和最便捷的方法。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。
Redisson 分散式重入鎖用法
Redisson 支援單點模式、主從模式、哨兵模式、集群模式,這裡以單點模式為例:
// 1.構造redisson實現分散式鎖必要的Config Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0); // 2.構造RedissonClient RedissonClient redissonClient = Redisson.create(config); // 3.獲取鎖對象實例(無法保證是按執行緒的順序獲取到) RLock rLock = redissonClient.getLock(lockKey); try { /** * 4.嘗試獲取鎖 * waitTimeout 嘗試獲取鎖的最大等待時間,超過這個值,則認為獲取鎖失敗 * leaseTime 鎖的持有時間,超過這個時間鎖會自動失效(值應設置為大於業務處理的時間,確保在鎖有效期內業務能處理完) */ boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS); if (res) { //成功獲得鎖,在這裡處理業務 } } catch (Exception e) { throw new RuntimeException("aquire lock fail"); }finally{ //無論如何, 最後都要解鎖 rLock.unlock(); }
加鎖流程圖
解鎖流程圖
我們可以看到,RedissonLock是可重入的,並且考慮了失敗重試,可以設置鎖的最大等待時間, 在實現上也做了一些優化,減少了無效的鎖申請,提升了資源的利用率。
需要特別注意的是,RedissonLock 同樣沒有解決 節點掛掉的時候,存在丟失鎖的風險的問題。而現實情況是有一些場景無法容忍的,所以 Redisson 提供了實現了redlock演算法的 RedissonRedLock,RedissonRedLock 真正解決了單點失敗的問題,代價是需要額外的為 RedissonRedLock 搭建Redis環境。
所以,如果業務場景可以容忍這種小概率的錯誤,則推薦使用 RedissonLock, 如果無法容忍,則推薦使用 RedissonRedLock。
參考
https://github.com/javazhiyin/advanced-java/
https://crazyfzw.github.io/2019/04/15/distributed-locks-with-redis/
最近三期