☰
MySQL进程KILL不掉?事务提交回滚卡死的排查与处理
2026/10/19 10:39:54 网站建设 项目流程

做 MySQL 运维或者后端开发的人,几乎都会碰到一个特别憋屈的场面:一条 SQL 卡在那里跑不动,你想把它 KILL 掉,结果 KILL 指令发出去之后,线程在SHOW PROCESSLIST里确实变成了 Killed,但就是不退出;与此同时,这个连接上的事务既提交不了、也回滚不了,后面的请求全部堵住,数据库就像被人按了暂停键。

这种“MySQL 进程 kill 不掉,事务提交或回滚不了”的现象,在业务高峰期、大促前或者半夜跑批的时候特别常见。先别急着反复执行 KILL,也别一上来就重启数据库,搞不好问题会越搞越大。这篇就把我实际操作中踩过的坑、排查思路和处理手法摊开讲一讲,你如果正好遇到类似现场,可以直接照着这套流程去定位。

1. 先弄明白:KILL 到底干了什么,为什么它会失败

1.1 KILL 本质上只是“发信号”,不是强制终止

很多人对 KILL 有一个误解,以为它像操作系统里的kill -9一样,可以把一个线程直接抹掉。MySQL 里的 KILL 并不是这么设计的。它本质上是给目标线程设置一个“终止标志”,真正的清理动作还要由那个线程自己走到一个安全的检查点之后才能执行。

MySQL 在服务层把 KILL 分成了两种语义:

  • KILL QUERY <id>:只终止当前正在执行的查询,连接保持不断开,会话和事务状态保留。
  • KILL CONNECTION <id>(或者简写成KILL <id>):终止整个连接,包括查询、会话和连接状态。

无论是哪种,最终都要等目标线程“自己愿意退出”。InnoDB 引擎在执行 SQL 的过程中,会在某些循环节点检查这个终止标志。如果线程正卡在一个无法返回的地方——比如等待一个锁、等待一次磁盘 IO、或者在回滚一个巨大的事务——那它根本没有机会走到检查点,表现出来就是你 KILL 了,线程还在那里挂着。

MySQL 官方帮助文档里有一句话很关键:如果用户线程在存储引擎内部被卡住,KILL 命令可能无法立即生效,线程会一直持续到外部条件解除。这句话基本概括了我们在生产环境里遇到的所有尴尬情况。

1.2 事务为什么既提交不了、也回滚不了

KILL 一个连接之后,InnoDB 需要对该连接上未提交的事务做回滚处理,把已经修改的数据页恢复到事务开始之前的状态。这个恢复过程靠的是 undo log。理论上回滚是必然要发生的,但问题在于回滚本身可能是重活、慢活。

举个具体的例子:某个跑批任务往一张千万级别的表里批量 UPDATE,一次事务改了 500 万行。这个时候你去 KILL 它,KILL 信号是收到了,InnoDB 会立刻进入ROLLING BACK状态。但回滚需要逐条读取 undo log,再把原来的数据写回去,这个工作量基本和当初执行 UPDATE 的工作量是对等的,有时候甚至更慢,因为还要额外处理锁信息和清理操作。

在回滚完成之前,这个事务占用的行锁、间隙锁、MDL 锁全都还捏在手里。后果是什么呢?就是其他事务要么在等待这个锁,要么看到这个事务处于 ROLLING BACK 状态后产生死锁检测,而当前这个连接你又不能再发任何命令——因为它已经在清理途中了,不是不响应你,是没法响应你。最终你站在控制台前,左等右等,状态栏里那个 Killed 就是一直不消失。

1.3 KILL 失效的几种典型场景

这几种情况我都在真实环境里遇到过,你可以对号入座:

  • 大事务回滚中:事务修改行数几十万甚至上百万,KILL 后长时间显示 Killed 或者 Rolling back。
  • 等待 MDL 锁:一个会话持有了表的元数据锁,另一个会话被阻塞,你去 KILL 被阻塞的会话时,经常发现它半天没反应,因为它正卡在等待锁的环节,根本抽不出身去处理 KILL 信号。
  • 磁盘 IO 打满或磁盘故障:线程在等待 IO 完成,整个 InnoDB 都在等待 IO 子系统返回,KILL 指令排不上队。
  • 内存不足导致系统 swap 严重:mysqld 进程的线程都在等内存页换入换出,所有操作都很慢,KILL 也跟着一起慢。
  • 诊断卡死 / 数据字典锁:某些版本在特定操作下会卡在数据字典层面,线程无法响应终止信号。

2. 拿到现场之后,先判断它到底卡在哪一步

2.1 第一眼看 processlist,别只看有没有 Killed

很多人排查的第一步就是SHOW PROCESSLIST,但只看这一条不够。我一般会先执行SHOW FULL PROCESSLIST,把完整的 SQL 文本也带出来,因为 SQL 往往能直接告诉你它卡在什么地方。

SHOW FULL PROCESSLIST;

或者直接查 information_schema:

SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist ORDER BY time DESC;

重点关注 state 一列。不同的 state 代表完全不同的卡点:

  • Killed:说明 KILL 信号已经发出,正在等待回收。如果持续好几分钟,多半是事务回滚中,或者线程卡在 IO 等待。
  • Sending data:这是一个大坑,名字有迷惑性。它其实表示正在执行 SELECT 并把结果数据发到客户端,也可能是临时表排序、文件排序,大量数据读取时经常挂在这里。
  • Waiting for table metadata lock:典型的 MDL 锁等待,后面会有专节讲。
  • Waiting for handler commit:等待存储引擎完成提交,一般和 redo log 刷盘有关。
  • Rolling back:明确告诉你正在回滚一个大事务。

如果 processlist 里一堆Sleep状态的连接,也别忽略。一个处于 Sleep 状态的连接不代表它没有事务。应用层把 autoCommit 关掉之后,在同一个连接上发了多条 SQL,然后一直保持着连接不释放,这时候连接表面上是 Sleep,实际上事务可能已经开启很久了,持有大量锁。

2.2 查 InnoDB 事务视图,锁定真正的事务归属

从 processlist 只能看到线程,看不到事务。要想知道谁在真正占着资源,必须查information_schema.innodb_trx:

SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_rows_modified, trx_operation_state FROM information_schema.innodb_trx;

这里有几个字段我每次都会重点关注:

  • trx_started:事务开始时间。如果这个时间已经过去几个小时,这基本就是一坨陈旧大事务,优先级最高。
  • trx_state:状态如果是LOCK WAIT,说明事务在等锁;如果是RUNNING,说明事务正在执行;如果是ROLLING BACK,说明它在痛苦地回滚。
  • trx_rows_modified:修改了多少行,这个数字直接决定了回滚要花多久。几十万以上的行数,回滚时间基本是按分钟甚至小时算的。
  • trx_operation_state:正在做什么操作,比如Rolling back还是starting index read。

举个例子,我看到过最极端的一次:一个事务执行了 4 个多小时,修改了 800 万行,KILL 之后回滚了将近 3 个小时。这段时间里整个业务连这个表的写操作全部堵死,你敢信,这就是真实生产环境里正在发生的故障。

另外在 MySQL 8.0 里,还可以用performance_schema.data_lock_waits看更细的锁等待细节,8.0 对锁信息的采集比 5.7 全面很多。

2.3 查锁等待链,找到谁在阻塞谁

如果事务状态是LOCK WAIT,就一定要把阻塞链找出来。常用的查询是 sys 库自带视图:

SELECT * FROM sys.innodb_lock_waits;

这个视图会告诉你哪个事务在等待,哪个事务在持有锁。类似于链式结构,A 等 B,B 等 C,最后找到根因的那个人。我把这个视图比作“交通拥堵时的探头画面”,它能直接拍到你前面那辆车为什么不走。

还有一个辅助手段是SHOW ENGINE INNODB STATUS\G,它输出的LATEST DETECTED DEADLOCK和TRANSACTIONS段落能提供事务和锁的实时快照。虽然这个输出很长,但里面信息密度很高,尤其是事务状态和等待锁的类型,值得逐行读一遍。

SHOW ENGINE INNODB STATUS\G

如果 MySQL 8.0 的 performance_schema 里metadata_locks表可查,还可以直接查 MDL 锁的持有情况:

SELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, SOURCE FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA = '你的库名';

3. 分原因处理:从最轻量到最强制,按次序来

3.1 如果是 MDL 锁等待:杀被阻塞者没用,要去杀持有者

先说一个我见过很多次的误区。业务侧反馈某张表所有查询都超时,开发同学一看 processlist,发现一个线程卡在Waiting for table metadata lock,然后顺手就把它 KILL 了。结果 KILL 之后,下一个线程又卡进来,无限循环,原因就是在他们背后还蹲着一个持有 MDL 锁的连接。

MDL 锁是 MySQL 在 server 层对表结构加的锁,只要有会话拿着这张表的 MDL 锁,其他所有 DML 和 DDL 都会被挡在外面。最常见的持有者场景是:

  1. 一个连接里执行了ALTER TABLE,正在执行或者还没提交。
  2. 一个连接执行完查询,结果集还没完全发给客户端,事务也没结束。
  3. 一个连接长时间处于 Sleep,但事务没有提交,把 MDL 锁一直攥着。

处理 MDL 锁等待的正确思路是:先找到持有者,再考虑怎么处理持有者。查询持有者的方法在 2.3 节已经写过,核心就是通过sys.schema_table_lock_waits去看阻塞关系。

SELECT * FROM sys.schema_table_lock_waits\G

从输出中能看到waiting_thread_id和blocking_thread_id,直接按照blocking_thread_id去 processlist 里找对应连接,再评估这个连接能不能被杀。

需要特别注意的是,如果持有 MDL 锁的会话只是 Sleep,上面还带着一个未提交事务,贸然 KILL 它也会进入回滚流程。所以在杀持有者之前,最好先看一眼information_schema.innodb_trx里对应的trx_rows_modified,评估一下杀它的代价。

3.2 如果是大事务回滚:KILL 成功只是万里长征第一步

当你对一个正在执行大 UPDATE 的连接执行 KILL CONNECTION 后,连接状态会变成 Killed,但真正的大戏才刚刚开始。InnoDB 会启动回滚流程,此时你不能再对它做什么了,唯一能做的就是监控回滚进度。

MySQL 从 5.7 开始,在SHOW PROCESSLIST里可以看到 state 列为Rolling back,同时information_schema.innodb_trx里对应事务的trx_state也是ROLLING BACK。但具体回滚到百分之多少,MySQL 本身没有直接的进度接口。

我用的土办法是盯着两个指标:

  • trx_rows_modified的数值变化,它会随着回滚的推进逐渐减少,最终归零。
  • processlist 里事务是否消失。

我写过一个粗糙的监控脚本,大概就是隔 10 秒去查一次innodb_trx,把 trx_rows_modified 打印出来,观察递减速度,用来估算剩余回滚时间。这个方法不精确,但在现场足够用了。

SELECT trx_id, trx_state, trx_started, trx_rows_modified, NOW() AS check_time FROM information_schema.innodb_trx WHERE trx_state = 'ROLLING BACK';

回滚期间需要做什么?我的建议是:

  1. 别再去 KILL 其他相关事务,等它回滚完。
  2. 把该表的写入流量暂时切换走,或者让应用侧做降级。
  3. 通知业务方,明确告知这个表会有多长时间不可写。

千万注意:回滚大事务期间,如果你再去重启 MySQL,重启后 InnoDB 的崩溃恢复还是会把这个未完成事务再回滚一遍,该等的时间一分都不会少,甚至因为 crash recovery 要处理的问题更多,恢复时间可能更长。

3.3 如果是锁等待和死锁:回滚也有自己的优先级

如果事务卡在LOCK WAIT,KILL 通常是能成功的,因为 MySQL 会主动唤醒等待锁的线程,让它放弃等待并回滚事务。但如果你发现 KILL 之后它还是不动,就要考虑是不是 InnoDB 内部死锁检测机制出了状况。

MySQL 默认开启了死锁检测,检测到死锁时会回滚一个代价更低的事务。但死锁检测本身在大量事务竞争时会消耗不少 CPU,而且它和锁等待的冲突处理偶尔会表现得比较迟钝。这时候真正的问题不是 KILL 失效,而是你该直接处理持有锁的那一方。

找到持锁方之后,无非是两个方向:

  • 持锁方是零星的小事务,且业务可以中断,直接 KILL。
  • 持锁方是大事务,KILL 的代价太高,看是否能等它在正常周期内提交。

这里有一个经验法则:如果持锁方已经运行了很久,并且业务对这个事务的容忍度很低,宁可牺牲这个小事务来解局,也不要让它拖垮整个实例。现场处置的第一优先级永远是“恢复可用性”,而不是“保住所有事务”。

3.4 如果是物理资源耗尽:杀线程之前先看 IO 和负载

有时候你 KILL 不掉不是 MySQL 的问题,而是整个机器的 IO 已经打满了。mysqld 的线程全在等待磁盘返回,包括执行 KILL 的回包也可能排队。

排查命令其实不复杂:

iostat -x 1 10 vmstat 1 10 free -h

重点关注%util是否接近 100%,wa是否居高不下。如果 IO 已经 100% 打满,你这时候 KILL 一个大查询,可能反而加剧回滚导致的写放大,因为回滚也是要写数据页的。

处理优先级应该是:

  1. 先看是什么在消耗 IO(可能是其他实例、备份任务、或者当前实例的某个大查询)。
  2. 把消耗 IO 的外部任务暂停,给数据库喘口气。
  3. 然后再决定要不要 KILL 目标查询。

如果是因为内存不足导致 swap,free -h能看出来。swap 严重时,任何操作都慢如蜗牛,KILL 信号也照样排队。这个场景的处理往往需要重启才能缓解,但在重启之前要先把磁盘上的脏状态想清楚。

4. 最坏情况下的兜底方案

4.1 让 InnoDB 自己完成恢复:innodb_force_recovery 的正确用法

如果前面所有手段都无效,事务回滚就是死活完不成,或者连 processlist 都查不出来了,那就只剩下重启和强制恢复两条路。而innodb_force_recovery是最后的基于数据库自身的兜底手段。

参数有 1 到 6 六个等级。简单说:

  • 1:忽略损坏页继续尝试恢复,一般不会造成数据丢失。
  • 2:阻止主线程运行。
  • 3:回滚不执行,只恢复到一致状态。
  • 4:不计算统计信息。
  • 5:不执行 undo log 回滚,启动后数据库以只读方式访问。
  • 6:连日志也不做前滚,通常是最不得已时才用。

注意:5 和 6 级别的启动是以牺牲数据一致性为代价的,启动后数据库基本是只读的,业务不可能正常运行。它们只是在数据救急时用来把表数据导出来。

我的建议是:从 1 开始试,逐级往上,能到哪一级就停在哪一级。不要上来就设 6,因为级别越高,数据丢失风险越大。设置方法:

[mysqld] innodb_force_recovery = 4

在这个模式下启动后,目标不是让业务恢复,而是把数据尽量抢救出来,比如用mysqldump导出,或者直接复制数据文件。抢救完毕,立刻把这个参数移除,再正常重启。

4.2 系统层面强制结束 mysqld 进程的利弊

当你发现连mysqladmin shutdown都执行不动的时候,就必须考虑系统层面的处理了。有两条路:

一是给 mysqld 发 SIGTERM,让 MySQL 走优雅关闭流程:

kill $(cat /var/run/mysqld/mysqld.pid)

优雅关闭会等待所有事务处理完成。如果问题本身就出在一个回滚不完的大事务身上,优雅关闭一样会卡住,可能等多久都关不掉。

二是发 SIGKILL:

kill -9 $(cat /var/run/mysqld/mysqld.pid)

这个操作会直接终止进程,但不要天真地以为这样就不用回滚了。进程重启后,InnoDB 的崩溃恢复机制会扫描 redo log 和 undo log,那些未提交事务照样要回滚。换句话说,大事务该占用的时间还是占用,只是把时间从“业务持续不可用”变成了“重启后恢复阶段不可用”。

好处是什么?好处是你能争取到一个相对明确的恢复窗口,而且可以在重启前做一次整体环境的梳理,比如扩充磁盘、调整参数。坏处是如果数据文件有损坏,崩溃恢复可能失败,要配合 innodb_force_recovery 使用。

我的现场经验是:大事务回滚超过 3 个小时还看不到头,并且业务已经完全不可用,与其干等,不如果断 kill -9 后重启。因为回滚迟早要做,晚做不如早做,至少给业务一个明确的恢复预期。

4.3 重启后的恢复时间预估与观测点

重启之后,别急着接流量。先看日志,InnoDB 恢复阶段会打印类似Applying batch of N row changes的信息。

tail -f /var/log/mysql/error.log

重点观察:

  • InnoDB: Starting recovery之后是否快速推进。
  • InnoDB: Rollback of uncommitted transaction如果出现了,说明事务回滚开始了。
  • 日志最后出现InnoDB: Recovery completed才算恢复结束。

恢复期间尽量不要去操作数据库,甚至不需要登录,因为它忙不过来。等恢复完成后,再检查一次information_schema.innodb_trx,确认没有残留事务,然后逐步放开业务流量。

我见过有人在恢复期间强行打开数据库查询,结果把恢复进程干扰了,导致数据文件损坏范围扩大,最后只能从备份恢复,这是真的很惨痛的教训。

5. 现场排查速查表与长期避坑经验

5.1 症状-原因-处理对照表

我把常见情况整理成一张表,你可以在现场直接对照,省得临时想:

现象可能原因优先级动作
线程显示 Killed 持续不消失大事务回滚中查 innodb_trx 估算回滚行数和时间
线程显示 Waiting for table metadata lock有会话持有 MDL 锁查 schema_table_lock_waits,杀持有者
线程显示 LOCK WAIT锁等待或死锁查 sys.innodb_lock_waits,定位阻塞源头
实例整体响应缓慢,KILL 没反应IO 瓶颈或 swap用 iostat/vmstat 确认资源,先解除资源瓶颈
事务删不掉也提交不了,state=ROLLING BACK回滚耗时过长评估继续等待 vs kill -9 重启
重启后仍无法启动数据文件损坏从 innodb_force_recovery=1 开始逐级尝试

这张表我直接贴在了自己的笔记本内页,每次处理类似问题都会先看一眼,从最轻量、最安全的手段开始,而不是一上来就重启。

5.2 怎么从源头避免“KILL 不掉”的局面

说句实话,等到 KILL 不掉再处理,已经是被动了。主动防御的价值远大于事后救火。我复盘过几次重大故障,发现根源几乎没有意外,全是大事务、长事务和锁使用不当。

几个我现在一直在用的检查动作:

第一,监控大事务。数据库中设置一个定时任务,每 5 分钟扫一次information_schema.innodb_trx,凡是trx_started超过 10 分钟的事务都要告警。重点看trx_rows_modified,超过某个阈值(我一般设 5 万行)直接推送给值班人员。

第二,拆分批量操作。开发同学喜欢一个 UPDATE 更新全表百万行,执行很快,但回滚很慢。我要求所有批量更新改成按主键分批执行,每次只更新几千行,然后提交事务。这样即使出问题,KILL 掉也只是损失一批,不会把所有业务拖死。

第三,调整合理的锁等待超时。innodb_lock_wait_timeout默认 50 秒,对于在线交易系统来说太长。很多公司把它调到 5 秒甚至 3 秒,让锁等待的事务快速失败,而不是无限堆积。当然这个值也不能太小,否则业务稍微有一点高峰就会大量报错,需要根据业务特点反复打磨。

第四,严格排查长连接里的隐式事务。应用的连接池如果没有正确提交事务,或者中间件把 autocommit 改了,就会产生很多 Sleep 状态但持有事务的连接。这种问题难发现,但危害极大。我建议在数据库侧开启init_connect=‘SET autocommit=1’来兜底大部分场景。

第五,在全量 ALTER 操作前先评估 MDL 锁的影响。特别是大表加索引、加字段,建议使用在线 DDL 工具,并且避开业务高峰期执行。

5.3 几点个人体会

后面这一点是我个人在实际处理故障中最深的感触,算不上什么官方建议,纯粹是经验之谈。

第一,遇到 KILL 不掉的情况,心态上先别慌,因为你越是反复 KILL,越容易把 InnoDB 的内部分布式事务状态搞得更复杂。KILL 一次就够了,剩下的交给回滚。

第二,动手之前先把现场信息收集齐。processlist 快照、innodb_trx 快照、锁等待快照、引擎状态快照,这四样是后续做分析和复盘的基础材料。现场信息比任何记忆都可靠。

第三,杀进程之前永远先确认有备份或者至少知道备份集在哪里。如果数据库因为强制恢复之后数据不完整,你至少还有退路。

最后再分享一个小技巧:如果你在生产环境里需要快速判断一个 KILL 是否真的生效,可以看performance_schema.threads里的PROCESSLIST_STATE字段,如果它停在 Killed 上超过 30 秒,基本上就说明进入回滚流程了,这个时候你盯着 processlist 也没用,不如赶紧评估回滚时间、通知业务方,然后把监控脚本跑起来。这个内容后续还可以扩展的方向,是把事务监控接入统一的告警平台,让 KILL 不掉这种事在发生之前就被拦截下来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询