MySQL KILL命令为何失效?事务卡死与锁等待排查实战指南 1. 先把这个“杀不掉”的现象说清楚前阵子处理了一个把值班同事折磨到怀疑人生的 MySQL 案例生产库上有个进程发起了KILL命令线程状态也确实变成了Killed但会话就是赖在SHOW PROCESSLIST里不走。更麻烦的是这个线程卡住之后它所在的事务一直处于“提交不了也回滚不了”的悬空状态后续很多对这个表的读写操作全部排队连接池被占满业务接口大量超时。先说结论MySQL 的KILL命令本质上只是“通知线程该退出了”并不是操作系统层面的强制终结。线程收到指令后要主动去响应、去清理才能把资源释放出来。如果这个线程正卡在一个无法被打断的位置——比如等待元数据锁、等待 InnoDB 行锁、或者在回滚一个巨大事务——那它就会长时间停留在Killed状态表现成案发现场最常见的两个症状进程 kill 不掉事务交不出去也回滚不了。这个问题的本质是 MySQL 内部有很多“不能强拆”的逻辑关卡。理解了这几个关卡再去看KILL失灵、事务卡死、连接堆积这些现象其实都是同一件事的不同侧面。这篇文章我就从头到尾把这条链路拆开结合我实际踩过的坑说一下排查思路和处理顺序最后给一份可以直接保存的排查命令和参数清单。2. 为什么 KILL 命令会“哑火”三个核心阻塞点这节是全文最重要的一块。你如果理解了这三个阻塞点以后遇到类似问题基本就能“半分钟内给出判断方向”。2.1 元数据锁MDL互斥排队都排不上MySQL 从 5.5 开始引入了 MDLMetadata Lock元数据锁后来 5.7、8.0 对它做了大量完善。MDL 的作用是保证表结构定义的一致性当一个事务拿着某张表的 MDL 读锁时另一个线程想对这个表做 DDL比如ALTER TABLE、RENAME TABLE就必须等读锁释放。问题恰恰出在这个“等”上。DDL 线程在等待 MDL 时它自己的状态变成Waiting for table metadata lock看上去它只是在排队。可关键点是MySQL 的 MDL 等待队列有优先级等待写锁的线程一旦排在前面它后面所有想要获取同表 MDL 的读写请求都会被压住。于是一个迟到的ALTER TABLE可以把整张表的读写全部堵死。这就是典型的“一个线程拖死整条业务线”。更坑的是这样的 DDL 线程你用KILL杀它效果也常常不明显。因为它正阻塞在 MDL 等待上MySQL 的 kill 序列要等它先拿到执行的“入场券”或者退出等待才能继续响应终止指令。表现出来就是进程状态一会儿是Killed一会儿又是Waiting for table metadata lock反反复复事务永远得不到一个干净的终态。我在排查这种问题时第一反应是去performance_schema.metadata_locks表里看 MDL 等待图。它能直接告诉我们谁持有锁、谁在等待。2.2 InnoDB 行锁与超大事务回滚本身就在排长队MDL 只是表结构层面的关卡行锁则是数据层更常见的“杀手”。事务 A 更新了一大批行但迟迟不提交事务 B 想更新同一批行就只能等在information_schema.innodb_trx里状态为LOCK WAIT。这是最常见的锁等待场景。但我要强调的是对“kill 不掉”而言更容易迷惑人的场景是KILL已发出事务却进入了回滚阶段而这个回滚比你想象中慢得多。为什么慢因为 InnoDB 的更新操作不是简单改掉就完事的。它会把修改前的旧版本数据写进 undo log回滚的时候需要扫描 undo 里的历史版本逐条把数据“恢复”到事务开始时的样子。如果一个事务更新了几百万行那回滚时同样要处理几百万条 undo 记录。这个过程不是瞬间完成的可能需要几分钟、几十分钟甚至更久。注意这个阶段线程在事务控制里是“回滚中”但连接层可能还显示为Killed或Cleaning up。外部看就是KILL下去了事务一直没结束。我见过最夸张的一个案例某个批处理事务update了上千万行杀完之后回滚跑了 40 多分钟期间 CPU、IO 都被占满应用侧反馈和“数据库挂了”几乎没区别。2.3 磁盘 IO 和系统调用卡在 MySQL“管不着”的地方第三种场景常常被忽略线程并不是卡在锁上而是卡在 IO 上。比如磁盘控制器打满、云盘超限流、文件系统卡顿这时候一个线程可能正等待一次写盘操作返回。MySQL 线程状态会显示为Waiting for handler commit、Writing to net或Updating status之类的。这种场景下KILL发出后线程想退出但它正阻塞在操作系统层面的一个调用上MySQL 线程库没法强制打断。就像一个人正卡在电梯门口你喊他“出来”他也很想出来但门卡住了。这种情况靠 MySQL 命令无解需要先恢复底层 IO或者通过系统层手段处理掉这个连接。3. 从现象到定位一套可以按顺序执行排查流程光说原理不够得能动手。我习惯按下面四步走每一步都能筛掉一批可能性。3.1 第一步先看进程列表判读线程状态登录 MySQL 以后第一件事是执行SHOW FULL PROCESSLIST;注意这里一定要用FULL不然info列会被截断看不到完整 SQL。看的时候重点不是只看State而是结合Command和Info一起看。我给你一个判读口径状态组合含义优先级Command: QueryState: Waiting for table metadata lock在等 MDL常见于 DDL 被长事务挡着高Command: QueryState: Waiting for commit在等待存储引擎层提交完成可能卡在 IO高Command: QueryState: Killed已收到 kill 信号正在清理资源或等待锁释放极高Command: Sleep且事务未提交长事务持有锁源头之一极高大部分情况下我们能在这一步就看到“元凶线程”。但SHOW PROCESSLIST展示的信息有限比如它不显示事务开始时间也不显示锁的具体持有关系所以要继续往下看。3.2 第二步用 sys 库给会话和事务拍快照如果 MySQL 版本支持sys库5.7 默认就有8.0 也有我建议直接用这几条-- 查看所有正在执行的会话重点看当前执行的语句和状态 SELECT * FROM sys.session\G -- 只查看处于等待状态的会话 SELECT conn_id, user, db, command, state, time, current_statement FROM sys.session WHERE command IN (Query, Execute) AND state IS NOT NULL ORDER BY time DESC\Gsys.session本质上把performance_schema.threads和information_schema.processlist做了关联还给你算好了time和执行语句。这个视图最好用的地方是它有一个statement_latency可以直观看出哪个线程的当前 SQL 已经跑了多久。再往前一步直接看事务SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_runtime_sec, trx_mysql_thread_id, trx_rows_modified, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC\G这里最值得关注的是trx_rows_modified。如果这个数字很大说明事务已经修改了大量行这种事务被 kill 后一定会有一段漫长的回滚期。你心里有数就不会傻等。3.3 第三步把锁等待关系画出来对于 MDL 问题直接查SELECT * FROM performance_schema.metadata_locks;输出里能看到LOCK_STATUS: GRANTED和LOCK_STATUS: PENDING两种记录。PENDING 的线程通常就是被 KILL 之后还在等待 MDL 的那个。结合OWNER_THREAD_ID和OBJECT_NAME可以快速找到是谁在持有锁。对于 InnoDB 行锁可以直接用sys库的现成视图SELECT * FROM sys.innodb_lock_waits\G这个视图会把“谁在等锁、谁持有了锁、等待了多久、事务的 SQL 是什么”一次性展示出来。它是information_schema里一堆表的关联结果比自己拼 SQL 方便非常多。如果你要更原始的数据可以看事务与锁的关联状态SELECT lw.trx_id AS waiting_trx_id, lw.trx_mysql_thread_id AS waiting_thread, ls.trx_id AS holding_trx_id, ls.trx_mysql_thread_id AS holding_thread FROM information_schema.innodb_lock_waits AS lw JOIN information_schema.innodb_trx AS ls ON lw.blocking_trx_id ls.trx_id;拿到 waiting_thread 和 holding_thread 之后就能明确决定要“杀谁”。一般情况下我们会杀持有锁的源头事务而不是杀等待者。还有一个隐藏点MDL 的持有者往往不是当前正在执行的查询而是一个“早就空转但事务没提交”的会话。它在SHOW PROCESSLIST里显示为Sleep但performance_schema.metadata_locks里还挂着 GRANTED 的锁。这种最坑因为一眼看过去好像没人干活表却被锁得死死的。3.4 第四步量化 undo 和 IO 的压力判断大事务回滚有多“重”除了看trx_rows_modified还可以看 undo 的规模SHOW ENGINE INNODB STATUS\G在TRANSACTIONS段落里重点找undo log相关的描述。如果能看到类似trx id ... undo log entries ...的内容说明这个事务的回滚工作集非常庞大。同时配合系统层面看 IO 是否打满。Linux 下我会顺序执行iostat -x 1 iotop -o pidstat -d 1如果%util接近 100%且对应 MySQL 线程的IO读写很高那基本可以确定线程是困在磁盘 IO 上。4. 处理手段从温和手段到应急手段的完整顺序定位到问题后最忌讳的是直接上来就KILL因为你可能杀错对象也可能把问题从“锁等待”变成“回滚长跑”。正确顺序是先按从轻到重的方式处理。4.1 区分 KILL QUERY 和 KILL CONNECTIONMySQL 提供两种 kill 粒度-- 只终止当前正在执行的语句保留连接 KILL QUERY 12345; -- 终止连接同时也终止当前正在执行的语句 KILL 12345;日常建议先用KILL QUERY。为什么因为如果线程只是某一条 SQL 卡住了把它杀掉之后连接还在事务还可能继续存在如果事务里前面的操作也危险后续还能通过这个连接回滚。但如果你直接KILL连接事务一定会被终止并进入回滚。识别清楚当前是什么状态再动手能少踩坑。有一个经验如果一个线程正在等待 MDL而持有 MDL 的是另一个Sleep连接这时候杀等待者是没用的杀持有者才是关键。很多新手把KILL QUERY发给了等待者折腾半天状态还是老样子原因就是目标错了。4.2 先杀长事务再谈表和连接通常我会用下面这个顺序处理从sys.innodb_lock_waits里找出哪个线程是锁持有者。确定这个持有锁的事务是否可以安全中断。如果它只是个空事务比如应用开了事务忘记提交执行KILL QUERY。如果杀掉之后状态变成Killed但事务仍不结束说明进入了回滚。此时一般建议等待同时观察information_schema.innodb_trx中该事务的trx_state是否变为ROLLING BACK。如果回滚时间太长且业务已经不可用申请维护窗口重启 MySQL 实例可能是更务实的选择。第 4 步要解释清楚重启 MySQL 实例不是让你“逃避问题”而是因为 InnoDB 在做崩溃恢复时会对未完成事务的 undo 做更高效的并行处理有时比在线等它回滚更快。但重启前一定要确认案例状态确认是主库还是从库做好切换预案。4.3 用参数提前兜底避免再次发生处理完一轮必须把参数审查一遍。和这个案例强相关的参数有四个参数默认值常见版本建议innodb_lock_wait_timeout50改成 5-10 秒避免行锁等待过久lock_wait_timeout31536000一年改成 10-30 秒控制 MDL 等待innodb_rollback_on_timeoutOFF保持默认不把整事务强制回滚wait_timeout/interactive_timeout28800 等按业务设置短一点避免连接长时间空转特别是lock_wait_timeout很多人不知道它管的是 MDL 等待的超时时间默认值长达一年。等于说一个 DDL 可以傻等一年业务没感知数据库却已经瘫痪了。把它调短可以让 MDL 等待快速失败让 DDL 报错而不是无限阻塞。另外如果应用侧有大量连接长时间不释放要排查是不是连接池的minIdle和maxLifetime配置不合理。事务开启后不提交是数据库里最普遍的一类“隐形杀手”。4.4 关于事务本身的设计要复盘处理事务问题不能只盯着数据库侧应用侧才是源头。检查是否有“开了事务但忘了 commit”的代码路径尤其是异常分支里没有rollback。批量操作拆小批。单事务一次更新 100 万行一旦出问题回滚时间同样按百万行算拆成 1 万行一批任一环节失败损失都可控。如果必须跑大查询考虑只读从库避免在业务高峰期影响主库的并发事务。5. 高频问题速查与经验提醒最后给一张速查表几乎能把这次遇到的现象和解决方案概括进来排查时对照着看就行。5.1 高频“杀不掉”场景对照现象大概率原因首选操作Killed状态一直存在事务不消失大事务回滚耗时undo 量大等待量化回滚进度必要时重启Waiting for table metadata lockDDL 被长事务/Old会话持锁阻塞找performance_schema.metadata_locks杀持锁方Waiting for commit磁盘 IO 打满commit 等待落盘排查 IO降低写入量必要时重挂存储KILL之后应用立刻报错但旧连接仍占用连接池中的旧连接正处于回滚观察innodb_trx等待清理完成从库进程 kill 不掉复制 SQL 线程正在应用大事务或回滚操作暂停复制并评估再决定继续还是跳过事务5.2 排查工具与字段对照我在实际排查中常驻几个备选方案可以快速起手-- A. 看全局线程概况 SELECT THREAD_ID, PROCESSLIST_ID, NAME, PROCESSLIST_STATE, PROCESSLIST_INFO FROM performance_schema.threads WHERE PROCESSLIST_STATE IS NOT NULL\G -- B. 看最近被 kill 但还残留的线程 SELECT THREAD_ID, PROCESSLIST_ID, PROCESSLIST_STATE FROM performance_schema.threads WHERE PROCESSLIST_STATE IN (Killed, Waiting for table metadata lock, Cleaning up)\G -- C. 结合 events_statements_current 查看线程最近一次执行的语句 SELECT THREAD_ID, EVENT_NAME, SQL_TEXT FROM performance_schema.events_statements_current WHERE THREAD_ID IN (SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_STATE Killed)\G这套组合拳能在最紧张的时候帮你快速确认“到底还有谁没退出、它在干什么”。5.3 我个人最想提醒的几点第一不要在业务高峰期去处理大表 DDL。就算你用了在线 DDL 工具MDL 依然会在某些阶段短暂阻塞。先把会话和事务边界检查清楚再把 DDL 放进维护窗口。第二KILL之前一定要先回答三个问题这个线程当前在执行什么它持有哪些资源杀掉之后可能触发的回滚范围有多大三个问题回答不上来宁可再观察 30 秒也不要贸然下手。数据库处理问题最重要的不是手快而是方向准。第三我习惯长期开着性能库的等待事件采集UPDATE performance_schema.setup_consumers SET ENABLED YES WHERE NAME events_waits_current;这样下次发生类似问题时可以直接从sys.session里回看历史的等待事件不用等故障发生时才匆忙开监控。第四如果是从库上出现的“kill 不掉”现象先别急着动手。要看复制线程在 apply 哪个事务、这个事务在源库是否已经提交。贸然杀掉复制线程可能导致复制重建代价比主库杀进程大得多。最后分享一个我自己的习惯每周固定检查一次数据库里是否存在超过 10 分钟的Sleep事务发现一个处理一个。很多所谓的“MySQL 卡死疑难杂症”根源就是这种不起眼的长事务积累出来的。这个习惯坚持下来能帮你避开绝大部分“进程 kill 不掉、事务结束不了”的夜班事故。