propagation.requires_new与行锁冲突是死锁高发点:外层事务持锁未提交,内层独立事务尝试获取同一行锁被阻塞,而外层又依赖内层完成,形成闭环等待;根本诱因常是update未走索引导致锁范围扩大或隔离级别过高。

Propagation.REQUIRES_NEW 与行锁冲突是死锁高发点
当方法A用 @Transactional(默认 Propagation.REQUIRED)调用方法B,而B标注了 @Transactional(propagation = Propagation.REQUIRES_NEW),且两者都执行 UPDATE 同一张表的相同行(哪怕只是部分重叠),就极易触发死锁。根本原因不是事务“嵌套”,而是两个独立事务对同一资源加锁的时机错位:外层事务已持锁未提交,内层事务尝试获取相同行锁时被阻塞;而外层又依赖内层返回结果或完成才能提交——形成闭环等待。
常见错误现象:MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction,但日志里看不到明显循环等待链,只看到两个事务ID交替出现。
- 必须确认
UPDATE语句是否命中索引:若WHERE条件没走索引,InnoDB 会升级为next-key lock(记录锁+间隙锁),锁住整个范围,大幅增加冲突概率 - 检查事务传播行为是否真有必要:90% 的
REQUIRES_NEW场景其实只需用REQUIRED+ 显式TransactionStatus.setRollbackOnly()控制局部回滚 - 避免在同一个 Service 类中混合使用不同传播行为的方法——自调用会掩盖事务边界,让锁持有逻辑更难追踪
UPDATE 语句没走索引时,锁范围会失控
死锁常被误判为“并发太高”,实际根源往往是 SQL 没命中索引。InnoDB 在 UPDATE 或 DELETE 时,若无法定位到具体行,就会对扫描范围内的所有行加 X 锁,甚至升级为全表锁(尤其在 RR 隔离级别下)。这时哪怕两个事务只更新不同 ID,只要扫描路径重叠(比如都走主键全表扫描),就可能互相阻塞。
验证方式很简单:在 MySQL 中执行 EXPLAIN 查看执行计划,重点看 type 是否为 const 或 ref,key 是否显示实际使用的索引。
- 即使
WHERE id = ?看似走主键,也要确认字段类型匹配(如 Java 传入Long,DB 字段却是INT,会导致隐式转换,索引失效) - 复合索引要遵循最左前缀原则:
WHERE status = ? AND create_time > ?,若只用create_time条件,索引完全无效 - 避免在
WHERE中对字段做函数操作:WHERE DATE(create_time) = '2026-07-01'会让索引失效
事务隔离级别影响锁粒度和竞争窗口
Spring 默认事务隔离级别是 ISOLATION_DEFAULT,实际取决于 MySQL 配置(通常为 REPEATABLE READ)。这个级别下,InnoDB 使用 next-key lock 防止幻读,但也会扩大锁范围、延长锁持有时间,显著提升死锁概率。相比之下,READ COMMITTED 只对已修改的行加记录锁,不加间隙锁,锁更轻、释放更早。
不是所有业务都能降级隔离级别,但关键路径(如订单支付、库存扣减)应优先评估是否可接受 RC 级别——它能直接砍掉一半以上的死锁场景。
- 全局设置:在
DataSource的connectionProperties中添加transactionIsolation=TRANSACTION_READ_COMMITTED - 单方法设置:
@Transactional(isolation = Isolation.READ_COMMITTED) - 注意副作用:RC 下可能出现不可重复读,需确认业务逻辑是否依赖多次读取一致性(比如先查余额再扣减,中间被其他事务改写)
排查死锁不能只看应用日志
应用抛出 Deadlock found 异常时,MySQL 已经完成死锁检测并回滚了其中一个事务,但完整上下文只保留在 InnoDB 引擎状态里。光看 Java 日志,你只能知道“发生了”,不知道“谁锁了谁、为什么锁”。
必须立刻执行 SHOW ENGINE INNODB STATUS\G,它输出的最后一段 LATEST DETECTED DEADLOCK 才是真相:
- 里面会明确列出两个事务的 SQL、持有的锁、等待的锁、事务 ID、甚至线程 ID
- 重点关注
WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S)对应的行号和索引名 - 如果输出为空,说明死锁已被清理,需开启
innodb_print_all_deadlocks = ON持久化记录到 error log
真正棘手的不是单次死锁,而是反复出现在同一段代码、同一张表、同一组 ID 上的模式——那说明你的数据访问顺序或事务边界设计存在系统性缺陷,靠重试解决不了。











