<p>需查看SHOW ENGINE INNODB STATUS中LATEST DETECTED DEADLOCK部分的 (1) TRANSACTION和 (2) TRANSACTION下的trx_query字段,即为触发死锁的具体SQL语句;再结合MySQL thread id关联INFORMATION_SCHEMA.INNODB_TRX与PROCESSLIST确认是否来自某存储过程调用。</p>

怎么看存储过程里哪句SQL触发了死锁
存储过程本身不加锁,真正加锁的是它内部执行的 UPDATE、DELETE、SELECT FOR UPDATE 等语句。死锁日志里不会直接显示“来自存储过程xxx”,但会完整记录事务中最后几条 SQL,包括调用栈中的实际语句。关键看 SHOW ENGINE INNODB STATUS 输出里的 LATEST DETECTED DEADLOCK 区域:
- 每条
*** (1) TRANSACTION下的query id和query字段,就是正在执行/等待的那条 SQL —— 它可能就是存储过程体内的某一行 - 注意
MySQL thread id和OS thread handle,可结合INFORMATION_SCHEMA.INNODB_TRX中的trx_mysql_thread_id关联,确认该线程是否在执行某个存储过程(查PROCESSLIST的INFO列,有时能看到CALL proc_name(...)) - 如果存储过程用了动态 SQL(
PREPARE/EXECUTE),日志里显示的是拼出来的最终语句,不是原始CONCAT逻辑
为什么存储过程更容易出现交叉加锁顺序
存储过程常被多个业务路径复用,不同入口传入的参数组合可能导致锁行顺序不一致。比如一个扣库存的存储过程 proc_deduct_stock:
- 入口 A 调用
CALL proc_deduct_stock(1001, 2001)→ 先锁商品 1001,再锁订单 2001 - 入口 B 调用
CALL proc_deduct_stock(2001, 1001)→ 先锁商品 2001,再锁订单 1001 - 两个并发调用就构成经典循环等待
更隐蔽的是:存储过程内部 if 分支、循环、游标遍历顺序,若依赖输入参数或查询结果排序,也会导致加锁顺序不可控。没有显式 ORDER BY 的 SELECT ... FOR UPDATE 在 RR 隔离级别下,InnoDB 可能按索引物理顺序加锁,而该顺序不保证跨事务一致。
怎么验证存储过程是否走了索引导致锁扩大
存储过程里没走索引的 SQL,容易从行锁退化为间隙锁甚至全表扫描锁,大幅提高死锁概率。验证方法很直接:
- 把存储过程内关键 SQL 单独拿出来,在相同参数下执行
EXPLAIN FORMAT=TRADITIONAL,重点看type(是否为const/ref)、key(是否命中预期索引)、rows(扫描行数是否合理) - 特别注意
WHERE条件字段是否有索引,以及是否因隐式类型转换(如字符串字段传数字)、函数包裹(如WHERE DATE(create_time) = '2026-10-15')导致索引失效 - 在 RR 隔离级别下,范围查询(
>、BETWEEN、LIKE 'abc%')会触发Gap Lock或Next-Key Lock,日志里会明确标出lock_mode X locks gap before rec—— 这类锁极易与并发插入冲突
如何让存储过程事务更可控
存储过程默认继承调用者的事务上下文,一旦开启事务后调用它,整个过程就在同一个事务里。这带来两个风险:锁持有时间长、回滚粒度大。建议:
- 避免在存储过程中显式
START TRANSACTION/COMMIT,除非你明确需要自治事务(MySQL 8.0+ 支持CREATE PROCEDURE ... MODIFIES SQL DATA+DECLARE CONTINUE HANDLER做局部回滚,但慎用) - 业务层控制事务边界:调用前
BEGIN,调用后COMMIT或ROLLBACK,确保存储过程只做数据操作,不扛事务生命周期 - 对高并发场景的存储过程,考虑拆成更小粒度的原子操作,或改用应用层重试机制(捕获
Deadlock found when trying to get lock后重试整个业务逻辑) - 在开发阶段给存储过程加注释,标明它会更新哪些表、依赖哪些索引、是否可能持有间隙锁 —— 这些信息比代码本身更容易被忽略


















