Redisson失效場景

一、失效場景說明

環(huán)境是Redis集群,下面主要列舉三種場景,其中場景一和場景二在開發(fā)過程中會經(jīng)常遇到。場景三出現(xiàn)的機率比較小,但是能加深我們對分布式鎖的理解。

二、失效場景場景一(Redisson)

在事務(wù)內(nèi)部使用鎖,鎖在事務(wù)提交前釋放

2.1 場景描述

假設(shè)有這樣一個需求:創(chuàng)建付款單,要求不能重復(fù)創(chuàng)建相同業(yè)務(wù)單號的付款單。為了保證冪等,我們需要判斷數(shù)據(jù)庫中是否已經(jīng)存在相同業(yè)務(wù)單號的付款單,并且需要加鎖處理并發(fā)安全性問題。

@Transactional
public void createPaymentOrderInnerLock(PaymentOrder paymentOrder){
    RLock lock = redissonClient.getLock(paymentOrder.getBizNo());
    //采用的redisson可重入鎖,提供watchdog機制,在鎖釋放前默認每10s重置鎖失效時間為30s
    lock.lock();
    try {
        LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
        paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
        //判斷數(shù)據(jù)庫中是否存在相同業(yè)務(wù)單號的付款單
        long count = this.count(paymentOrderLambdaQueryWrapper);
        //存在相同業(yè)務(wù)單號的付款單則拋異常
        if(count>0){
            throw new RuntimeException("不可重復(fù)提交付款單");
        }else{
            //無重復(fù)數(shù)據(jù),創(chuàng)建付款單
            this.save(paymentOrder);
            //其他DB操作
            ...
        }
    } finally {
            // 釋放鎖
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
    }
}
2.2 問題分析

上述問題的流程圖如下

1
2.3 解決方案

為了避免鎖在事務(wù)提交前釋放,我們應(yīng)該在事務(wù)外層使用鎖。

  • 方式一:在Controller層用Redisson,而不是在Service層用Redisson。
  • 方式二:在Service層用Redisson,不用聲明式事務(wù),而采用編程式事務(wù)(最小范圍控制事務(wù))。

三、失效場景場景二(非Redisson)

業(yè)務(wù)未執(zhí)行完,鎖超時釋放

3.1 場景描述

需求:創(chuàng)建付款單,要求不能重復(fù)創(chuàng)建相同業(yè)務(wù)單號的付款單

@Override
public void createPaymentOrderRenault(List<PaymentOrder> paymentOrderList){
    if(!CollectionUtils.isEmpty(paymentOrderList)){
        for (PaymentOrder paymentOrder : paymentOrderList) {
            /**
             * 采用公司框架提供的分布式鎖
             * 10---等待鎖釋放時間
             * 1---嘗試獲取鎖時間間隔
             * 5---鎖失效時間
             * 注意:此處設(shè)置鎖失效時間為5秒,在createPaymentOrderNoLock中睡眠5秒模擬耗時操作,此時會出現(xiàn)業(yè)務(wù)未執(zhí)行完,鎖超時釋放的問題
             */
            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);
        //模擬耗時操作...
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
3.2 問題分析

出現(xiàn)上述問題是因為在指定的鎖的失效時間內(nèi)(并且沒有續(xù)命機制),鎖內(nèi)部的業(yè)務(wù)代碼沒有執(zhí)行完,鎖超時釋放了。尤其我們財務(wù)端處于業(yè)務(wù)鏈下游,處理的數(shù)據(jù)量一般都比較大,交互的端比較多,尤其要注意這種情況。下列情形都有可能出現(xiàn)代碼沒有執(zhí)行完,鎖超時釋放的問題。

  • 鎖的失效時間設(shè)置的太短
  • 鎖的粒度太大,處理鏈路冗長
  • 鎖內(nèi)部包含很多耗時操作,比如遠程調(diào)用、大數(shù)據(jù)量處理等
3.3 解決方案

首先會想到,把失效時間設(shè)置長一點,確實可以。但設(shè)置多長合適呢,設(shè)置過長有可能存在拿到鎖的客戶端宕掉了,此時就要等鎖過期才能釋放,其他節(jié)點處于阻塞狀態(tài),降低了系統(tǒng)吞吐。又或者預(yù)估了一個失效時間在項目初期沒問題,隨著數(shù)據(jù)量增多,或者其他一些不確定因素造成了超時,也會出現(xiàn)問題。

可以采用類似Redisson的watchdog機制給鎖續(xù)命。另外,注意減小鎖的粒度,把存在并發(fā)安全性問題的關(guān)鍵代碼鎖住即可,增加系統(tǒng)吞吐量。同時也要注意減小事務(wù)的粒度,把查詢操作、甚至一些遠程調(diào)用放到事務(wù)外部(注意讀寫分離的情況),避免出現(xiàn)大事務(wù)問題。

四、失效場景場景三(非Redisson)

Redis節(jié)點主從切換

4.1 場景描述

我們在使用Redis時,一般會采用主從集群 + 哨兵的模式部署,這樣做的好處在于當(dāng)主庫異常宕機時,哨兵可以實現(xiàn)故障自動切換,把從庫提升為主庫,繼續(xù)提供服務(wù),以此保證可用性。
當(dāng)【主從發(fā)生切換】時,Redis分布鎖會存在安全性問題

  • 客戶端A從master獲取到鎖

  • 在master將鎖同步到slave之前,master宕掉了。

  • slave節(jié)點被晉升為master節(jié)點

  • 客戶端B取得了同一個資源被客戶端A已經(jīng)獲取到的同一個鎖。

4.2 問題分析

首先要說明一點,出現(xiàn)這種情形的概率是很低的。針對于這種情況,Redis的作者antirez設(shè)計出了RedLock算法,然而RedLock算法依賴時鐘正確性,存在爭議。

Redlock 必須「強依賴」多個節(jié)點的時鐘是保持同步的,一旦有節(jié)點時鐘發(fā)生錯誤,那這個算法模型就失效了。

  • 客戶端 A 獲取節(jié)點 1、2、3 上的鎖。由于網(wǎng)絡(luò)問題,無法訪問 4 和 5。
  • 節(jié)點 3 上的時鐘向前跳躍,導(dǎo)致鎖到期。
  • 客戶端 B 獲取節(jié)點 3、4、5 上的鎖。由于網(wǎng)絡(luò)問題,無法訪問 1 和 2。
  • 客戶端 A 和 B 現(xiàn)在都相信他們持有鎖。
4.3 Redisson棄用RedLock

起初Redisson也提供的RedLock的實現(xiàn),但在3.12.5版本后棄用了。

//redisson 3.12.5版本之前 RedLock 使用示例,基于RedissonMultiLock實現(xiàn)
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同時加鎖:lock1 lock2 lock3
// 紅鎖在大部分節(jié)點上加鎖成功就算成功。
lock.lock();
...
lock.unlock();

Redisson 的開發(fā)者認為 Redis 的紅鎖存在爭議,但是為了保證可用性,RLock 對象執(zhí)行的每個 Redis 命令執(zhí)行都通過 Redis 3.0 中引入的 WAIT 命令進行同步。WAIT 命令會阻塞當(dāng)前客戶端,直到所有以前的寫命令都成功的傳輸并被指定數(shù)量的副本確認。如果達到以毫秒為單位指定的超時,則即使尚未達到指定數(shù)量的副本,該命令也會返回。WAIT 命令同步復(fù)制也并不能保證強一致性,不過在主節(jié)點宕機之后,只不過會盡可能的選擇最佳的副本(slaves)。

4.4 解決方案

Redis分布式鎖在極端情況下,不一定是安全的。如果你對并發(fā)安全性帶來的問題零容忍,為了保證正確性,我們可以做一些兜底工作,
例如:

  • 建立唯一索引
  • 監(jiān)控、告警、提供補償方案

轉(zhuǎn)載自:Redis分布式鎖失效場景分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容