踩坑记录-spring事务传播与MySQL行级锁
有这么一个业务场景:
用一个大的事务包裹住业务流程,保证数据可以安全回滚。但是万一真报错了,我又想不只是回滚,还要把错误原因、错误状态记录下来。
上述场景中,仅仅是一个大的事务注解,仅可以保证数据回滚,记录下来的错误报告也一并回滚了,无法正常使用。
因此我使用了以下设计:
事务A { try { ... } catch (e) { otherService.methodB() // 这里对methodB使用了@Transactional(propagation = Propagation.REQUIRES_NEW) // 当前事务的传播行为为:总是创建一个新事务,如果处于旧事务中,则挂起旧事务,创建新事务并执行 throw e; // 不能吞了异常,确保事务A正常回滚 } }
在一般情况下,上述功能已经可以正确记录错误原因,也会有回滚成功。但是我却遇到了一种特殊情况:
我在事务A里查出来的某条数据,将其状态改为“运行中”,正常情况下事务A的末尾会再将其状态修改为“运行成功”,为了确保数据一致性,我在methodB里将该数据状态修改为“运行失败”。
实际显示的日志是:执行了methodB的方法,
UPDATE x SET task_status=?,update_by=?,update_time=?,remark=? WHERE del_flag=0 AND (id = ?)
==> Parameters: 1(String), 1(String), 2026-02-25 (Timestamp), 运行失败(String), 2026022501(Long)
此时数据库的数据还停留在“运行中”,但是上述方法新事务明明已经执行了,并且理论上不受外层事务回滚影响,却没有正确执行,所以在一开始,我怀疑他也被回滚了。
但又迟迟没有返回期望的执行成功的语句:
DEBUG c.r.w.x.m.A.updateById - [debug,135] - <== Updates: 1
等待一段时间后,出现了一个报错日志:
org.springframework.dao.CannotAcquireLockException: ### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
造成这个现象的原因是:当业务异常时,在外层事务持有该条data的行锁,新事务只能等待外层事务释放锁。而外层事务又在挂起等待新事务结束,这就造成了死锁。还好MySQL有行锁等待超时,及时提供了报错信息。
问题总结:两个事务对同一行数据的更新产生了行锁竞争,导致MySQL超时。
解决的方法是:将第一次状态写入放在事务A之外,保证死锁不会发生。
后续经验:简化状态写入路径,避免同一个数据在多层事务中反复更新;谨慎使用REQUIRES_NEW传播,多留意是否会形成锁竞争。

浙公网安备 33010602011771号