Redis高并發(fā)分布鎖的示例
問題場景
場景一: 沒有捕獲異常
// 僅僅加鎖
// 讀取 stock=15
Boolean ret = stringRedisTemplate.opsForValue().setIfAbsent("lock_key", "1"); // jedis.setnx(k,v)
// TODO 業(yè)務(wù)代碼 stock--
stringRedisTemplate.delete("lock_key");
**問題 **
以上場景在代碼出現(xiàn)異常的時候,會出現(xiàn)死鎖,導(dǎo)致后面的線程無法獲取鎖,會阻塞所有線程
場景二: 線程間交互刪除鎖
// 加鎖,且設(shè)置鎖過期時間
// 讀取 stock = 15
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("key", "1", 10, TimeUnit.SECONDS);
// TODO 業(yè)務(wù)代碼 stock--
stringRedisTemplate.delete(key);
問題
相對于場景一多了鎖的過期時間
假如線程A執(zhí)行業(yè)務(wù)代碼的時間是15s,而鎖的時間是10s,那么鎖過期后自動會被刪除,此時線程B獲取鎖,執(zhí)行業(yè)務(wù)代碼時間為8s,而這個時候線程A剛好執(zhí)行完業(yè)務(wù)代碼了,就會出現(xiàn)線程A把線程B的鎖刪除掉
// 加鎖,且(給每個線程)設(shè)置鎖過期時間, 刪除鎖時判斷是否當前線程
// 讀取 stock = 15
String uuid = UUID.getUuid;
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("key", uuid, 10, TimeUnit.SECONDS);
// TODO 業(yè)務(wù)代碼 stock-- 15 -> 14
// 判斷是否當前線程
if (uuid.equals(stringRedisTemplate.opsForValue().get(key)) {
// 極端場景下:(執(zhí)行時間定格在9.99秒)突然卡頓 10ms or redis服務(wù)宕機?。?!
// 此時剛好鎖過期,自動刪除
// 其他線程獲取鎖,然后會把上個線程的鎖刪除,又會出現(xiàn)bug
stringRedisTemplate.delete(key);
}
問題
當線程A持有鎖,執(zhí)行完扣減庫存后,假設(shè)鎖過期時間是10s,恰好此時在執(zhí)行9.99s的時候出現(xiàn)卡頓,等服務(wù)器反應(yīng)過來之間,鎖過期自動刪除了,這個時候線程B獲取鎖,然后執(zhí)行業(yè)務(wù)代碼,此時線程A剛好反應(yīng)過來,執(zhí)行鎖刪除,這樣就會把線程B的鎖刪除,要知道此時線程B是沒有執(zhí)行完業(yè)務(wù)代碼的,鎖刪除后,線程C又獲取鎖,此時線程B執(zhí)行完,又會把線程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)點
- 自帶
鎖續(xù)命功能,默認30s過期時間,可以自行調(diào)整過期時間 - LUA腳本模擬商品減庫存
//模擬一個商品減庫存的原子操作
//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) " +
// 模擬語法報錯回滾操作
" 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實現(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分布式鎖實現(xiàn)中,就有用到LUA腳本實現(xiàn)創(chuàng)建/獲取鎖的操作,而Redis的事務(wù)機制(multi/exec)非常雞肋,可以對相同的key通過不同的數(shù)據(jù)結(jié)構(gòu)做修改,比如事務(wù)開啟后,將String類型的key,再次使用hset修改,而且還能修改成功,這就意味著事務(wù)已失效,而且不支持事務(wù)回滾
Redisson分布式鎖流程
- 高并發(fā)下Lua腳本保證了原子性
- Schedule定期鎖續(xù)命
- 未獲取鎖的線程先Subscribe channel
- 自旋,再次嘗試獲取鎖
- 如果還是未獲取鎖,則通過Semaphore->tryAcquire(ttl.TimeUnit)阻塞所有進入自旋代碼塊的線程(
這樣做的目的是為了不讓其他線程因為不停的自旋而給服務(wù)器造成壓力,所以讓其他線程先阻塞一段時間,等阻塞時間結(jié)束,再次自旋) - 獲取鎖的線程解鎖后,使用Redis的發(fā)布功能進行發(fā)布消息,訂閱消息的線程調(diào)用release方法釋放阻塞的線程,再次嘗試獲取鎖
- 如果是調(diào)用Redisson的tryAcquire(1000,TimeUnit.SECONDS)方法,那么未獲取到鎖的線程不用進行自旋,因為時間一到,未獲取到鎖的線程就會自動往下走進入業(yè)務(wù)代碼塊

總結(jié)
Redis分布式鎖自己去實現(xiàn)可能會出現(xiàn)幾個問題
沒有在finally顯示釋放鎖,當客戶端掛掉了,鎖沒有被及時刪除,這樣會導(dǎo)致死鎖問題,它這個是需要我們顯示的釋放鎖
假如此時我們設(shè)置過期時間,但是我們用的是同一個key,就可能出現(xiàn)下一個線程刪除上一個線程的鎖,但是上一個線程還沒有執(zhí)行完,它這個需要key是不能重復(fù)的
假如我們既設(shè)置了過期時間也指定了不同的key,此時可能因為網(wǎng)絡(luò)延遲出現(xiàn)上一個線程刪除下一個線程的鎖,也就是說業(yè)務(wù)執(zhí)行的時間超過了鎖過期的時間,它這個需要一個鎖續(xù)命的功能
對于Redis它也有事務(wù),但是它的事務(wù)非常雞肋,僅僅只能保證多個指令按照順序執(zhí)行,并不能保證原子性,而且key還能被其他指令修改對應(yīng)的數(shù)據(jù)結(jié)構(gòu),所以我們選擇Redisson來進行分布式鎖的實現(xiàn),因為它提供了鎖續(xù)命的功能以及通過lua腳本保證了多個指令的原子操作,主要流程是這樣的
當線程搶到了鎖,假如業(yè)務(wù)沒執(zhí)行完,會定時去進行鎖續(xù)命,而其他線程會訂閱這個搶到鎖的線程的channel,然后自旋一定時間去嘗試獲取鎖,如果獲取鎖失敗,會被安排進入隊列中阻塞,一旦線程釋放鎖,他們會被通知到,然后繼續(xù)去自旋一定時間去嘗試獲取鎖,重復(fù)此操作
到此這篇關(guān)于Redis高并發(fā)分布鎖的示例的文章就介紹到這了,更多相關(guān)Redis高并發(fā)分布鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Ubuntu系統(tǒng)中Redis的安裝步驟及服務(wù)配置詳解
本文主要記錄了Ubuntu服務(wù)器中Redis服務(wù)的安裝使用,包括apt安裝和解壓縮編譯安裝兩種方式,并對安裝過程中可能出現(xiàn)的問題、解決方案進行說明,以及在手動安裝時,服務(wù)器如何添加自定義服務(wù)的問題,需要的朋友可以參考下2024-12-12
RedisDesktopManager無法遠程連接Redis的完美解決方法
下載RedisDesktopManager客戶端,輸入服務(wù)器IP地址,端口(缺省值:6379);點擊Test Connection按鈕測試連接,連接失敗,怎么回事呢?下面小編給大家?guī)砹薘edisDesktopManager無法遠程連接Redis的完美解決方法,一起看看吧2018-03-03
Linux、Windows下Redis的安裝即Redis的基本使用詳解
Redis是一個基于內(nèi)存的key-value結(jié)構(gòu)數(shù)據(jù)庫,Redis 是互聯(lián)網(wǎng)技術(shù)領(lǐng)域使用最為廣泛的存儲中間件,這篇文章主要介紹了Linux、Windows下Redis的安裝即Redis的基本使用詳解,需要的朋友可以參考下2022-09-09
Redis list 類型學(xué)習(xí)筆記與總結(jié)
這篇文章主要介紹了Redis list 類型學(xué)習(xí)筆記與總結(jié),本文著重講解了關(guān)于List的一些常用方法,比如lpush 方法、lrange 方法、rpush 方法、linsert 方法、 lset 方法等,需要的朋友可以參考下2015-06-06

