一、失效場(chǎng)景說(shuō)明
環(huán)境是Redis集群狞谱,下面主要列舉三種場(chǎng)景,其中場(chǎng)景一和場(chǎng)景二在開(kāi)發(fā)過(guò)程中會(huì)經(jīng)常遇到禁漓。場(chǎng)景三出現(xiàn)的機(jī)率比較小跟衅,但是能加深我們對(duì)分布式鎖的理解。
二播歼、失效場(chǎng)景場(chǎng)景一(Redisson)
在事務(wù)內(nèi)部使用鎖伶跷,鎖在事務(wù)提交前釋放
2.1 場(chǎng)景描述
假設(shè)有這樣一個(gè)需求:創(chuàng)建付款單,要求不能重復(fù)創(chuàng)建相同業(yè)務(wù)單號(hào)的付款單荚恶。為了保證冪等撩穿,我們需要判斷數(shù)據(jù)庫(kù)中是否已經(jīng)存在相同業(yè)務(wù)單號(hào)的付款單磷支,并且需要加鎖處理并發(fā)安全性問(wèn)題谒撼。
@Transactional
public void createPaymentOrderInnerLock(PaymentOrder paymentOrder){
RLock lock = redissonClient.getLock(paymentOrder.getBizNo());
//采用的redisson可重入鎖,提供watchdog機(jī)制雾狈,在鎖釋放前默認(rèn)每10s重置鎖失效時(shí)間為30s
lock.lock();
try {
LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
//判斷數(shù)據(jù)庫(kù)中是否存在相同業(yè)務(wù)單號(hào)的付款單
long count = this.count(paymentOrderLambdaQueryWrapper);
//存在相同業(yè)務(wù)單號(hào)的付款單則拋異常
if(count>0){
throw new RuntimeException("不可重復(fù)提交付款單");
}else{
//無(wú)重復(fù)數(shù)據(jù)廓潜,創(chuàng)建付款單
this.save(paymentOrder);
//其他DB操作
...
}
} finally {
// 釋放鎖
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
2.2 問(wèn)題分析
上述問(wèn)題的流程圖如下
2.3 解決方案
為了避免鎖在事務(wù)提交前釋放,我們應(yīng)該在事務(wù)外層使用鎖。
- 方式一:在
Controller
層用Redisson辩蛋,而不是在Service層用Redisson呻畸。 - 方式二:在Service層用Redisson,不用聲明式事務(wù)悼院,而采用編程式事務(wù)(最小范圍控制事務(wù))伤为。
三、失效場(chǎng)景場(chǎng)景二(非Redisson)
業(yè)務(wù)未執(zhí)行完据途,鎖超時(shí)釋放
3.1 場(chǎng)景描述
需求:創(chuàng)建付款單绞愚,要求不能重復(fù)創(chuàng)建相同業(yè)務(wù)單號(hào)的付款單
@Override
public void createPaymentOrderRenault(List<PaymentOrder> paymentOrderList){
if(!CollectionUtils.isEmpty(paymentOrderList)){
for (PaymentOrder paymentOrder : paymentOrderList) {
/**
* 采用公司框架提供的分布式鎖
* 10---等待鎖釋放時(shí)間
* 1---嘗試獲取鎖時(shí)間間隔
* 5---鎖失效時(shí)間
* 注意:此處設(shè)置鎖失效時(shí)間為5秒,在createPaymentOrderNoLock中睡眠5秒模擬耗時(shí)操作颖医,此時(shí)會(huì)出現(xiàn)業(yè)務(wù)未執(zhí)行完位衩,鎖超時(shí)釋放的問(wèn)題
*/
try (AutoReleaseLock lock = acquireLock(paymentOrder.getBizNo(), 10, 1, 5, TimeUnit.SECONDS)) {
if(lock != null) {
paymentOrderService.createPaymentOrderNoLock(paymentOrder);
} else {
log.info("未獲取到鎖!");
}
}catch (CacheParamException e) {
log.info("獲取鎖失敗");
}
}
}
}
@Override
@Transactional
public void createPaymentOrderNoLock(PaymentOrder paymentOrder) {
LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
long count = this.count(paymentOrderLambdaQueryWrapper);
if(count>0){
log.info("不可重復(fù)提交付款單");
throw new RuntimeException("不可重復(fù)提交付款單");
}else{
this.save(paymentOrder);
//模擬耗時(shí)操作...
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.2 問(wèn)題分析
出現(xiàn)上述問(wèn)題是因?yàn)樵谥付ǖ逆i的失效時(shí)間內(nèi)(并且沒(méi)有續(xù)命機(jī)制)熔萧,鎖內(nèi)部的業(yè)務(wù)代碼沒(méi)有執(zhí)行完糖驴,鎖超時(shí)釋放了。尤其我們財(cái)務(wù)端處于業(yè)務(wù)鏈下游佛致,處理的數(shù)據(jù)量一般都比較大贮缕,交互的端比較多,尤其要注意這種情況俺榆。下列情形都有可能出現(xiàn)代碼沒(méi)有執(zhí)行完跷睦,鎖超時(shí)釋放的問(wèn)題。
- 鎖的失效時(shí)間設(shè)置的太短
- 鎖的粒度太大肋演,處理鏈路冗長(zhǎng)
- 鎖內(nèi)部包含很多耗時(shí)操作抑诸,比如遠(yuǎn)程調(diào)用、大數(shù)據(jù)量處理等
3.3 解決方案
首先會(huì)想到爹殊,把失效時(shí)間設(shè)置長(zhǎng)一點(diǎn)蜕乡,確實(shí)可以。但設(shè)置多長(zhǎng)合適呢梗夸,設(shè)置過(guò)長(zhǎng)有可能存在拿到鎖的客戶端宕掉了层玲,此時(shí)就要等鎖過(guò)期才能釋放,其他節(jié)點(diǎn)處于阻塞狀態(tài)反症,降低了系統(tǒng)吞吐辛块。又或者預(yù)估了一個(gè)失效時(shí)間在項(xiàng)目初期沒(méi)問(wèn)題,隨著數(shù)據(jù)量增多铅碍,或者其他一些不確定因素造成了超時(shí)润绵,也會(huì)出現(xiàn)問(wèn)題。
可以采用類(lèi)似Redisson的watchdog機(jī)制給鎖續(xù)命胞谈。另外尘盼,注意減小鎖的粒度憨愉,把存在并發(fā)安全性問(wèn)題的關(guān)鍵代碼鎖住即可,增加系統(tǒng)吞吐量卿捎。同時(shí)也要注意減小事務(wù)的粒度配紫,把查詢操作、甚至一些遠(yuǎn)程調(diào)用放到事務(wù)外部(注意讀寫(xiě)分離的情況)午阵,避免出現(xiàn)大事務(wù)問(wèn)題躺孝。
四、失效場(chǎng)景場(chǎng)景三(非Redisson)
Redis節(jié)點(diǎn)主從切換
4.1 場(chǎng)景描述
我們?cè)谑褂肦edis時(shí)底桂,一般會(huì)采用主從集群 + 哨兵的模式部署括细,這樣做的好處在于當(dāng)主庫(kù)異常宕機(jī)時(shí),哨兵可以實(shí)現(xiàn)故障自動(dòng)切換戚啥,把從庫(kù)提升為主庫(kù)奋单,繼續(xù)提供服務(wù),以此保證可用性猫十。
當(dāng)【主從發(fā)生切換】時(shí)览濒,Redis分布鎖會(huì)存在安全性問(wèn)題
客戶端A從master獲取到鎖
在master將鎖同步到slave之前,master宕掉了拖云。
slave節(jié)點(diǎn)被晉升為master節(jié)點(diǎn)
客戶端B取得了同一個(gè)資源被客戶端A已經(jīng)獲取到的同一個(gè)鎖贷笛。
4.2 問(wèn)題分析
首先要說(shuō)明一點(diǎn),出現(xiàn)這種情形的概率是很低的宙项。針對(duì)于這種情況乏苦,Redis的作者antirez設(shè)計(jì)出了RedLock算法,然而RedLock算法依賴時(shí)鐘正確性尤筐,存在爭(zhēng)議汇荐。
Redlock 必須「強(qiáng)依賴」多個(gè)節(jié)點(diǎn)的時(shí)鐘是保持同步的,一旦有節(jié)點(diǎn)時(shí)鐘發(fā)生錯(cuò)誤盆繁,那這個(gè)算法模型就失效了掀淘。
- 客戶端 A 獲取節(jié)點(diǎn) 1、2油昂、3 上的鎖革娄。由于網(wǎng)絡(luò)問(wèn)題,無(wú)法訪問(wèn) 4 和 5冕碟。
- 節(jié)點(diǎn) 3 上的時(shí)鐘向前跳躍拦惋,導(dǎo)致鎖到期。
- 客戶端 B 獲取節(jié)點(diǎn) 3安寺、4厕妖、5 上的鎖。由于網(wǎng)絡(luò)問(wèn)題我衬,無(wú)法訪問(wèn) 1 和 2叹放。
- 客戶端 A 和 B 現(xiàn)在都相信他們持有鎖饰恕。
4.3 Redisson棄用RedLock
起初Redisson也提供的RedLock的實(shí)現(xiàn)挠羔,但在3.12.5版本后棄用了井仰。
//redisson 3.12.5版本之前 RedLock 使用示例,基于RedissonMultiLock實(shí)現(xiàn)
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同時(shí)加鎖:lock1 lock2 lock3
// 紅鎖在大部分節(jié)點(diǎn)上加鎖成功就算成功破加。
lock.lock();
...
lock.unlock();
Redisson 的開(kāi)發(fā)者認(rèn)為 Redis 的紅鎖存在爭(zhēng)議俱恶,但是為了保證可用性,RLock 對(duì)象執(zhí)行的每個(gè) Redis 命令執(zhí)行都通過(guò) Redis 3.0 中引入的 WAIT 命令進(jìn)行同步范舀。WAIT 命令會(huì)阻塞當(dāng)前客戶端合是,直到所有以前的寫(xiě)命令都成功的傳輸并被指定數(shù)量的副本確認(rèn)。如果達(dá)到以毫秒為單位指定的超時(shí)锭环,則即使尚未達(dá)到指定數(shù)量的副本聪全,該命令也會(huì)返回。WAIT 命令同步復(fù)制也并不能保證強(qiáng)一致性辅辩,不過(guò)在主節(jié)點(diǎn)宕機(jī)之后难礼,只不過(guò)會(huì)盡可能的選擇最佳的副本(slaves)。
4.4 解決方案
Redis分布式鎖在極端情況下玫锋,不一定是安全的蛾茉。如果你對(duì)并發(fā)安全性帶來(lái)的問(wèn)題零容忍,為了保證正確性撩鹿,我們可以做一些兜底工作谦炬,
例如:
- 建立唯一索引
- 監(jiān)控、告警节沦、提供補(bǔ)償方案
轉(zhuǎn)載自:Redis分布式鎖失效場(chǎng)景分析