Redisson+事务=失效的场景

1、失效场景1

假设有这样一个需求:创建付款单,要求不能重复创建相同业务单号的付款单。为了保证幂等,我们需要判断数据库中是否已经存在相同业务单号的付款单,并且需要加锁处理并发安全性问题。

@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());
        //判断数据库中是否存在相同业务单号的付款单
        long count = this.count(paymentOrderLambdaQueryWrapper);
        //存在相同业务单号的付款单则抛异常
        if(count>0){
            throw new RuntimeException("不可重复提交付款单");
        }else{
            //无重复数据,创建付款单
            this.save(paymentOrder);
            //其他DB操作
            ...
        }
    } finally {
            // 释放锁
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
    }
}

上述问题分析图如下:

解决方案:

为了避免锁在事务提交前释放,我们应该在事务外层使用锁。

  • 方式一:在Controller层用Redisson,而不是在Service层用Redisson。
  • 方式二:在Service层用Redisson,不用声明式事务,而采用编程式事务(最小范围控制事务)。

2、失效场景2

业务未执行完,锁超时释放

@Override
public void createPaymentOrderRenault(List<PaymentOrder> paymentOrderList){
    if(!CollectionUtils.isEmpty(paymentOrderList)){
        for (PaymentOrder paymentOrder : paymentOrderList) {
            /**
             * 采用公司框架提供的分布式锁
             * 10---等待锁释放时间
             * 1---尝试获取锁时间间隔
             * 5---锁失效时间
             * 注意:此处设置锁失效时间为5秒,在createPaymentOrderNoLock中睡眠5秒模拟耗时操作,此时会出现业务未执行完,锁超时释放的问题
             */
            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("不可重复提交付款单");
        throw new RuntimeException("不可重复提交付款单");
    }else{
        this.save(paymentOrder);
        //模拟耗时操作...
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

问题分析:

出现上述问题是因为在指定的锁的失效时间内(并且没有续命机制),锁内部的业务代码没有执行完,锁超时释放了。尤其我们财务端处于业务链下游,处理的数据量一般都比较大,交互的端比较多,尤其要注意这种情况。下列情形都有可能出现代码没有执行完,锁超时释放的问题。

  • 锁的失效时间设置的太短
  • 锁的粒度太大,处理链路冗长
  • 锁内部包含很多耗时操作,比如远程调用、大数据量处理等
解决方案:

首先会想到,把失效时间设置长一点,确实可以。但设置多长合适呢,设置过长有可能存在拿到锁的客户端宕掉了,此时就要等锁过期才能释放,其他节点处于阻塞状态,降低了系统吞吐。又或者预估了一个失效时间在项目初期没问题,随着数据量增多,或者其他一些不确定因素造成了超时,也会出现问题。

可以采用类似Redisson的watchdog机制给锁续命。另外,注意减小锁的粒度,把存在并发安全性问题的关键代码锁住即可,增加系统吞吐量。同时也要注意减小事务的粒度,把查询操作、甚至一些远程调用放到事务外部(注意读写分离的情况),避免出现大事务问题。

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值