樂觀鎖、悲觀鎖、分佈式鎖的概念及實現

基本概念

一、樂觀鎖
總是認爲不會產生併發問題,每次去取數據的時候總認爲不會有其他線程對數據進行修改,因此不會上鎖,但是在更新時會判斷其他線程在這之前有沒有對數據進行修改,一般會使用版本號機制或CAS操作實現。

 version方式:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

核心SQL代碼:

1

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

CAS操作方式:即compare and swap 或者 compare and set,涉及到三個操作數,數據所在的內存值,預期值,新值。當需要更新時,判斷當前內存值與之前取到的值是否相等,若相等,則用新值更新,若失敗則重試,一般情況下是一個自旋操作,即不斷的重試。

代碼示例:

public RefundOrderInfo saveRefund(PayOrderInfo payOrderInfo, FundRefundReq req) {
    try {
        PayOrderInfo updatePayOrderInfo = new PayOrderInfo();
        updatePayOrderInfo.setOrdNo(payOrderInfo.getOrdNo());
        //set新的狀態R1(期望更新爲的狀態,預期值)
        updatePayOrderInfo.setOrdSts(PayOrderInfoStsEnum.R1.getStatus());  
        updatePayOrderInfo.setMsgInfo(PayOrderInfoStsEnum.R1.getDesc());
        RefundOrderInfo refundOrderInfo = builderRefundOrdInfo(payOrderInfo, req);
        refundTransactionService.updatePayOrdInfoAndCreateRefund(updatePayOrderInfo, PayOrderInfoStsEnum.S, refundOrderInfo);
        return refundOrderInfo;
    } catch (DuplicateKeyException de) {
        log.error("重複入庫,rfdOrderId={},mercOrNo={}", req.getRfdOrderId(), req.getMercOrdNo(), de);
        throw new PFDException(RspCodeEnum.REPEATE_REQUEST);
    } catch (Exception e) {
        log.error("saveRefund 異常,rfdOrderId={},mercOrNo={}", req.getRfdOrderId(), req.getMercOrdNo(), e);
        throw e;
    }
}
 
 
//被調用的updatePayOrdInfoAndCreateRefund方法:
public void updatePayOrdInfoAndCreateRefund(PayOrderInfo payOrderInfo, PayOrderInfoStsEnum fromSts, RefundOrderInfo refundOrderInfo) {
    //fromSts.getStatus():S(內存值)     payOrderInfo.getOrdSts():R1 (新值)
    int result = payOrderInfoMapper.updateStatusFromOldStatus(payOrderInfo.getOrdNo(), fromSts.getStatus(), payOrderInfo.getOrdSts(), payOrderInfo.getMsgInfo());
    if (result == 0) {
        throw new UnexpectedRollbackException("PayOrderInfo更新表異常");
    }
    result = refundOrderInfoMapper.insertSelective(refundOrderInfo);
    if (result == 0) {
        throw new UnexpectedRollbackException("refundOrderInfo表入庫異常");
    }
}
//被調用的SQL方法
int updateStatusFromOldStatus(@Param("ordNo") String ordNo, @Param("oldOrdSts") String oldOrdSts, @Param("newOrdSts") String newOrdSts, @Param("msgInfo") String msgInfo);
//具體SQL
<update id="updateStatusFromOldStatus">
    UPDATE refund_order_info SET
    <if test="desc != null">
        msg_info = #{desc},
    </if>
    rfd_sts=#{newOrdSts}
    WHERE rfd_ord_no=#{rfdOrdNo} AND rfd_sts = #{oldSts}
</update>

二、悲觀鎖

總是假設最壞的情況,每次取數據時都認爲其他線程會去修改,所以都會加鎖(讀鎖、寫鎖、行鎖等),當其他線程想要訪問數據時,都需要阻塞掛起等待。可以依靠數據庫實現,如行鎖、讀鎖和寫鎖等,都是在操作之前加鎖,在Java中,synchronized的思想也是悲觀鎖;

排他性,只有當前進行加鎖的用戶,纔可以對被鎖的數據進行操作,賬務中悲觀鎖的使用相對較多,其他系統使用較少,因爲悲觀鎖影響性能。

代碼示例:

List<Acmtacin> selectByAcNoForUpdate(@Param("acNo") String acNo, @Param("acOrg") String acOrg);
 
 
//具體SQL,通過select xxx for update進行加鎖
<select id="selectByAcNoForUpdate" resultMap="AllColumnMap">
    SELECT AC_STS FROM ACMTACIN WHERE AC_NO = #{acNo} AND AC_ORG = #{acOrg} FOR UPDATE
</select>
List<Acmtacbl> acmtacblList = acmtacblMapper.selectByAcNoForUpdate(acNo, acOrg);
boolean isClose = true;
String closeSts = acmtacin.getAcSts();
//對鎖住的數據acmtacblList進行操作,一般是當事務釋放時,鎖被釋放;
if(acmtacblList.size() > 0){
    String capTypSts = "3";
    for(int i = 0; i<acmtacinList.size(); i++){
        Acmtacbl acmtacbl = acmtacblList.get(i);
        BigDecimal curAcBal = acmtacbl.getCurAcBal();
        if(curAcBal.compareTo(BigDecimal.ZERO) ==0){
            capTypSts = "1";
            closeSts = "1";
        }else{
            if("3".equals(acmtacin.getAcSts())){
                throw new BizException(BizError.BAL_NOT_ZERO_UN_CLOSEAC);
            }else if("N".equals(clsFun)){
                throw new BizException(BizError.BAL_NOT_ZERO_UN_CLOSEAC);
            }
            isClose =false;
        }
    }

三、分佈式鎖

比如redis鎖:通過查詢獲取key的方式實現加鎖,可以理解爲是悲觀鎖的一種;

每次對數據進行操作,先查詢key,如果查詢到key,即當前資源被佔用,不能進行操作;如果要加鎖的那個鎖key不存在的話,你就可以進行加鎖;

執行lock.unlock(),就可以釋放分佈式鎖;

代碼示例:

if (!redisService.lockDefaultTime(payOrderInfo.getMercId(), payOrderInfo.getOrdNo())) {
    log.warn("獲取鎖失敗返回當時訂單狀態,ordNo={}", payOrderInfo.getOrdNo());
    this.getRspByDB(payOrderInfo.getOrdNo(), fundPurchaseRsp);
    return;
}
 
//調用lock方法進行鎖處理
public boolean lockDefaultTime(String mercId, String mercOrderId) {
    return lock(mercId, mercOrderId, ExpireTimeConfig.DEFAULT_LOCK_SECONDS);
}
lock實現:

//加鎖的具體實現
public boolean lock(String mercId, String mercOrderId, int expireSeconds) {
    String lockKey = getLockKey(mercId, mercOrderId);
    try {
        for (int i = 0; i < RETRY_COUNT; i++) {  // 3次重試
            // value 設置爲到期時間
            String value = String.valueOf(System.currentTimeMillis() + expireSeconds * 1000L);
            boolean ret = this.setnx(lockKey, value);
            if (ret == false) {
                // 死鎖檢測 ------------------------------- begin
                String lockKeyValue = this.getStr(lockKey);
                // Case1: 鎖到期了,但沒釋放。
                if (lockKeyValue != null) {
                    // Case1-STEP1: 比較現在V.S.鎖的到期時間
                    long now = System.currentTimeMillis();
                    long oldLockTimeOut = Long.parseLong(lockKeyValue);
                    long newLockTimeOut = System.currentTimeMillis() + expireSeconds * 1000L;
                    // Case1-STEP2: 比較鎖是否已經失效
                    if (now > oldLockTimeOut + 1 * 1000L) { // 1秒的delay factor
                        log.error("死鎖檢測-檢測到鎖已經失效!");
                        // Case1-STEP3: 獲取上一個鎖的到期時間,並設置現在的鎖到期時間。
                        String oldTimeOutFromRedis = this.getAndSet(lockKey, String.valueOf(newLockTimeOut));
                        if (StringUtils.equals(String.valueOf(oldLockTimeOut), oldTimeOutFromRedis)) {
                            // Case1-STEP4: 獲取成功,設置失效時間.
                            this.setKeyExpire(lockKey, expireSeconds);
                            log.info("獲取鎖成功-死鎖檢測生效");
                            return true;
                        }
                    }
                } else {
                    Thread.sleep(50);
                    continue;
                }
                // 死鎖檢測 ------------------------------- end
                log.info("獲取鎖-失敗");
                return ret;
            } else {
                this.setKeyExpire(lockKey, expireSeconds);
 
                return true;
            }
        }
    } catch (Exception ex) {
        log.error("異常-redis設置鎖異常", ex);
        return false;
    }
 
    return false;
}

釋放鎖的實現:

//調用unLock進行鎖釋放
redisService.unLock(payOrderInfo.getMercId(), payOrderInfo.getOrdNo());
 
 
//unLock釋放鎖的實現
public void unLock(String mercId, String mercOrderId) {
    String lockKey = getLockKey(mercId, mercOrderId);
    try {
        this.delete(lockKey);
    } catch (Exception ex) {
        log.error("異常-釋放鎖異常, mercId={}, mercOrderId={}", mercId, mercOrderId, ex);
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章