更新時間:2023-08-24 來源:黑馬程序員 瀏覽量:
本地鎖只能控制所在虛擬機中的線程同步執行,現在要實現分布式環境下所有虛擬機中的線程去同步執行就需要讓多個虛擬機去共用一個鎖,虛擬機可以分布式部署,鎖也可以分布式部署,如下圖:
虛擬機都去搶占同一個鎖,鎖是一個單獨的程序提供加鎖、解鎖服務,誰搶到鎖誰去查詢數據庫。
該鎖已不屬于某個虛擬機,而是分布式部署,由多個虛擬機所共享,這種鎖叫分布式鎖。
實現分布式鎖的方案有很多,常用的如下:
1、基于數據庫實現分布鎖
利用數據庫主鍵唯一性的特點,或利用數據庫唯一索引的特點,多個線程同時去插入相同的記錄,誰插入成功誰就搶到鎖。
2、基于redis實現鎖
redis提供了分布式鎖的實現方案,比如:SETNX、set nx、redisson等。
拿SETNX舉例說明,SETNX命令的工作過程是去set一個不存在的key,多個線程去設置同一個key只會有一個線程設置成功,設置成功的的線程拿到鎖。
3、使用zookeeper實現
zookeeper是一個分布式協調服務,主要解決分布式程序之間的同步的問題。zookeeper的結構類似的文件目錄,多線程向zookeeper創建一個子目錄(節點)只會有一個創建成功,利用此特點可以實現分布式鎖,誰創建該結點成功誰就獲得鎖。
redis實現分布式鎖的方案可以在redis.cn網站查閱,地址http://www.redis.cn/commands/set.html
使用命令: SET resource-name anystring NX EX max-lock-time 即可實現。
NX:表示key不存在才設置成功。
EX:設置過期時間
這里啟動三個ssh客戶端,連接redis: docker exec -it redis redis-cli
先認證: auth redis
同時向三個客戶端發送測試命令如下:
表示設置lock001鎖,value為001,過期時間為30秒
Plain Text SET lock001 001 NX EX 30
命令發送成功,觀察三個ssh客戶端發現只有一個設置成功,其它兩個設置失敗,設置成功的請求表示搶到了lock001鎖。
如何在代碼中使用Set nx去實現分布鎖呢?
使用spring-boot-starter-data-redis 提供的api即可實現set nx。添加依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.6.2</version> </dependency>
添加依賴后,在bean中注入restTemplate。我們先分析一段偽代碼如下:
if(緩存中有){ 返回緩存中的數據 }else{ 獲取分布式鎖 if(獲取鎖成功){ try{ 查詢數據庫 }finally{ 釋放鎖 } } }
使用redisTemplate.opsForValue().setIfAbsent(key,vaue)獲取鎖。
這里考慮一個問題,當set nx一個key/value成功1后,這個key(就是鎖)需要設置過期時間嗎?
如果不設置過期時間當獲取到了鎖卻沒有執行finally這個鎖將會一直存在,其它線程無法獲取這個鎖。所以執行set nx時要指定過期時間,即使用如下的命令。
SET resource-name anystring NX EX max-lock-time
具體調用的方法是:redisTemplate.opsForValue().setIfAbsent(K var1, V var2, long var3, TimeUnit var5)
釋放鎖分為兩種情況:key到期自動釋放,手動刪除。
1)key到期自動釋放的方法
因為鎖設置了過期時間,key到期會自動釋放,但是會存在一個問題就是 查詢數據庫等操作還沒有執行完時key到期了,此時其它線程就搶到鎖了,最終重復查詢數據庫執行了重復的業務操作。
怎么解決這個問題?
可以將key的到期時間設置的長一些,足以執行完成查詢數據庫并設置緩存等相關操作。
如果這樣效率會低一些,另外這個時間值也不好把控。
2)手動刪除鎖
如果是采用手動刪除鎖可能和key到期自動刪除有所沖突,造成刪除了別人的鎖。
比如:當查詢數據庫等業務還沒有執行完時key過期了,此時其它線程占用了鎖,當上一個線程執行查詢數據庫等業務操作完成后手動刪除鎖就把其它線程的鎖給刪除了。
要解決這個問題可以采用刪除鎖之前判斷是不是自己設置的鎖,偽代碼如下:
if(緩存中有){ 返回緩存中的數據 }else{ 獲取分布式鎖: set lock 01 NX if(獲取鎖成功){ try{ 查詢數據庫 }finally{ if(redis.call("get","lock")=="01"){ 釋放鎖: redis.call("del","lock") } } } }
以上代碼第11行到13行非原子性,也會導致刪除其它線程的鎖。查看文檔上的說明:http://www.redis.cn/commands/set.html
上述優化方法會避免下述場景:a客戶端獲得的鎖(鍵key)已經由于過期時間到了被redis服務器刪除,但是這個時候a客戶端還去執行DEL命令。而b客戶端已經在a設置過期時間之后重新獲取了這個同樣key的鎖,那么a執行DEL就會釋放了b客戶端加好的鎖。
解鎖腳本的一個例子將類似于以下:
if redis.cal1("get",KEYS[1]) == ARGV[1] then return redis. call("del",KEYS[1]) else return 0 end
在調用setnx命令設置key/value時,每個線程設置不一樣的value值,這樣當線程去刪除鎖時可以先根據key查詢出來判斷是不是自己當時設置的vlaue,如果是則刪除。
這整個操作是原子的,實現方法就是去執行上邊的lua腳本。
Lua 是一個小巧的腳本語言,redis在2.6版本就支持通過執行Lua腳本保證多個命令的原子性。
什么是原子性?
這些指令要么全成功要么全失敗。
以上就是使用Redis Nx方式實現分布式鎖,為了避免刪除別的線程設置的鎖需要使用redis去執行Lua腳本的方式去實現,這樣就具有原子性,但是過期時間的值設置不存在不精確的問題。