MySQL InnoDB未显式加锁查询出现死锁问题咨询
死锁原因排查结论
这种场景下的S锁、X锁都不是应用显式创建的,本质是InnoDB自动加锁规则+Spring事务配置/代码逻辑疏漏触发的,Spring-JDBC本身不会主动生成行锁,所有行锁的加锁动作都由InnoDB完成。
触发S锁自动添加的常见原因
你执行的UPDATE table SET unindexedColum=1 WHERE pk=X语句正常走主键索引命中单行时,InnoDB本来应该直接给命中行加排他X锁,出现共享S锁通常是以下情况导致:
- 同事务内提前执行了共享锁读:在执行UPDATE之前,同一个事务里已经执行过
SELECT ... FOR SHARE(旧版本写法为SELECT ... LOCK IN SHARE MODE)语句,这类语句会自动给命中行加S共享锁。很多时候这类语句不是业务主动写的,可能是DAO层通用查询方法默认带了锁参数,或者关联逻辑里的查询被遗漏排查。 - 外键约束触发自动加锁:如果当前表的主键或者其他被关联字段,是其他子表的外键引用字段,InnoDB在做外键约束校验时,会自动给父表被关联的记录加S共享锁,这个逻辑不受你写的UPDATE语句控制。
- 索引失效导致扫描加锁:如果UPDATE语句因为隐式类型转换(比如主键是int类型传入了字符串参数)、字符集不兼容等问题没有走主键索引,转而走全表扫描/二级索引扫描,InnoDB会给扫描过程中碰到的所有记录先加锁再做条件判断,这个过程中也可能出现S锁残留。
- 触发器/存储过程隐式加锁:如果表上配置了UPDATE相关的触发器,触发器内部的锁定读语句也会自动给记录加S锁。
死锁形成的逻辑
你看到的死锁是典型的S锁升级X锁导致的环路等待:
- 线程1开启事务,先通过上述某一种场景拿到了目标记录的S共享锁
- 线程2执行相同的UPDATE语句,需要申请该记录的X排他锁,因为S锁和X锁互斥,线程2进入锁等待队列
- 线程1执行UPDATE语句,需要把自己持有的S锁升级为X锁,但因为锁等待队列里已经有线程2在等待X锁,InnoDB不允许S锁直接升级跳过等待的线程,线程1也进入阻塞状态
- 两个线程互相等待形成死锁,InnoDB会回滚开销更小的线程2,也就是你看到的抛出死锁错误、持有X锁的失败线程。
快速排查方案
- 执行
SHOW ENGINE INNODB STATUS拉取最近一次死锁的详细日志,日志里会明确记录两个事务分别持有、等待的锁类型,以及对应加锁的SQL语句,不要靠猜测定位。 - 检查Spring事务的隔离级别配置,如果业务没有强可重复读的需求,将隔离级别调整为
READ COMMITTED,该级别下InnoDB会提前释放扫描过程中不符合条件的记录锁,能大幅降低这类死锁概率。 - 检查表结构的外键配置、触发器配置,确认有没有隐式的约束加锁逻辑。
- 用
EXPLAIN解析你的UPDATE语句,确认执行计划确实走了主键索引,没有出现索引失效的问题。 - 梳理Spring声明式事务的边界,不要把非DB操作的逻辑包裹在事务范围内,避免锁持有时间过长导致多个线程锁交叉。
- 排查DAO层所有通用查询方法,确认没有默认拼接
FOR SHARE这类锁语句的逻辑。
内容的提问来源于stack exchange,提问作者Tobia
相关产品推荐
相关产品推荐

