Redis高并發(fā)分布鎖的示例
問題場(chǎng)景
場(chǎng)景一: 沒有捕獲異常
// 僅僅加鎖 // 讀取 stock=15 Boolean ret = stringRedisTemplate.opsForValue().setIfAbsent("lock_key", "1"); // jedis.setnx(k,v) // TODO 業(yè)務(wù)代碼 stock-- stringRedisTemplate.delete("lock_key");
**問題
**
以上場(chǎng)景在代碼出現(xiàn)異常的時(shí)候,會(huì)出現(xiàn)死鎖,導(dǎo)致后面的線程無法獲取鎖,會(huì)阻塞所有線程
場(chǎng)景二: 線程間交互刪除鎖
// 加鎖,且設(shè)置鎖過期時(shí)間 // 讀取 stock = 15 Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("key", "1", 10, TimeUnit.SECONDS); // TODO 業(yè)務(wù)代碼 stock-- stringRedisTemplate.delete(key);
問題
相對(duì)于場(chǎng)景一多了鎖的過期時(shí)間
假如線程A執(zhí)行業(yè)務(wù)代碼的時(shí)間是15s,而鎖的時(shí)間是10s,那么鎖過期后自動(dòng)會(huì)被刪除,此時(shí)線程B獲取鎖,執(zhí)行業(yè)務(wù)代碼時(shí)間為8s,而這個(gè)時(shí)候線程A剛好執(zhí)行完業(yè)務(wù)代碼了,就會(huì)出現(xiàn)線程A把線程B的鎖刪除掉
// 加鎖,且(給每個(gè)線程)設(shè)置鎖過期時(shí)間, 刪除鎖時(shí)判斷是否當(dāng)前線程 // 讀取 stock = 15 String uuid = UUID.getUuid; Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("key", uuid, 10, TimeUnit.SECONDS); // TODO 業(yè)務(wù)代碼 stock-- 15 -> 14 // 判斷是否當(dāng)前線程 if (uuid.equals(stringRedisTemplate.opsForValue().get(key)) { // 極端場(chǎng)景下:(執(zhí)行時(shí)間定格在9.99秒)突然卡頓 10ms or redis服務(wù)宕機(jī)!?。? // 此時(shí)剛好鎖過期,自動(dòng)刪除 // 其他線程獲取鎖,然后會(huì)把上個(gè)線程的鎖刪除,又會(huì)出現(xiàn)bug stringRedisTemplate.delete(key); }
問題
當(dāng)線程A持有鎖,執(zhí)行完扣減庫存后,假設(shè)鎖過期時(shí)間是10s,恰好此時(shí)在執(zhí)行9.99s的時(shí)候出現(xiàn)卡頓
,等服務(wù)器反應(yīng)過來之間,鎖過期自動(dòng)刪除了,這個(gè)時(shí)候線程B獲取鎖,然后執(zhí)行業(yè)務(wù)代碼,此時(shí)線程A剛好反應(yīng)過來,執(zhí)行鎖刪除
,這樣就會(huì)把線程B的鎖刪除,要知道此時(shí)線程B是沒有執(zhí)行完業(yè)務(wù)代碼的,鎖刪除后,線程C又獲取鎖,此時(shí)線程B執(zhí)行完,又會(huì)把線程C的鎖刪除,依次類推
解決方案
方案: 使用Redisson分布式鎖
@Autowire public Redisson redisson; public void stock () { String key = "key"; RLock lock = redisson.getLock(key); try { lock.lock(); // TODO: 業(yè)務(wù)代碼 } catch(Exception e) { lock.unlock(); } }
優(yōu)點(diǎn)
- 自帶
鎖續(xù)命
功能,默認(rèn)30s過期時(shí)間,可以自行調(diào)整過期時(shí)間 - LUA腳本模擬商品減庫存
//模擬一個(gè)商品減庫存的原子操作 //lua腳本命令執(zhí)行方式:redis-cli --eval /tmp/test.lua , 10 jedis.set("product_stock_10016", "15"); // 初始化商品10016的庫存 String script = " local count = redis.call('get', KEYS[1]) " + " local a = tonumber(count) " + " local b = tonumber(ARGV[1]) " + " if a >= b then " + " redis.call('set', KEYS[1], a-b) " + // 模擬語法報(bào)錯(cuò)回滾操作 " bb == 0 " + " return 1 " + " end " + " return 0 "; Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10")); System.out.println(obj);
Redisson實(shí)現(xiàn)
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException { long threadId = Thread.currentThread().getId(); Long ttl = this.tryAcquire(leaseTime, unit, threadId); if (ttl != null) { RFuture<RedissonLockEntry> future = this.subscribe(threadId); this.commandExecutor.syncSubscription(future); try { while(true) { ttl = this.tryAcquire(leaseTime, unit, threadId); if (ttl == null) { return; } if (ttl >= 0L) { this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } else { this.getEntry(threadId).getLatch().acquire(); } } } finally { this.unsubscribe(future, threadId); } } }
LUA腳本適合用于做原子操作,在Redisson分布式鎖實(shí)現(xiàn)中,就有用到LUA腳本實(shí)現(xiàn)創(chuàng)建/獲取鎖的操作,而Redis的事務(wù)機(jī)制(multi/exec)非常雞肋,可以對(duì)相同的key通過不同的數(shù)據(jù)結(jié)構(gòu)做修改,比如事務(wù)開啟后,將String類型的key,再次使用hset修改,而且還能修改成功,這就意味著事務(wù)已失效,而且不支持事務(wù)回滾
Redisson分布式鎖流程
- 高并發(fā)下Lua腳本保證了原子性
- Schedule定期鎖續(xù)命
- 未獲取鎖的線程先Subscribe channel
- 自旋,再次嘗試獲取鎖
- 如果還是未獲取鎖,則通過Semaphore->tryAcquire(ttl.TimeUnit)阻塞所有進(jìn)入自旋代碼塊的線程(
這樣做的目的是為了不讓其他線程因?yàn)椴煌5淖孕o服務(wù)器造成壓力,所以讓其他線程先阻塞一段時(shí)間,等阻塞時(shí)間結(jié)束,再次自旋
) - 獲取鎖的線程解鎖后,使用Redis的發(fā)布功能進(jìn)行發(fā)布消息,訂閱消息的線程調(diào)用release方法釋放阻塞的線程,再次嘗試獲取鎖
- 如果是調(diào)用Redisson的tryAcquire(1000,TimeUnit.SECONDS)方法,那么未獲取到鎖的線程不用進(jìn)行自旋,因?yàn)闀r(shí)間一到,未獲取到鎖的線程就會(huì)自動(dòng)往下走進(jìn)入業(yè)務(wù)代碼塊
總結(jié)
Redis分布式鎖自己去實(shí)現(xiàn)可能會(huì)出現(xiàn)幾個(gè)問題
沒有在finally顯示釋放鎖,當(dāng)客戶端掛掉了,鎖沒有被及時(shí)刪除,這樣會(huì)導(dǎo)致死鎖問題,它這個(gè)是需要我們顯示的釋放鎖
假如此時(shí)我們?cè)O(shè)置過期時(shí)間,但是我們用的是同一個(gè)key,就可能出現(xiàn)下一個(gè)線程刪除上一個(gè)線程的鎖,但是上一個(gè)線程還沒有執(zhí)行完,它這個(gè)需要key是不能重復(fù)的
假如我們既設(shè)置了過期時(shí)間也指定了不同的key,此時(shí)可能因?yàn)榫W(wǎng)絡(luò)延遲出現(xiàn)上一個(gè)線程刪除下一個(gè)線程的鎖,也就是說業(yè)務(wù)執(zhí)行的時(shí)間超過了鎖過期的時(shí)間,它這個(gè)需要一個(gè)鎖續(xù)命的功能
對(duì)于Redis它也有事務(wù),但是它的事務(wù)非常雞肋,僅僅只能保證多個(gè)指令按照順序執(zhí)行,并不能保證原子性,而且key還能被其他指令修改對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),所以我們選擇Redisson來進(jìn)行分布式鎖的實(shí)現(xiàn),因?yàn)樗峁┝随i續(xù)命的功能以及通過lua腳本保證了多個(gè)指令的原子操作,主要流程
是這樣的
當(dāng)線程搶到了鎖,假如業(yè)務(wù)沒執(zhí)行完,會(huì)定時(shí)去進(jìn)行鎖續(xù)命,而其他線程會(huì)訂閱這個(gè)搶到鎖的線程的channel,然后自旋一定時(shí)間去嘗試獲取鎖,如果獲取鎖失敗,會(huì)被安排進(jìn)入隊(duì)列中阻塞,一旦線程釋放鎖,他們會(huì)被通知到,然后繼續(xù)去自旋一定時(shí)間去嘗試獲取鎖,重復(fù)此操作
到此這篇關(guān)于Redis高并發(fā)分布鎖的示例的文章就介紹到這了,更多相關(guān)Redis高并發(fā)分布鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis中事務(wù)機(jī)制及樂觀鎖的實(shí)現(xiàn)
這篇文章主要介紹了redis中事務(wù)機(jī)制及樂觀鎖的相關(guān)內(nèi)容,通過事務(wù)的執(zhí)行分析Redis樂觀鎖,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10Ubuntu系統(tǒng)中Redis的安裝步驟及服務(wù)配置詳解
本文主要記錄了Ubuntu服務(wù)器中Redis服務(wù)的安裝使用,包括apt安裝和解壓縮編譯安裝兩種方式,并對(duì)安裝過程中可能出現(xiàn)的問題、解決方案進(jìn)行說明,以及在手動(dòng)安裝時(shí),服務(wù)器如何添加自定義服務(wù)的問題,需要的朋友可以參考下2024-12-12Redis設(shè)置密碼的實(shí)現(xiàn)步驟
本文主要介紹了Redis設(shè)置密碼的實(shí)現(xiàn)步驟,主要包括兩種方法:臨時(shí)密碼和持久密碼,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08redis分布式鎖優(yōu)化的實(shí)現(xiàn)
本文主要介紹了redis分布式鎖優(yōu)化的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09RedisDesktopManager無法遠(yuǎn)程連接Redis的完美解決方法
下載RedisDesktopManager客戶端,輸入服務(wù)器IP地址,端口(缺省值:6379);點(diǎn)擊Test Connection按鈕測(cè)試連接,連接失敗,怎么回事呢?下面小編給大家?guī)砹薘edisDesktopManager無法遠(yuǎn)程連接Redis的完美解決方法,一起看看吧2018-03-03Linux、Windows下Redis的安裝即Redis的基本使用詳解
Redis是一個(gè)基于內(nèi)存的key-value結(jié)構(gòu)數(shù)據(jù)庫,Redis 是互聯(lián)網(wǎng)技術(shù)領(lǐng)域使用最為廣泛的存儲(chǔ)中間件,這篇文章主要介紹了Linux、Windows下Redis的安裝即Redis的基本使用詳解,需要的朋友可以參考下2022-09-09Redis list 類型學(xué)習(xí)筆記與總結(jié)
這篇文章主要介紹了Redis list 類型學(xué)習(xí)筆記與總結(jié),本文著重講解了關(guān)于List的一些常用方法,比如lpush 方法、lrange 方法、rpush 方法、linsert 方法、 lset 方法等,需要的朋友可以參考下2015-06-06