解決Redis分布式鎖的誤刪問題和原子性問題
Redis的分布式鎖
Redis的分布式鎖是通過利用Redis的原子操作和特性來實(shí)現(xiàn)的。在分布式環(huán)境中,多個(gè)應(yīng)用程序或服務(wù)可能同時(shí)訪問共享資源,為了保證數(shù)據(jù)的一致性和避免沖突,可以使用分布式鎖來進(jìn)行同步控制。
以下是一種常見的使用Redis實(shí)現(xiàn)分布式鎖的方式:
- 獲取鎖:當(dāng)一個(gè)應(yīng)用程序需要獲取鎖時(shí),它可以通過執(zhí)行以下操作在Redis中設(shè)置一個(gè)特定的鍵值對:
SET lock_key unique_value NX PX lock_timeout
這里的lock_key是鎖的唯一標(biāo)識,unique_value是唯一的值,可以是隨機(jī)生成的UUID,NX表示只有當(dāng)鍵不存在時(shí)才會設(shè)置成功,PX表示設(shè)置鍵的過期時(shí)間。通過設(shè)置過期時(shí)間,即使獲取鎖的應(yīng)用程序崩潰或異常退出,鎖也會在一段時(shí)間后自動釋放,避免出現(xiàn)死鎖。
- 釋放鎖:當(dāng)應(yīng)用程序完成對共享資源的操作后,它可以通過執(zhí)行以下操作釋放鎖:
if GET lock_key == unique_value then DELETE lock_key end
應(yīng)用程序首先獲取鎖的當(dāng)前值,然后比較是否與自己持有的唯一值相等,如果相等則刪除該鍵,表示釋放鎖。這樣可以確保只有持有鎖的應(yīng)用程序才能釋放鎖,避免誤釋放其他應(yīng)用程序的鎖。
需要注意的是,分布式鎖并不是絕對安全和可靠的。在高并發(fā)的環(huán)境中,可能存在競爭條件和死鎖等問題。因此,在實(shí)際使用中,需要考慮更復(fù)雜的場景和解決方案。
誤刪問題
遇到下面的情況的話,會出現(xiàn)Redis分布式鎖的誤刪問題
這種情況下。線程1
首先獲取鎖,但是發(fā)生了阻塞,于是線程2
拿到了執(zhí)行權(quán),在線程2
執(zhí)行的過程中,線程1
蘇醒了,繼續(xù)執(zhí)行,到后面,線程1
執(zhí)行到了刪除鎖的操作,此時(shí)就會把本應(yīng)該屬于線程2
的鎖刪除,這樣子就造成了誤刪問題
解決方法
就是在每個(gè)線程釋放鎖的時(shí)候,去判斷一下當(dāng)前這把鎖是否屬于自己,如果屬于自己,則不進(jìn)行鎖的刪除,假設(shè)還是上邊的情況,線程1卡頓,鎖自動釋放,線程2進(jìn)入到鎖的內(nèi)部執(zhí)行邏輯,此時(shí)線程1反應(yīng)過來,然后刪除鎖,但是線程1,一看當(dāng)前這把鎖不是屬于自己,于是不進(jìn)行刪除鎖邏輯,當(dāng)線程2走到刪除鎖邏輯時(shí),如果沒有卡過自動釋放鎖的時(shí)間點(diǎn),則判斷當(dāng)前這把鎖是屬于自己的,于是刪除這把鎖。
代碼實(shí)現(xiàn)
public class SimpleRedisLock implements ILock { private String name; private StringRedisTemplate stringRedisTemplate; public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } private static final String KEY_PREFIX = "lock:"; //使用uuid,在獲取鎖的時(shí)候存入線程標(biāo)識 private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-"; @Override public boolean tryLock(long timeoutSec) { // 獲取線程標(biāo)示 String threadId = ID_PREFIX + Thread.currentThread().getId(); // 獲取鎖 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); //這里不能是return success;否則 因?yàn)閜ublic后面的boolean是基本類型,而Boolean是引用類型,如果直接返回success,是一個(gè)自動拆箱的過程,可能回發(fā)生空指針異常 } @Override public void unlock() { // 獲取線程標(biāo)示 String threadId = ID_PREFIX + Thread.currentThread().getId(); // 獲取鎖中的標(biāo)示 String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); // 判斷標(biāo)示是否一致 if(threadId.equals(id)) { // 釋放鎖 stringRedisTemplate.delete(KEY_PREFIX + name); } } }
原子性問題
上面我們解決了誤刪問題
在誤刪問題的情況下,遇到下面的情況的話,會出現(xiàn)Redis分布式鎖的原子性問題
這種情況下,線程1先執(zhí)行一段,線程1先判斷鎖標(biāo)識,判斷成功,標(biāo)識是屬于線程1的,后面就在線程1正準(zhǔn)備刪除鎖釋放的過程中,突然線程1的鎖過期了,線程1發(fā)生阻塞
這個(gè)時(shí)候線程2開始執(zhí)行,在線程2執(zhí)行過程中,線程1阻塞結(jié)束了,會執(zhí)行刪除鎖的操作,相當(dāng)于判斷鎖標(biāo)識并沒有起到作用(因?yàn)橹耙痪渑袛噙^了),于是就把線程2的鎖給刪除掉了,又一次發(fā)生了誤刪操作
這個(gè)時(shí)候線程3趁虛而入,執(zhí)行業(yè)務(wù)
這就是刪鎖時(shí)的原子性問題,之所以有這個(gè)問題,是因?yàn)榕袛噫i標(biāo)識和刪除鎖是2個(gè)動作,這2個(gè)動作中間產(chǎn)生了阻塞
那么我們就要讓這2個(gè)操作一起執(zhí)行,中間不能出現(xiàn)間隔
Lua腳本
Redis提供了Lua腳本功能,在一個(gè)腳本中編寫多條Redis命令,確保多條命令執(zhí)行時(shí)的原子性。Lua是一種編程語言,它的基本語法大家可以參考網(wǎng)站:https://www.runoob.com/lua/lua-tutorial.html,這里重點(diǎn)介紹Redis提供的調(diào)用函數(shù),我們可以使用lua去操作redis,又能保證他的原子性,這樣就可以實(shí)現(xiàn)拿鎖比鎖刪鎖是一個(gè)原子性動作了,作為Java程序員這一塊并不作一個(gè)簡單要求,并不需要大家過于精通,只需要知道他有什么作用即可。
這里重點(diǎn)介紹Redis提供的調(diào)用函數(shù),語法如下:
redis.call('命令名稱', 'key', '其它參數(shù)', ...)
例如,我們要執(zhí)行set name jack,則腳本是這樣:
# 執(zhí)行 set name jack redis.call('set', 'name', 'jack')
例如,我們要先執(zhí)行set name Rose,再執(zhí)行g(shù)et name,則腳本如下:
# 先執(zhí)行 set name jack redis.call('set', 'name', 'Rose') # 再執(zhí)行 get name local name = redis.call('get', 'name') # 返回 return name
寫好腳本以后,需要用Redis命令來調(diào)用腳本,調(diào)用腳本的常見命令如下:
例如,我們要執(zhí)行 redis.call(‘set’, ‘name’, ‘jack’) 這個(gè)腳本,語法如下:
如果腳本中的key、value不想寫死,可以作為參數(shù)傳遞。key類型參數(shù)會放入KEYS數(shù)組,其它參數(shù)會放入ARGV數(shù)組,在腳本中可以從KEYS和ARGV數(shù)組獲取這些參數(shù):
利用Java代碼調(diào)用Lua腳本改造分布式鎖
接下來我們來回一下我們釋放鎖的邏輯:
釋放鎖的業(yè)務(wù)流程是這樣的
1、獲取鎖中的線程標(biāo)示
? 2、判斷是否與指定的標(biāo)示(當(dāng)前線程標(biāo)示)一致
? 3、如果一致則釋放鎖(刪除)
? 4、如果不一致則什么都不做
如果用Lua腳本來表示則是這樣的:
最終我們操作redis的拿鎖比鎖刪鎖的lua腳本就會變成這樣
-- 這里的 KEYS[1] 就是鎖的key,這里的ARGV[1] 就是當(dāng)前線程標(biāo)示 -- 獲取鎖中的標(biāo)示,判斷是否與當(dāng)前線程標(biāo)示一致 if (redis.call('GET', KEYS[1]) == ARGV[1]) then -- 一致,則刪除鎖 return redis.call('DEL', KEYS[1]) end -- 不一致,則直接返回 return 0
lua腳本本身并不需要大家花費(fèi)太多時(shí)間去研究,只需要知道如何調(diào)用,大致是什么意思即可,所以在筆記中并不會詳細(xì)的去解釋這些lua表達(dá)式的含義。
我們的RedisTemplate中,可以利用execute方法去執(zhí)行l(wèi)ua腳本,參數(shù)對應(yīng)關(guān)系就如下圖
代碼實(shí)現(xiàn)
我們先寫入lua這個(gè)腳本
-- 比較線程標(biāo)示與鎖中的標(biāo)示是否一致 if(redis.call('get', KEYS[1]) == ARGV[1]) then -- 釋放鎖 del key return redis.call('del', KEYS[1]) end return 0
然后我們來調(diào)用這個(gè)腳本
下面是完整代碼
public class SimpleRedisLock implements ILock { private String name; private StringRedisTemplate stringRedisTemplate; public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } private static final String KEY_PREFIX = "lock:"; private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-"; private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static { UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); UNLOCK_SCRIPT.setResultType(Long.class); } @Override public boolean tryLock(long timeoutSec) { // 獲取線程標(biāo)示 String threadId = ID_PREFIX + Thread.currentThread().getId(); // 獲取鎖 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); //這里不能是return success;否則 因?yàn)閜ublic后面的boolean是基本類型,而Boolean是引用類型,如果直接返回success,是一個(gè)自動拆箱的過程,可能回發(fā)生空指針異常 } @Override public void unlock() { // 調(diào)用lua腳本 stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId()); } }
在技術(shù)的道路上,我們不斷探索、不斷前行,不斷面對挑戰(zhàn)、不斷突破自我??萍嫉陌l(fā)展改變著世界,而我們作為技術(shù)人員,也在這個(gè)過程中書寫著自己的篇章。讓我們攜手并進(jìn),共同努力,開創(chuàng)美好的未來!愿我們在科技的征途上不斷奮進(jìn),創(chuàng)造出更加美好、更加智能的明天!
以上就是解決Redis分布式鎖的誤刪問題和原子性問題的詳細(xì)內(nèi)容,更多關(guān)于Redis分布式鎖誤刪和原子性問題的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
redis分布式鎖優(yōu)化的實(shí)現(xiàn)
本文主要介紹了redis分布式鎖優(yōu)化的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09Ubuntu系統(tǒng)中Redis的安裝步驟及服務(wù)配置詳解
本文主要記錄了Ubuntu服務(wù)器中Redis服務(wù)的安裝使用,包括apt安裝和解壓縮編譯安裝兩種方式,并對安裝過程中可能出現(xiàn)的問題、解決方案進(jìn)行說明,以及在手動安裝時(shí),服務(wù)器如何添加自定義服務(wù)的問題,需要的朋友可以參考下2024-12-12使用redis實(shí)現(xiàn)高效分頁的項(xiàng)目實(shí)踐
在很多場景下,我們需要對大量的數(shù)據(jù)進(jìn)行分頁展示,本文主要介紹了使用redis實(shí)現(xiàn)高效分頁的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02詳解用Redis實(shí)現(xiàn)Session功能
本篇文章主要介紹了用Redis實(shí)現(xiàn)Session功能,具有一定的參考價(jià)值,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。2016-12-12Spring boot+redis實(shí)現(xiàn)消息發(fā)布與訂閱的代碼
這篇文章主要介紹了Spring boot+redis實(shí)現(xiàn)消息發(fā)布與訂閱,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值需要的朋友可以參考下2020-04-04使用SpringBoot?+?Redis?實(shí)現(xiàn)接口限流的方式
這篇文章主要介紹了SpringBoot?+?Redis?實(shí)現(xiàn)接口限流,Redis?除了做緩存,還能干很多很多事情:分布式鎖、限流、處理請求接口冪等,文中給大家提到了限流注解的創(chuàng)建方式,需要的朋友可以參考下2022-05-05基于Redis實(shí)現(xiàn)每日登錄失敗次數(shù)限制
這篇文章主要介紹了通過redis實(shí)現(xiàn)每日登錄失敗次數(shù)限制的問題,通過redis記錄登錄失敗的次數(shù),以用戶的username為key,本文給出了實(shí)例代碼,需要的朋友可以參考下2019-08-08