MySQL锁表排查指南:从锁类型识别到阻塞链定位与释放策略 线上数据库突然慢了一截应用日志里刷出一片 “Lock wait timeout exceeded”或者你准备在业务低峰期做一次 ALTER TABLE结果会话卡在那里半天不动——这种时候很多人都会问我同一个问题MySQL 中如何查看表是否被锁。说实话这个问题背后真正要解决的从来不是“看一眼有没有锁”而是把锁的关系、阻塞的源头、以及后续怎么快速恢复这条链路搞清楚。我自己这几年排查下来得出的结论是锁本身不可怕可怕的是不知道是谁握着锁不撒手。这篇文章就把完整的排查思路整理一遍适合 DBA、后端研发、运维同学参考。我会从最常用的两三条命令讲起再深入锁等待视图最后给出处理方法和防止“锁表”的前置手段。看到最后你会发现真正需要记住的不是某一条命令而是一套固定的决策顺序。1. 先搞清楚你在查哪种“锁”表锁只是一个模糊说法很多人一着急就把问题描述成“表被锁了”但 MySQL 里的锁至少分成三类表锁、行锁、元数据锁。用错排查方向很容易导致你盯着一堆输出却找不到关键行。1.1 三类锁和它们最常见的出现场景表锁最传统的锁作用于整张表。MyISAM、MEMORY 引擎会用到InnoDB 在显式执行LOCK TABLES ... WRITE时也会产生。最常见的触发场景是批量导入、某些备份工具、以及老业务里的 MyISAM 表。行锁InnoDB 的常规锁粒度锁的是索引记录或间隙。大多数“卡更新”的线上事故其实是行锁竞争而不是表锁。但在业务方感知里“更新不了”和“表被锁”没有区别所以信息传到 DBA 这里就变成了“表被锁了”。元数据锁MDL别人看不见摸不着但最坑的就是它。任何查询开始执行的时候都会拿一个 MDL 读锁DDL 需要 MDL 写锁。只要有一个长时间不结束的事务占着读锁后面的 ALTER TABLE 就会永久卡在Waiting for table metadata lock。这三种锁的排查入口完全不同。表锁看SHOW OPEN TABLES的状态列行锁要查information_schema和performance_schema里的事务锁视图MDL 必须去performance_schema.metadata_locks看。所以第一步不是急着进数据库而是先凭现象判断你面对的是哪一类。1.2 根据报错和表现反推锁类型根据我踩过的坑大部分现场可以这么对应现场表现最可能的锁类型对应的核心排查工具报错Lock wait timeout exceededInnoDB 行锁等待超时sys.innodb_lock_waitsALTER TABLE或DROP TABLE卡住State 显示Waiting for table metadata lockMDL 元数据锁performance_schema.metadata_locks所有针对某张表的查询全部排队In_use明显大于 0MyISAM 表锁或显式表锁SHOW OPEN TABLES实例整体只读只有一个会话在跑FLUSH TABLES WITH READ LOCK全局锁SHOW PROCESSLIST我把这个表放在最前面是因为后面所有操作都依赖这一步判断。如果你拿着查行锁的命令去查 MDL大概率查不到东西还会觉得“MySQL 没锁”实际上业务已经卡死半天了。1.3 一个反直觉的事实SHOW OPEN TABLES 不等于全部先提醒一个非常常见的误判SHOW OPEN TABLES列出的In_use大于 0只代表这张表当前被某些会话以“打开并使用”的形式占用但它不体现 InnoDB 行锁的任何信息。也就是说你用这条命令没发现锁不代表业务没问题。有人拿它查了一圈说“没锁”结果真实原因是行锁堆积。后面我会把每类锁的具体查法拆开讲。2. 第一步操作SHOW OPEN TABLES 和 PROCESSLIST 配合使用如果你接到的是“帮我看看表是不是被锁了”这类需求我建议你按下面顺序执行不要跳步。这套组合花不了十秒钟能覆盖表锁和大部分明显阻塞场景。2.1 SHOW OPEN TABLES 的准确用法基础命令长这样-- 看看当前库里所有打开的表 SHOW OPEN TABLES; -- 只看某个库 SHOW OPEN TABLES FROM your_db; -- 只筛选正在被使用的表这一步最关键 SHOW OPEN TABLES FROM your_db WHERE In_use 0;输出里有两列需要读懂In_use表示这张表当前被多少个会话引用。对于 MyISAM 表锁如果有一个会话持有了写锁后续所有读写的In_use都会累加。也就是说超过 1 且持续不降说明真的有人在排队。Name_locked跟表名锁定有关通常在DROP TABLE或RENAME TABLE卡住时变成 1。看到这一项为 1说明表名级别的操作在等待。我遇到过这样一个场景业务方反馈某张历史表的所有 select 都变慢我执行这条命令看到In_use达到 5说明大量会话在排队等同一张表。再结合引擎类型一确认果然是 MyISAM。所以SHOW OPEN TABLES对表级锁和显式表锁非常直观。但对于 InnoDB 行锁就算两张业务表争同一行数据争到天昏地暗这里也只会显示In_use 1甚至不显示。原因前面说了它只管“打开的表”不管“行锁竞争”。所以这一步没查出结果千万别下结论。2.2 SHOW FULL PROCESSLIST 看谁卡在哪里第二步也是很多人习惯性敲的命令SHOW FULL PROCESSLIST;我更喜欢用 information_schema 里的表来查过滤条件更灵活SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE time 10 ORDER BY time DESC;这里最需要关注两列State如果看到Waiting for table metadata lock那基本就是 MDL 问题Waiting for table flush也类似是表被 FLUSH 类操作卡住。Info正在执行的 SQL。如果一个UPDATE或DELETE执行了很多秒还没结束它很可能就是在等行锁如果Command为Sleep且Time很长这个会话可能是持有锁却一直不提交的元凶。有个细节容易被忽略InnoDB 行锁等待时PROCESSLIST 的 State 不一定有特别标志性的文字经常停留在updating或者干脆空白。所以当你看到一大堆updating状态的会话就要意识到这可能不是 SQL 本身慢而是后面有行锁堵着。这时候光靠 PROCESSLIST 不够必须进入第三步去锁等待视图里找阻塞链。2.3 快速排查的决策路径我把前两步合并成一个判断逻辑先看SHOW OPEN TABLES ... WHERE In_use 0确认有没有表级锁占用。再看SHOW FULL PROCESSLIST确认有没有长时间不结束的会话以及 State 是否提示 MDL。如果第一步查出 MyISAM 表锁直接定位持锁会话如果第二步查到Waiting for table metadata lock去查 MDL 视图。如果 PROCESSLIST 显示一堆执行中的写入但看不出锁状态跳转第三步。这套路径能解决大概一半的问题。剩下那一半几乎都是行锁和深层次的 MDL 问题需要专门的锁视图。3. 深挖锁等待用 INNODB_TRX 和 SYS 视图定位阻塞链真正要把“谁锁了谁”这层关系扒出来需要用到事务表和锁等待视图。这里我会同时给 MySQL 5.7 和 8.0 的兼容写法。3.1 先看当前有哪些事务还悬着所有锁的持有者都是事务所以第一件事就是看事务列表SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id AS thread_id, trx_query, trx_rows_locked, trx_rows_modified FROM information_schema.innodb_trx ORDER BY trx_started;重点看三列trx_state如果存在LOCK WAIT状态说明这个事务正在等待别人释放锁。trx_started事务开启时间。这个时间越老风险越大。一个从早上九点开始就没提交的事务哪怕它只是执行了一条SELECT也可能阻塞后面的 DDL。trx_rows_locked和trx_rows_modified粗略判断这个事务锁了多少行、改了多大数据。如果一个事务已经改了 2 万行没提交KILL 它的回滚代价就会比较大。这个表只告诉我们“谁在等待、谁在做事”但没直接说“谁阻塞了谁”。要看阻塞关系往下走。3.2 直接使用 sys.innodb_lock_waits 视图MySQL 5.7 之后自带的 sys 库里有个视图叫innodb_lock_waits它把事务表、锁等待表、线程信息全部 join 好了查一次就能得到阻塞链和对应的 kill 命令。执行方式SELECT * FROM sys.innodb_lock_waits\G这是我个人最常用的命令因为它输出非常清楚。我模拟一个实际输出*************************** 1. row *************************** wait_started: 2025-06-01 10:23:45 wait_age: 00:02:15 wait_age_secs: 135 locked_table: db1.user locked_index: PRIMARY locked_type: RECORD waiting_trx_id: 421512345678 waiting_trx_started: 2025-06-01 10:21:30 waiting_pid: 21 waiting_query: UPDATE user SET status 1 WHERE id 100 waiting_lock_mode: X,REC_NOT_GAP blocking_trx_id: 421512345677 blocking_pid: 18 blocking_query: SELECT * FROM user WHERE id 100 FOR UPDATE blocking_trx_started: 2025-06-01 10:19:00 blocking_trx_rows_locked: 1 blocking_trx_rows_modified: 1 sql_kill_blocking_query: KILL QUERY 18 sql_kill_blocking_connection: KILL 18这段输出信息量很大。你能直接看到waiting_query是卡住的 SQLUPDATE user SET status 1 WHERE id 100blocking_query是持有锁的 SQLSELECT * FROM user WHERE id 100 FOR UPDATEblocking_pid是 18意味着你是要 KILL 这个会话而不是去 KILL 等待方 21。视图最后直接给了你两条现成的 kill 命令照着执行就行。这个视图帮我节省了大量手工 join 的时间。唯一的注意点是它能查到的是行锁等待MDL 的等待不会出现在这里面。3.3 MySQL 8.0 下看 performance_schema 的锁表如果你的环境是 MySQL 8.0sys 视图底层依赖的两个表已经发生了变化。早期版本大家习惯查information_schema.innodb_locks和innodb_lock_waits8.0 里这两个表已经废弃取而代之的是performance_schema.data_locks和performance_schema.data_lock_waits。想直接查锁和等待关系可以用这两条-- 查看当前所有持锁和等待记录 SELECT * FROM performance_schema.data_locks\G -- 查看锁等待关系推荐关注 ENGINE_TRANSACTION_ID SELECT * FROM performance_schema.data_lock_waits\Gdata_locks里比较关键的列是ENGINE_TRANSACTION_ID、OBJECT_SCHEMA、OBJECT_NAME、LOCK_TYPE、LOCK_MODE、LOCK_STATUS。LOCK_STATUS为WAITING的就是在等锁的会话为GRANTED的就是已经拿到锁的会话。如果你需要自写关联查询可以参考下面这段在 8.0 里验证过的 SQL能把等待方和阻塞方对应起来SELECT w.ENGINE_TRANSACTION_ID AS waiting_trx_id, ws.THREAD_ID AS waiting_thread_id, w.OBJECT_NAME AS locked_table, l.ENGINE_TRANSACTION_ID AS blocking_trx_id, ls.THREAD_ID AS blocking_thread_id FROM performance_schema.data_lock_waits w JOIN performance_schema.data_locks l ON w.BLOCKING_ENGINE_LOCK_ID l.ENGINE_LOCK_ID JOIN performance_schema.threads ws ON w.ENGINE_TRANSACTION_ID ws.PROCESSLIST_ID JOIN performance_schema.threads ls ON l.ENGINE_TRANSACTION_ID ls.PROCESSLIST_ID;实际操作中大多数时候我并不会手写这些 join先跑sys.innodb_lock_waits拿不到结果再去翻data_locks和data_lock_waits的原始记录。3.4 5.7 和 8.0 的命令兼容性怎么处理有些公司线上还是 MySQL 5.7甚至 5.6所以我常被问到版本差异。简单整理一下MySQL 5.6/5.7information_schema.innodb_trx、innodb_locks、innodb_lock_waits都在sys.innodb_lock_waits在 5.7 可用。MySQL 8.0innodb_locks和innodb_lock_waits被移除统一看performance_schema.data_locks和data_lock_waits。SHOW OPEN TABLES、SHOW FULL PROCESSLIST在所有版本都可用是兼容性最好的两条命令。如果你要写一套巡检脚本给多版本环境用建议优先选sys.innodb_lock_waits并加上版本判断8.0 就走performance_schema分支。4. 最容易被误判的两个场景MDL 和行锁“假装”表锁排查多了你会发现真正让你在群里被反复问“到底锁没锁”的往往是 MDL 和行锁。4.1 DDL 卡住从 PROCESSLIST 到 metadata_locks 的完整链路最典型的情况是这样的你的同事在上午十点执行了一个ALTER TABLE t ADD COLUMN ...然后这个会话就卡在那里。你执行SHOW FULL PROCESSLIST看到 State 是Waiting for table metadata lock。这是 MDL 无疑。但问题来了到底是谁在阻塞PROCESSLIST 里那个ALTER语句不会直接告诉你阻塞者是谁。你需要从performance_schema.metadata_locks里找线索SELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, OWNER_THREAD_ID, OWNER_EVENT_ID, SOURCE FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA db1 AND OBJECT_NAME t;输出里你会看到两类记录LOCK_TYPE SHARED_READ、LOCK_STATUS GRANTED这些是已经拿到 MDL 读锁的会话它们可能是普通的 SELECT也可能是一个开了事务却迟迟不提交的连接。每一行都代表一个持有者。LOCK_TYPE EXCLUSIVE、LOCK_STATUS PENDING这就是你那个ALTER TABLE它在排队等着前面的读锁释放。拿到了OWNER_THREAD_ID之后还要把它转换成SHOW PROCESSLIST里的连接 ID。可以通过performance_schema.threads表转换SELECT PROCESSLIST_ID, PROCESSLIST_USER, PROCESSLIST_DB, PROCESSLIST_COMMAND, PROCESSLIST_TIME, PROCESSLIST_STATE, PROCESSLIST_INFO FROM performance_schema.threads WHERE THREAD_ID 上面查到的 OWNER_THREAD_ID;这一串操作看起来繁琐但逻辑很清晰MDL 的阻塞者不一定在真正执行 SQL它可能是一个开启事务后一直Sleep的连接。这也是为什么很多人只查 PROCESSLIST 查不到头绪——那个持有读锁的会话Command是Sleep看起来人畜无害实际上把整个 DDL 堵死了。4.2 行锁等待久了业务侧感知就是“表被锁”InnoDB 的行锁通常只锁几条记录但在业务侧的表现往往就是“这张表写不进去了”。比如订单表某一行被一个事务锁住其他所有要更新这一行的会话都会排队。你从业务侧看所有更新都卡住于是报障说“订单表被锁”。遇到这种情况直接执行SELECT * FROM sys.innodb_lock_waits\G它的优先级高于手动查innodb_trx因为一步到位。如果查出来锁的是同一行数据且访问量不大最常见的建议是找到blocking_trx_id对应的业务会话通知对应负责人尽快提交或回滚。需要注意行锁和表锁有一项本质区别行锁竞争通常会触发Lock wait timeout exceeded报错这个超时时间由参数innodb_lock_wait_timeout控制默认 50 秒。50 秒后事务会被回滚所以它是一种“有自动恢复机制”的等待。而 MyISAM 表锁和 MDL 如果没人干预可能会无限期卡下去。这也是排查时判断类型的一个辅助依据。4.3 MyISAM 表锁时代的典型表现虽然 InnoDB 是默认引擎但存量系统里仍有不少 MyISAM 表特别是数据仓库类的历史表。MyISAM 使用表级锁写锁是独占的。一旦有一个写操作在跑所有其他会话不管是读还是写全部排队。特征非常好认SHOW OPEN TABLES的In_use会持续大于 1同时PROCESSLIST里能看到大量会话处于Waiting for table level lock状态。再补一个辅助状态变量SHOW GLOBAL STATUS LIKE Table_locks%;输出类似------------------------------ | Variable_name | Value | ------------------------------ | Table_locks_immediate | 100 | | Table_locks_waited | 30 | ------------------------------Table_locks_waited表示有多少次表锁请求不得不等待。这个值如果持续快速增长说明表锁竞争已经比较严重。不过它对 InnoDB 行锁不敏感只能作为引擎层面的辅助参考。5. 确认锁住之后怎么安全地释放而不是简单 KILL排查只是第一步真正考验功底的是处理。我看到很多人拿到阻塞者 PID 就直接KILL运气好解决了运气不好把事务回滚拖垮了实例。所以释放锁要有先后顺序而且要评估代价。5.1 先用 KILL QUERY而不是直接 KILL 连接MySQL 的 KILL 指令有两种-- 只杀死当前正在执行的查询连接还在 KILL QUERY 18; -- 断开整个连接事务必然回滚 KILL 18;我的建议是能KILL QUERY就先KILL QUERY。特别是当阻塞者是一个仍在执行的查询时取消查询通常能让它立刻释放持有的锁而KILL 18会把这个连接的所有状态全部清掉如果事务已经改了一大批数据回滚开销量会非常惊人。不过也有例外如果阻塞者是Sleep状态的空闲连接KILL QUERY根本没用因为它当前没有在跑查询。这种情况只能KILL连接。判断方法就是看PROCESSLIST里的Command列Query表示正在执行 SQLSleep表示空闲。5.2 KILL 之前先看回滚代价sys.innodb_lock_waits输出里的blocking_trx_rows_modified可以用来估算回滚代价。如果阻塞事务已经修改了十万行你直接 KILL 连接InnoDB 要花很长时间回滚期间实例负载可能飙升甚至让问题更严重。遇到这种大事务优先联系业务负责人问这个事务是不是可以提交。如果业务确认可以提交让它自然提交掉锁释放最快也不用回滚。只有业务方已经不掌握该连接、事务明显是异常残留时才考虑 KILL。我自己的判断逻辑是阻塞者的trx_started时间很短数据改动量小 → 直接 KILL。阻塞者的改动量大但业务能确认事务有效 → 等待提交。阻塞者已经“失联”超过几小时未提交业务也不认账 → 准备维护窗口 KILL并提前评估回滚时间。5.3 处理完别忘了观察连接数是否回落释放锁之后不能马上就说“搞定”。因为之前大量阻塞的会话会同时恢复一瞬间可能把连接池打满或者让 CPU 冲到高位。我处理完锁等待后一般会连续观察几个时间点SHOW GLOBAL STATUS LIKE Threads_running; SHOW GLOBAL STATUS LIKE Threads_connected;如果等待的会话数量很多强烈建议分批让流量恢复而不是瞬间放开。比如让应用负责人逐步重启部分服务节点或调整连接池大小这能有效避免“解锁后雪崩”。6. 防止表锁和锁等待成为常态的三件事最后讲点治本的手段。上面所有查询都是应对已经发生的问题但真正让运维轻松的办法是把锁冲突的频率压下来。6.1 强化事务纪律和超时设置行锁和 MDL 等待绝大多数源于“事务开了不提交”。研发同学的代码里如果出现这种模式会在事务里调用外部接口、执行耗时的批处理操作或者事务里不小心包了一个sleep那 DBA 再会查锁也没用。我建议业务侧统一规范事务范围尽量小事务内不要发网络请求批量操作每处理几百条就提交一次。同时数据库侧设置合理的等待超时不要让无意义的等待无限延续-- 全局默认行锁等待超时单位秒 SET GLOBAL innodb_lock_wait_timeout 30; -- 当前会话生效也可以针对连接池配置 SET SESSION innodb_lock_wait_timeout 30;这里不用设得太小太小会导致正常的短事务被误杀也不要 50 秒默认值照搬业务敏感期等 50 秒太长了。我们生产环境一般设在 20 到 30 秒之间。6.2 DDL 操作前置检查所有在核心表上的 DDL都建议在操作前执行一次这个查询SELECT id, user, db, command, time, state, LEFT(info, 100) AS sql_text FROM information_schema.processlist WHERE time 30 ORDER BY time DESC;如果发现存在长时间Sleep的连接或超过 30 秒还没结束的查询先清掉再动表。另外能在线执行的 DDL 尽量用在线 DDL例如ALTER TABLE t ADD COLUMN new_col INT NULL, ALGORITHMINPLACE, LOCKNONE;虽然 MySQL 8.0 默认很多 DDL 就是 INPLACE但显式写出来可以强迫执行前评估锁影响也给接手的人留下可读信息。6.3 让锁监控变成日常巡检而不是救火最后给一个可以直接放进定时巡检脚本的查询找出所有运行超过 30 秒的写入或处于锁等待的事务SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_rows_locked, trx_rows_modified FROM information_schema.innodb_trx WHERE trx_state LOCK WAIT OR trx_started NOW() - INTERVAL 30 SECOND;配合sys.innodb_lock_waits的结果每天定时跑一遍可以把绝大部分锁问题消灭在爆发前。真正让我觉得省心的不是某个命令有多高级而是每次业务方说“表被锁了”我已经有一个固定的处理流程判断类型 → 定位阻塞链 → 评估回滚代价 → 分批恢复。这套流程跑熟之后锁问题从发现到解决基本能控制在几分钟内。大家可以参考这篇文章里给出的命令结合自己的业务场景整理一份速查表下次遇到锁不用慌一条一条来。