前言

最近在 QA 环境测试订单退款流程审批通过的功能的时候看见 “Lock wait timeout exceeded; try restarting transaction” 的异常,但是这个业务逻辑并不复杂,怎么会出现锁等待超时的问题呢?跟着代码梳理了下整体的方法调用链,大致逻辑如下:


  1. 查询订单退款记录数据
  2. 更新订单退款记录的一些状态、备注等字段
  3. 调用订单退款服务
    1. 调用公共退款服务
    2. 根据退款发起结果以新事务的方式更新订单退款记录

其中第 3 步的根据退款发起结果以新事务的方式更新订单退款记录这一操作调用的方法上开启了事务并且指定了事务传播机制为 REQUIRES_NEW,再看第 2 步的操作,也是在对订单退款记录做更新,且更新时已经开启了事务,这下就明白问题所在了。

案发现场

问题代码如下(适当对方法和参数做了调整、注释,只展示了复现问题必要的一些逻辑):


订单退款记录服务:

@Transactional(rollbackFor = Exception.class)
@Override
public void auditPass(WorkflowOrderRefundAuditPassReq req) {
    String processInstanceId = req.getProcessInstanceId();
    TxOrderRefund orderRefund = this.checkAndReturnOrderRefundRecord(processInstanceId);
    // ... 其他业务逻辑 ...

    // 修改订单退款记录
    // orderRefund.setXXX();
    updateById(orderRefund);

    // 调用支付退款服务
    orderPayService.refundPay(orderRefund.getOrderCode());
}

支付退款服务:

@Transactional(rollbackFor = Exception.class)
public void refundPay(String orderCode) {
    // ... 其他业务逻辑 ...
    orderRefundService.updateOneNewTran(orderRefund);
}

其中 updateOneNewTran 方法上事务注解为:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)

破案

第 2 步更新订单退款记录时会将该条数据锁住,如果不是同一个事务其他对该条数据进行修改的操作必须要等待前一个事务结束才能获取到锁进行操作。
这里的第 3 步由于事务传播机制是 REQUIRES_NEW,因此会开启一个新的事务,而不是加入当前的事务,因此必须要等待第 2 步的操作结束将事务提交后才能成功拿到锁,但是第 2 步的操作和第 3 步操作在同一个大方法下,因此就形成了一个死循环:2 等着 3 执行完然后提交事务,3 又需要等 2 执行完才能拿到锁执行自己的事务,直到触发数据库的锁等待超时异常。

解决方案

解决方案也比较简单,只需要将第 3 步中的事务传播机制改为默认的 Requires 即可,让其加入已有事务而不是开启一个新的事务。

当然,还有其他解决方案,比如将第 2 步的订单退款记录更新操作放到第 3 步的退款操作中去,这样就不会存在两个事务竞争同一个锁的情况了。只要能破坏锁竞争的条件,都是可行的解决方案,根据实际情况选择一种合适的方案即可。

温故知新

  1. Spring 事务传播机制
  2. MySQL 学习笔记