一文詳解如何使用Redis實(shí)現(xiàn)分布式鎖
1. 什么是分布式鎖
當(dāng)我們在編寫多線程代碼的時(shí)候,不同的線程可能會(huì)發(fā)生資源的爭奪,為了避免資源爭奪造成的錯(cuò)誤,我們會(huì)對資源上鎖,只有獲得鎖的線程才能繼續(xù)往下執(zhí)行。
進(jìn)程中的鎖,本質(zhì)就是內(nèi)存中一個(gè)變量,當(dāng)一個(gè)線程執(zhí)行某個(gè)操作申請加鎖時(shí),如果能成功把代表鎖的變量值設(shè)置為1,則表示獲得了鎖,其他線程想要獲得鎖時(shí)會(huì)阻塞,而擁有鎖的線程執(zhí)行完操作后,再把鎖的值設(shè)置為0,則表示釋放了鎖。

上面我們說的是在一臺(tái)服務(wù)器的進(jìn)程內(nèi)不同線程之間的鎖,這個(gè)鎖是放在內(nèi)存中的,而對于分布式應(yīng)用程序來說,不同的應(yīng)用(進(jìn)程或線程)部署在不同的服務(wù)器上,這樣就不能通過內(nèi)存中的變量來表示鎖。
即然在一臺(tái)服務(wù)器上可以通過內(nèi)存這塊共享的空間來表示鎖,那么對于分布式應(yīng)用程序來說,可以共享存儲(chǔ)系統(tǒng)來存儲(chǔ)一個(gè)共享鎖,這就是分布式鎖,而Redis作為內(nèi)存數(shù)據(jù)庫,執(zhí)行非常快,很適合作為實(shí)現(xiàn)分布式鎖的共享存儲(chǔ)系統(tǒng)。

2. 使用Redis實(shí)現(xiàn)分布式鎖
對于一個(gè)鎖來說,其實(shí)只有兩個(gè)操作,加鎖和釋放鎖,下面我們看來看通過Redis要怎么實(shí)現(xiàn)?
2.1 加鎖
Redis的setnx命令會(huì)判斷鍵值是否存在,如果存在則不做任何操作,并返回0,如果不存在,則創(chuàng)建并賦值,并返回1,因此我們可以執(zhí)行setnx為一個(gè)代表鎖鍵設(shè)置值,如果能設(shè)置成功,則表示獲得鎖,失敗則無法獲得鎖。
# 使用key為lock來表示一個(gè)鎖 setnx lock 1
2.2 釋放鎖
當(dāng)執(zhí)行好操作之后,要釋放鎖的時(shí)候直接把Redis里的鍵值lock刪除就可以了,這樣其他進(jìn)程才能通過setnx命令重新設(shè)置并獲得該鎖。
# 釋放鎖 del lock
通過上面兩個(gè)命令,我們實(shí)現(xiàn)了一個(gè)簡單的分布式鎖,但這里就出現(xiàn)了一個(gè)問題:如果一個(gè)進(jìn)程通過setnx命令加鎖之后,在執(zhí)行具體操作出錯(cuò)了,沒有辦法及時(shí)釋放鎖,那么其他進(jìn)程就無法獲得該鎖,系統(tǒng)便無法繼續(xù)往下執(zhí)行,解決這個(gè)問題的辦法就是為鎖設(shè)置一個(gè)有效期,在這個(gè)有效期之后,自動(dòng)釋放鎖。
2.3 給鎖設(shè)置有效期
給鎖設(shè)置有效期非常簡單,直接使用Redis的expire命令就可以了,如:
# 加鎖 setnx lock 1 # 給鎖設(shè)置10s有效期 expire lock 10
但是,現(xiàn)在又出現(xiàn)另一個(gè)問題了,如果我們在設(shè)置了鎖之后,執(zhí)行expire命令之前該進(jìn)程掛掉了,那么expire就沒有執(zhí)行成功,鎖一樣是沒有被釋放掉的,所以一定要保證上面兩個(gè)命令要一起執(zhí)行,怎么保證呢?
有兩個(gè)方法,一個(gè)是使用LUA語言編寫的腳本,另一個(gè)是使用Redis的set命令,set命令后面跟nx參數(shù)后,執(zhí)行的效果與setnx一致,且set命令可以跟ex參數(shù)來設(shè)置過期時(shí)間,所以我們可以使用set命令把setnx和expire兩個(gè)合并在一起,這樣就可以保證執(zhí)行的原子性了。
# 判斷是否鍵值是否存在,ex后面跟著的是鍵值的有效期,10s set lock 1 nx ex 10
解決了鎖的有效問題,現(xiàn)在我們再來看另外一個(gè)問題。

如上圖所示,現(xiàn)在有A,B,C三個(gè)不同服務(wù)器上的進(jìn)程在執(zhí)行某個(gè)操作都需要獲得鎖,執(zhí)行后要釋放鎖。
現(xiàn)在的情況是進(jìn)程A執(zhí)行第2步時(shí)卡頓了(上面綠色區(qū)域所示),且時(shí)間超出了鎖有效期,所以進(jìn)程A設(shè)置的鎖自動(dòng)釋放了,這時(shí)候進(jìn)程B獲得了鎖,并開始執(zhí)行操作,但由于進(jìn)程A只是卡頓了而已,所以會(huì)繼續(xù)執(zhí)行的時(shí)候,在第3步的時(shí)候會(huì)手動(dòng)釋放鎖,但是這個(gè)時(shí)候,鎖由線程B所擁有,也就是說進(jìn)程A刪除的不是自己的鎖,而進(jìn)程B的鎖,這時(shí)候進(jìn)程B還沒執(zhí)行完,但鎖被釋放后,進(jìn)程C可以加鎖,也就是說由于進(jìn)程A卡頓釋放錯(cuò)了鎖,導(dǎo)致進(jìn)程B和進(jìn)程C可以同時(shí)獲得鎖。
怎么避免這種情況呢?如何區(qū)分其他進(jìn)程的鎖,避免刪除其他進(jìn)程的鎖呢?答案就是每個(gè)進(jìn)程在加鎖的時(shí)候,給鎖設(shè)置一個(gè)唯一值,并在釋放鎖的時(shí)候,判斷是不是自己設(shè)置的鎖。
2.4 給鎖設(shè)置唯一值
給鎖設(shè)置唯一值的時(shí)候,一樣是使用set命令,唯一的不同是將鍵值1改為一個(gè)隨機(jī)生成的唯一值,比如uuid。
# rand_uid表示唯一id set lock rand_id nx ex 10
當(dāng)鎖里的值由進(jìn)程設(shè)置后,釋放鎖的時(shí)候,就需要判斷鎖是不是自己的,步驟如下:
- 通過
Redis的get命令獲得鎖的值 - 根據(jù)獲得的值,判斷鎖是不是自己設(shè)置的
- 如果是,通過
del命令釋放鎖。
此時(shí)我們看到,釋放鎖需要執(zhí)行三個(gè)操作,如果三個(gè)操作依次執(zhí)行的話,是沒有辦法保證原子性的,比如進(jìn)程A在執(zhí)行到第2步后,準(zhǔn)備開始執(zhí)行del命令時(shí),而鎖由時(shí)有效期到了,被自動(dòng)釋放了,并被其他服務(wù)器上的進(jìn)程B獲得鎖,但這時(shí)候線程A執(zhí)行del還是把線程B的鎖給刪掉了。
解決這個(gè)問題的辦法就是保證上述三個(gè)操作執(zhí)行的原子性,即在執(zhí)行釋放鎖的三個(gè)操作中,其他進(jìn)程不可以獲得鎖,想要做到這一點(diǎn),需要使用到LUA腳本。
2.5 通過LUA腳本實(shí)現(xiàn)釋放鎖的原子性
Redis支持LUA腳本,LUA腳里的代碼執(zhí)行的時(shí)候,其他客戶端的請求不會(huì)被執(zhí)行,這樣可以保證原子性操作,所以我們可以使用下面腳本進(jìn)行鎖的釋放:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end將上述腳本保存為腳本后,可以調(diào)用Redis客戶端命令redis-cli來執(zhí)行,如下:
# lock為key,rand_id表示key里保存的值 redis-cli --eval unlock.lua lock , rand_id
3. 小結(jié)
無論是本地鎖還是分布式鎖,鎖的本質(zhì)就是一個(gè)共享的變量,只是在實(shí)現(xiàn)分布式鎖時(shí)候,把這個(gè)變量移到了Redis服務(wù)器所在的內(nèi)存中。
在上面實(shí)現(xiàn)分布式鎖的過程中我們碰到了以下幾個(gè)問題:
- 如何保證加鎖操作的原子性?
- 如何保證進(jìn)程崩潰自動(dòng)釋放鎖?
- 如何避免刪錯(cuò)其他進(jìn)程的鎖?
- 如何保證釋放鎖操作的原子性?
在解決上述問題的時(shí)候,我們也一步步完善一個(gè)可以在實(shí)際開發(fā)中應(yīng)用的Redis分布式鎖。
到此這篇關(guān)于一文詳解如何使用Redis實(shí)現(xiàn)分布式鎖的文章就介紹到這了,更多相關(guān)Redis實(shí)現(xiàn)分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis SETNX命令在Spring Cloud中的分布式鎖用法詳解
在Spring Cloud項(xiàng)目中,使用Java和Redis結(jié)合實(shí)現(xiàn)的分布式鎖可以確保訂單的一致性和并發(fā)控制,通過合理使用鎖的粒度以及注意事項(xiàng),可以減少死鎖問題并提高系統(tǒng)的并發(fā)性能,這篇文章主要介紹了Redis SETNX命令在Spring Cloud中的分布式鎖用法詳解,需要的朋友可以參考下2023-10-10
SpringBoot整合Redis入門之緩存數(shù)據(jù)的方法
Redis是一個(gè)開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫,并提供多種語言的API,下面通過本文給大家介紹下SpringBoot整合Redis入門之緩存數(shù)據(jù)的相關(guān)知識(shí),感興趣的朋友一起看看吧2021-11-11
手動(dòng)實(shí)現(xiàn)Redis的LRU緩存機(jī)制示例詳解
這篇文章主要介紹了手動(dòng)實(shí)現(xiàn)Redis的LRU緩存機(jī)制示例詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03

