锁表是MySQL运维中最让人头疼的问题之一。业务高峰期一条不当的SQL可能把整张表锁住,后续所有请求排队等待,连接数迅速飙升,应用层面表现为大面积超时。很多人遇到锁表的第一反应是重启数据库,这其实是最差的方案,因为重启后问题还会复发。正确的做法是利用MySQL自带的监控视图,快速找出持有锁的事务和对应的SQL语句。本文从锁的类型讲起,逐步演示排查锁表的完整流程。

先搞清楚你遇到的是哪种锁
排查锁表之前,必须先判断锁的类型,因为不同锁的排查手段完全不同。MySQL中常见的锁有三类:表级锁、行级锁和元数据锁(MDL)。表级锁通常来自显式的LOCK TABLES语句或某些DDL操作;行级锁是InnoDB引擎基于索引实现的,当UPDATE或DELETE语句没有走索引时,行锁会升级为效果等同于表锁的锁全表扫描;元数据锁则在执行DDL修改表结构时出现,任何未提交的事务都会阻塞DDL,而DDL又会反过来阻塞后续所有对该表的查询。
一个典型的坑是:一条查询忘记提交事务,之后执行ALTER TABLE就会一直等待MDL锁,紧接着所有访问这张表的SELECT都被卡住。表面上看起来是DDL导致了锁表,真正的元凶其实是那个长事务。所以排查时不要只盯着最后执行的语句,要顺着锁等待链条往前找源头。
另外,MyISAM引擎只支持表锁,读写互斥,如果表还在使用MyISAM,一个慢查询就会阻塞所有写请求。生产环境建议统一使用InnoDB,这也是排查锁表问题的前置条件之一。
用information_schema定位锁等待关系
MySQL 8.0之前,information_schema中有三张核心视图:innodb_trx记录当前所有运行中的事务,innodb_locks记录当前的锁信息,innodb_lock_waits记录锁等待关系。三者配合可以还原出完整的锁链条。
排查时先执行下面的查询,找出谁在等谁:
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
输出结果中,waiting_query是被阻塞的SQL,blocking_query就是持有锁的事务正在执行或最后执行的语句,这就是你要找的元凶。需要注意的是,如果阻塞事务处于空闲状态,blocking_query可能为空,这时要通过blocking_thread去information_schema.processlist里查对应线程的信息:
SELECT * FROM information_schema.processlist WHERE id = 上一步查到的blocking_thread;
拿到线程id后,用KILL 线程id即可终止阻塞源,业务会立即恢复。MySQL 8.0做了调整,这三张视图迁移到了performance_schema中,分别改名为data_lock_waits和data_locks,用法类似:
SELECT
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID
JOIN information_schema.innodb_trx r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID;
除了查视图,开启锁等待监控参数也很关键。innodb_lock_wait_timeout控制锁等待超时时间,默认50秒,排查期间可以适当调小,让被阻塞的语句尽快报错暴露出来,而不是默默排队。
借助performance_schema和日志做深度分析
如果锁表已经发生但你没来得及抓现场,performance_schema可以帮助事后分析。开启events_statements_history相关消费者后,每个线程最近执行的SQL都会被记录下来,配合sys.session或sys.innodb_lock_waits视图,可以直接看到“谁阻塞了谁以及对应的SQL文本”,比手工关联三张表方便得多。
-- sys库提供的一站式视图,直接输出可读的锁等待报告 SELECT waiting_pid, waiting_query, blocking_pid, blocking_query, sql_kill_blocking_query FROM sys.innodb_lock_waits;
最后一列sql_kill_blocking_query直接给出了杀掉阻塞连接的KILL语句,复制执行即可。慢查询日志同样有价值:导致锁表的SQL往往本身就是慢SQL,把long_query_time设置为1秒甚至更低,配合log_queries_not_using_indexes,能把不走索引的全表扫描语句全部记录下来,这类语句就是锁表的高危人群。
对于偶发且难以抓现场的场景,可以使用抓包工具或general log临时开启一段时间,但general log对性能影响较大,开启时间要控制好,抓完立即关闭。
常见锁表诱因与预防措施
排查只是治标,避免锁表复发才是关键。实践中最常见的诱因有四种:一是UPDATE或DELETE的WHERE条件没走索引,导致锁范围扩大;二是事务中有RPC调用或耗时操作,事务持有时间过长;三是代码中忘记提交或回滚事务,尤其在连接池复用连接时容易出现;四是低峰期外执行DDL,被长事务阻塞后引发连锁反应。
对应的预防手段包括:为所有UPDATE、DELETE涉及的WHERE字段建立合适索引,可以用EXPLAIN确认执行计划;事务里只做数据库操作,把RPC调用移到事务外;设置SET SESSION innodb_lock_wait_timeout = 10这类较短的超时让问题快速暴露;执行DDL前先用下面的语句检查长事务:
-- 查找运行超过60秒的事务,执行DDL前必须确认无长事务 SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
做好这些预防工作,配合前面介绍的排查流程,下次再遇到锁表,几分钟内就能定位到具体的SQL语句,而不是慌乱地重启服务赌运气。锁表排查的核心思路始终是:从等待者找到阻塞者,从阻塞事务找到源头SQL,再从SQL本身找到设计缺陷,形成闭环改进。