基本概念
一、樂觀鎖
總是認爲不會產生併發問題,每次去取數據的時候總認爲不會有其他線程對數據進行修改,因此不會上鎖,但是在更新時會判斷其他線程在這之前有沒有對數據進行修改,一般會使用版本號機制或CAS操作實現。
version方式:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
核心SQL代碼:
1 |
|
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);
}
}