mysql死锁后应用需捕获错误码1213并重试,避免长事务和无序加锁;通过innodb_trx定位问题事务,用kill query安全回滚;根治需统一多表加锁顺序、优化索引与业务逻辑。

死锁发生后业务卡住,怎么立刻让程序继续跑
MySQL检测到死锁后会自动回滚其中一方事务,但应用层如果没处理好异常,可能还在等那个被回滚的连接返回结果,导致请求挂起或超时。关键不是“等它自己恢复”,而是让应用快速感知失败、重试或降级。
实操建议:
- 应用代码里必须捕获
Deadlock found when trying to get lock这类错误(对应 MySQL 错误码1213),不能只 catchSQLException泛泛处理 - 重试逻辑要加指数退避,比如第一次 100ms 后重试,第二次 200ms,最多 3 次——避免重试风暴把死锁频率拉得更高
- 不要在事务里做 HTTP 调用、文件读写等长耗时操作,这类操作会让事务持有锁时间不可控,是死锁高发源头
写事务回滚脚本前先确认哪些事务真该回滚
不是所有“长时间运行的事务”都要强制 kill,盲目 rollback 可能导致数据不一致。重点盯两类:已卡住但没提交/回滚的事务、持有锁却无后续动作的事务。
查问题事务的命令:
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id,
trx_query, trx_weight
FROM information_schema.INNODB_TRX
WHERE trx_state = 'RUNNING' AND TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
注意:trx_weight 值越大,说明它修改/锁住的行越多,优先级越高;别一看到 trx_state = 'RUNNING' 就杀,有些只是大查询执行中,没锁表。
实操建议:
- 用
SHOW ENGINE INNODB STATUS\G看最近死锁详情,重点关注*** (1) WAITING FOR THIS LOCK TO BE GRANTED:和*** (2) HOLDS THE LOCK(S):两段,能直接定位冲突的 SQL 和事务 ID - 脚本里 kill 事务前,先记录
trx_mysql_thread_id和trx_query到日志,方便事后复盘是不是误杀了关键任务 - 避免用
KILL CONNECTION,改用KILL QUERY(只中断当前语句,不干掉整个连接),对连接池更友好
用 Python 写自动回滚脚本要注意的三个坑
很多脚本用 pymysql 或 mysql-connector-python 连上去就 KILL,结果发现根本杀不掉、或者杀错线程。核心问题是权限、连接隔离和状态判断不准。
实操建议:
- 执行
KILL的账号必须有SUPER或CONNECTION_ADMIN权限,普通PROCESS权限只能看,不能杀 - 别用同一个数据库连接去查事务再 kill 自己——MySQL 事务内不能 kill 当前连接的 thread_id,要用独立连接(比如新起一个
mysql.connector.connect()) - kill 前加一层校验:再次查
INNODB_TRX确认该trx_mysql_thread_id还存在且状态仍是RUNNING,防止查完瞬间事务已结束
为什么简单加 SELECT ... FOR UPDATE 就更容易死锁
不是锁得越细越安全,顺序错乱才是死锁主因。比如两个事务都按不同顺序更新 user 和 order 表,哪怕只锁一行,也会因加锁顺序不一致触发死锁。
实操建议:
- 所有涉及多表更新的事务,强制约定加锁顺序,比如统一按“先
user后order再payment”的物理表名排序,代码里用ORDER BY或显式SELECT ... FOR UPDATE预占锁 - 避免在事务里用子查询带出不确定顺序的 ID 列表,比如
SELECT id FROM order WHERE status=1 LIMIT 10返回顺序不固定,下次执行可能锁 A 表的第 3 行、B 表的第 7 行,和另个事务形成环路 - 用
innodb_lock_wait_timeout控制等待上限(默认 50 秒),比死等强;但别设太小(如 1 秒),否则正常慢查询也会被误判为死锁
死锁本身不可怕,可怕的是把它当成偶发异常忽略。真正难处理的是那些在高峰期批量任务里反复出现的“模式化死锁”——它们往往暴露了业务逻辑里的锁粒度、执行顺序或索引缺失问题,光靠脚本回滚只是止血,不是根治。











