乐观锁、悲观锁、分布式锁的概念及实现

基本概念

一、乐观锁
总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或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);
    }
}

 

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值