分布式鎖使用場景
現在的系統都是集群部署,每個服務都不是單節點的了。比如庫存服務,可能部署到3臺機器上分別命名為節點1,節點2,節點3。庫存服務需要扣減庫存,扣減庫存肯定需要鎖吧,如果使用Lock或者synchronized,只能鎖住自己的節點。而從前臺訪問是隨機路由到這3臺節點的。如果線程一進來使節點1上了鎖,當線程二進來可能訪問到的是節點2,這時節點2還沒有上鎖,那么庫存就會扣減錯誤。而庫存扣減還是一個核心操作,現在居然有Bug,想想就可怕。
這時我們就需要一個全局的鎖了。
實現全局的鎖不一定是redis。MySQL,Zookeeper也可設計為分布式鎖。本篇主要講的是Redis分布式鎖的實現方式,其他的實現方式不做講解。MySQL用作分布式鎖在性能上并不好,這里不建議使用。對Zookeeper分布式鎖有興趣的可以看看我寫的這篇文章。
“
手寫分布式鎖
Zookeeper鎖示意圖
當然市面已經有成熟的框架去實現分布式鎖了,不需要你重復造輪子了。
分布式鎖實現
Redis分布式鎖底層分析
記得之前面試被問Redis分布式鎖的底層原理,我是這么回答的
Redis分布式鎖底層
setnx保證鎖的唯一性。過期時間保證鎖在異常情況下也能解鎖。采用Lua腳本操作Redis,使操作具有原子性。后臺進程心跳檢測,如果當前時間持有鎖并且鎖還未失效,延長鎖的失效時間。如果當前線程沒有獲取到鎖,會一直自旋,直到獲取到鎖為止。
手寫Redis分布式鎖
編寫加鎖方法
我們來看看這段代碼,redisTemplate.execute參數解釋如下
String result = (String) redisTemplate.execute(scriptLock,
redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(key), uuid.toString(), String.valueOf(timeOut));
scriptLock為執行的Redis命令,里面是Lua腳本
腳本里面有setnx操作,還設置了超時時間。
兩個redisTemplate.getStringSerializer()為key和value序列化工具。
后面3個參數為設置key,設置value,設置超時時間。分別對應Lua腳本中的KEYS[1],ARGV[1],ARGV[2]。
如果setnx操作成功,說明鎖創建成功,返回new RedisLock(key, uuid.toString())。
如果失敗,則一直循環拿鎖,直到成功。
“
另外,這里的value為隨機生成的uuid,這是為什么呢?
”
因為如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執行完,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除key的話會有問題,所以得用隨機值加上面的Lua腳本來釋放鎖。
編寫釋放鎖的方法
執行scriptLock2,Lua腳本如下:
測試代碼
測試結果
2020-08-29 20:54:43.484 INFO 21880 --- [main] com.lvshen.demo.RedisLockTest : 獲得鎖
2020-08-29 20:54:49.532 INFO 21880 --- [main] com.lvshen.demo.RedisLockTest : 未獲得鎖
這里沒有做可重入功能,所以第二次訪問的時候,鎖還沒有釋放,所以未獲得鎖。
我們畫一個流程圖,完善下上面的流程
Redis鎖邏輯
有關Redis主從同步問題
在Redis集群中,如果Master節點數據還沒同步到Slave節點,Slave節點就掛了,下次Slave節點好了之后,就沒有保存鎖的數據,從而導致鎖失效。那該怎么辦?
這個場景是假設有一個Redis Cluster,有5個Redis Master實例。然后執行如下步驟獲取一把鎖:
- 獲取當前時間戳,單位是毫秒
- 跟上面類似,輪流嘗試在每個Master節點上創建鎖,過期時間較短,一般就幾十毫秒
- 嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1)
- 客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間,就算建立成功了
- 要是鎖建立失敗了,那么就依次刪除這個鎖
- 只要別人建立了一把分布式鎖,你就得不斷輪詢去嘗試獲取鎖
當超半數的主從同步成功了,才能判定為上鎖成功。
Redis分布式鎖缺點
我們來說說Redis分布式鎖的缺點:
“
Redis分布式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能。
如果是Redis獲取鎖的那個客戶端出Bug了或者掛了,那么只能等待超時時間之后才能釋放鎖。
Redis主從同步RedLock算法存在缺陷,鎖的續命設計也很麻煩。
”
文中涉及的源碼見Github
“
https://github.com/lvshen9/demo/tree/lvshen-dev/src/main/JAVA/com/lvshen/demo/redis/dislock