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è)置鎖過期時間, 刪除鎖時判斷是否當(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)) {
// 極端場景下:(執(zhí)行時間定格在9.99秒)突然卡頓 10ms or redis服務(wù)宕機?。?!
// 此時剛好鎖過期,自動刪除
// 其他線程獲取鎖,然后會把上個線程的鎖刪除,又會出現(xiàn)bug
stringRedisTemplate.delete(key);
}
問題
當(dāng)線程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ù)命功能,默認(rèn)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)阻塞所有進(jìn)入自旋代碼塊的線程(
這樣做的目的是為了不讓其他線程因為不停的自旋而給服務(wù)器造成壓力,所以讓其他線程先阻塞一段時間,等阻塞時間結(jié)束,再次自旋) - 獲取鎖的線程解鎖后,使用Redis的發(fā)布功能進(jìn)行發(fā)布消息,訂閱消息的線程調(diào)用release方法釋放阻塞的線程,再次嘗試獲取鎖
- 如果是調(diào)用Redisson的tryAcquire(1000,TimeUnit.SECONDS)方法,那么未獲取到鎖的線程不用進(jìn)行自旋,因為時間一到,未獲取到鎖的線程就會自動往下走進(jìn)入業(yè)務(wù)代碼塊

總結(jié)
Redis分布式鎖自己去實現(xiàn)可能會出現(xiàn)幾個問題
沒有在finally顯示釋放鎖,當(dāng)客戶端掛掉了,鎖沒有被及時刪除,這樣會導(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來進(jìn)行分布式鎖的實現(xiàn),因為它提供了鎖續(xù)命的功能以及通過lua腳本保證了多個指令的原子操作,主要流程是這樣的
當(dāng)線程搶到了鎖,假如業(yè)務(wù)沒執(zhí)行完,會定時去進(jìn)行鎖續(xù)命,而其他線程會訂閱這個搶到鎖的線程的channel,然后自旋一定時間去嘗試獲取鎖,如果獲取鎖失敗,會被安排進(jìn)入隊列中阻塞,一旦線程釋放鎖,他們會被通知到,然后繼續(xù)去自旋一定時間去嘗試獲取鎖,重復(fù)此操作
到此這篇關(guān)于Redis高并發(fā)分布鎖的示例的文章就介紹到這了,更多相關(guān)Redis高并發(fā)分布鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis哈希類型_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了redis哈希類型的常用方法及原理淺析,感興趣的朋友一起看看吧2017-08-08
jwt+redis實現(xiàn)登錄認(rèn)證的示例代碼
在登錄業(yè)務(wù)代碼中,當(dāng)用戶登錄成功時,生成一個登錄憑證存儲到redis中,本文主要介紹了jwt+redis實現(xiàn)登錄認(rèn)證的示例代碼,具有一定的參考價值,感興趣的可以了解一下2024-03-03
淺析PHP分布式中Redis實現(xiàn)Session的方法
這篇文章主要介紹了PHP分布式中Redis實現(xiàn)Session的方法,文中詳細(xì)介紹了兩種方法的使用方法,并給出了測試的示例代碼,有需要的朋友可以參考借鑒,下面來一起看看吧,2016-12-12

