根本原因是MySQL 8.0中未提交的悬挂事务在空闲连接下仍保持trx_state='RUNNING'并持续持有MDL锁和行锁,因wait_timeout默认长达8小时,导致ALTER TABLE等DDL被长期阻塞;需查INNODB_TRX中trx_query为空、trx_autocommit=0且trx_state='RUNNING'的记录定位持锁者。

为什么锁等待总在凌晨三点爆发
根本原因不是并发高,而是事务没提交就“睡着了”。MySQL 8.0 默认 wait_timeout = 28800(8 小时),连接空闲后不自动断开,但事务状态仍为 RUNNING、trx_query IS NULL,MDL 锁和行锁持续挂着。应用层用连接池复用连接时,这种悬挂事务极易卡住后续 DDL 或大查询。
必须同步调整三处:全局 wait_timeout 和 interactive_timeout 改为 300;应用连接池(如 HikariCP)配 connection-timeout=30000 和 idle-timeout=300000;Python/Java 客户端显式控制 autocommit,避免 ORM(如 SQLAlchemy、MyBatis)隐式开启长事务。
如何快速定位谁在持锁不放
别只看 SHOW PROCESSLIST —— 持锁线程往往显示为 Command = 'Sleep'、State = ''、Time 很大,但实际仍在占用锁资源。
- 先查
information_schema.INNODB_TRX,筛选:trx_state = 'RUNNING'且trx_query IS NULL且trx_autocommit = 0 - 再关联
performance_schema.metadata_locks,确认该trx_id是否对目标表持有LOCK_TYPE = 'SHARED_WRITE'或'EXCLUSIVE' - 最后用
SELECT * FROM performance_schema.data_lock_waits\G查等待链,blocking_trx_id指向的就是那个“睡着的持锁者”
ALTER TABLE 卡住时千万别直接 KILL
ALTER TABLE 需要 MDL_EXCLUSIVE 锁,但它与任何已存在的 MDL 锁(哪怕只是普通 SELECT 带来的 SHARED_READ)都互斥。盲目 KILL 会触发完整回滚,而回滚本身可能持续数分钟,期间继续阻塞其他 DDL。
更稳妥的操作顺序是:
- 先执行
KILL QUERY <code>thread_id(对 Sleep 连接虽无实际效果,但可排除它正在执行长查询的误判) - 等 15 秒,再查
INNODB_TRX状态是否变化 - 若仍为
RUNNING且trx_query IS NULL,再执行KILL <code>thread_id - 优先尝试
ALGORITHM=INSTANT(仅限ADD COLUMN、RENAME COLUMN、SET DEFAULT)绕过锁竞争
死锁日志里藏着最真实的执行路径
MySQL 默认不输出死锁详情,必须手动开启:SET GLOBAL innodb_print_all_deadlocks = 1,日志会写入 error log。每次死锁发生后,SHOW ENGINE INNODB STATUS\G 只保留最后一次记录,而配置开启后能长期留存分析依据。
关键要看日志里的三块内容:
-
*** (1) TRANSACTION和*** (2) TRANSACTION:两个冲突事务的trx_id、启动时间、持有锁类型(X/S/GAP)、锁定的索引行 -
*** WE ROLL BACK TRANSACTION (2):InnoDB 主动回滚的是哪个事务,说明它被判定为“代价更低” - 每条事务末尾的
mysql tables in use和locked tables:确认是否真有表级锁参与,还是纯行锁/间隙锁交叉
真正难调的不是锁本身,而是事务中混入了非数据库操作(比如 RPC 调用、JSON 解析、循环计算),让锁持有时间不可控 —— 这类逻辑必须从事务边界里剥离出去。


















