业务上第一次遇到MySQL更新锁表超时(Lock wait timeout exceeded try restarting transaction)

本文探讨了在使用嵌套事务处理系统还款流程时遇到的数据库更新锁表超时问题。通过调整代码中事务操作的顺序,并正确设置内层事务的传播规则为NESTED,成功避免了锁表超时,确保了事务的一致性和原子性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:

最近,我在公司开发某个业务功能,需要使用钉钉审批流,在最后一个结点审批通过后,使用MQ监听回调信息,根据回调信息去更新申请单状态、触发系统还款、保存相关审批记录等。如果在完美情况下,这一套流程没有任何问题,但是因为历史数据等其他原因,触发系统还款可能会失败。此时,审批流程已经正常结束,不可能再发起一遍审批流程。为了解决这样的问题,我将申请单状态的“已还款”拆成“审批通过”和“已还款”。这两个状态在正常情况下,是同时完成的,但是如果系统还款有问题,只会更新项目状态和保存相关审批意见。从技术上来说我这里用到了嵌套型事务,处理最后一个节点的方法自身开启一个事务,该方法会调用系统还款的方法,系统还款的方法也会重新开启一个事务,这里系统还款方法的事务传播规则需要使用Propagation.REQUIRES_NEW(关于Spring事务,可以我看的《面试问你Spring事务怎么办,不会?点进来看一看》)。在此背景下,我遇到了数据库更新锁表超时的问题。

问题复现:

由于该系统的是内部核心系统,不能公布源码,我通过一个模拟例子,说明代码逻辑和演示出现数据库更新锁表超时的问题,模拟例子如下:

   /**
     * 模拟最后一个节点,需要的操作
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void lastNode() {
        
        //模拟更新申请单状态
        dictTwoDao.updateStatusById(21L,"已还款");
        try {
            //系统还款
            txService2.systemRepayment();
            //保存成功审批记录
            System.out.println("模拟成功审批记录");

        } catch (Exception e) {
            String errorMsg=e.getMessage();
            //保存失败审批记录
            System.out.println("模拟失败审批记录"+errorMsg);
        }

    }
   /**
     * 模拟系统还款操作
     */
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void systemRepayment() {

        //系统还款
        System.out.println("模拟系统还款");

        //模拟系统还款异常
        //throw  new RuntimeException("系统还款异常");

        //更新申请单状态
        dictTwoDao.updateStatusById(21L,"已还款");

        //更新其他状态
        System.out.println("模拟更新其他状态");

    }

运行结果: 

### Error updating database.  Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
### The error may involve com.hanxiaozhang.common.DictTwoDao.updateStatusById-Inline
### The error occurred while setting parameters
### SQL: update sys_dict set type=? where id=?
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

解决问题: 

我第一个想到是解决方法是,修改spring事务的隔离级别,让内层事务能获取到更新状态的变化,最后我运行了一下,没有解决我这个问题,我仔细想了一下,事务的隔离级别不能解决此问题,降低事务的隔离级别只能让其他事务的操作可以读取到还未提交的数据变更记录,不改变它的锁表超时问题。

然后,我开始对代码进行了分析,我发现,造成锁表超时的原因是:外层事务中进行了对表的更新操作锁住了表,外层事务还没有提交的时候,调用了内层事务,内层事务也要对表进行更新操作,此时,外层事务锁住该表,内层事务无法对事务进行提交,MySQL抛出了此异常。通过分析,我觉得改变我代码的操作顺序,把外层对表的更新操作放在调用内层事务方法的下面,结果就不会发生锁表超时,代码如下:

   /**
     * 模拟最后一个节点,需要的操作
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void lastNode() {
        
        try {
            //系统还款
            txService2.systemRepayment();
            //保存成功审批记录
            System.out.println("模拟成功审批记录");

        } catch (Exception e) {
            //模拟更新申请单状态
            dictTwoDao.updateStatusById(21L,"已还款");
            String errorMsg=e.getMessage();
            //保存失败审批记录
            System.out.println("模拟失败审批记录"+errorMsg);
        }

    }

后记: 

解决了锁表超时的问题,我再看代码,这种写法其实根本就不使用嵌套型事务,直接让所有的方法加入到当前事务,如果有异常,就捕获,然后处理异常消息就可以。

2020-05-05

学艺不精,出来害人喽,我这里使用嵌套事务的想法没有问题,但是内层事务的传播规则不应该使用REQUIRES_NEW,使用REQUIRES_NEW就会出现以上的问题,内层事务的传播规则应该使用NESTED。原因是NESTED与REQUIRES_NEW的嵌套事务本质区别是:NESTED还在一个事务中,它与主事务一块提交;而REQUIRES_NEW是新开启一个事务,独自提交。所以在一个事务中的NESTED可以修改同一条数据,不会出现上述的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hanxiaozhang2018

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值