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