mysql metadata lock - yaokun123/php-wiki GitHub Wiki
Waiting for table metadata lock
一、了解一下 metadata lock
metadata lock 是为了保护 database objects (包括 表结构、存储过程、触发器等)而设计的。那 metadata lock 和事务有什么关系呢?在事务中,当它需要访问一个 database object 时都需要先获得其 metadata lock,在事务结束后才会释放 metadata lock。这样做有几个目的:
第一是为了进一步保证事务的一致性。
比如我在事务 A 中对某一行记录进行了更新,我的事务现在还没有提交,但是这个时候另外一个会话2要修改表名,如果事务 A 持有了 metadata lock,那么这时候另一个会话2将无法修改,show processlist 会发现它在 Waiting for table metadata lock 。直到事务 A 提交或回滚后,才能获得 metadata lock 修改成功。如果我们没有 metadata lock 的机制,那么会话2 就可以直接修改表名,这样当事务 A 由于其它原因需要回滚的时候,就回滚不了,因为表名被修改了,这样导致数据不一致。
第二是为了解决 binlog 同步的一个 bug,这个和上面的原因一样。
binlog 的操作是基于事务的提交顺序的。事务 A 还未提交,另一个会话删除了相关表,这样 binlog 先记录的是删除表的操作,从库执行的顺序就不对了。
有了这些基础知识后,我们就知道出现 Waiting for table metadata lock 这种情况多半是和事务有关,要么是一个长事务在运行,要么是事务没有提交造成的。下面我们在本地重现一下这个问题:
二、实验
MySQL 执行任何 DML 语句都是按事务来执行的,我们之所以不需要显示的提交或回滚是因为 MySQL 设置了默认全局自动提交,通过下面的命令查看 autocommit 的值
show global variables like '%autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
为了测试方便,我们将当前会话的 autocommit 设置为 false;
set autocommit = 0;
show variables like '%autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
环境搭建好后,我们开始重现这个问题:首先我们查询tb_user_tmp这张表,由于我们将当前会话的 autocommit 设置为 false,查询完后我们先不 commit。执行前我们先看看当前系统有没有事务在运行:
select * from information_schema.innodb_trx \G;
Empty set (0.02 sec)
select * from tb_user_tmp limit 0,1;
......
执行后我们看到我们这个事务正在运行:
select * from information_schema.innodb_trx \G;
*************************** 1. row ***************************
trx_id: 421624796451504
trx_state: RUNNING
trx_started: 2021-09-18 16:24:32
trx_requested_lock_id: NULL
trx_wait_started: NULL
trx_weight: 0
trx_mysql_thread_id: 692663
trx_query: NULL
trx_operation_state: NULL
trx_tables_in_use: 0
trx_tables_locked: 0
trx_lock_structs: 0
trx_lock_memory_bytes: 1136
trx_rows_locked: 0
trx_rows_modified: 0
trx_concurrency_tickets: 0
trx_isolation_level: REPEATABLE READ
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 0
trx_is_read_only: 0
trx_autocommit_non_locking: 0
1 row in set (0.00 sec)
现在我们开启另外一个会话,我们尝试修改 tb_user_tmp 的表名为tb_user_tmp_2:
ALTER TABLE `ucs`.`tb_user_tmp` RENAME TO `ucs`.`tb_user_tmp_2` ;
这时我们发现我们一直被阻塞,通过 show processlist 我们发现,我们在等待 metadata locak: waiting for table metadata lock
| 713819 | root | localhost| NULL| Query|4 | Waiting for table metadata lock | ALTER TABLE `ucs`.`tb_user_tmp` RENAME TO `ucs`.`tb_user_tmp_2` |
这个时候有非常严重的问题,当我们其它的会话都需要查询这张表时它们都被阻塞了:
select * from tb_user_tmp limit 0,1;
被阻塞
show processlist;
| 713819 | root | localhost| NULL | Query|311 | Waiting for table metadata lock | ALTER TABLE `ucs`.`tb_user_tmp` RENAME TO `ucs`.`tb_user_tmp_2` |
| 713864 | root | localhost | ucs | Query|40 | Waiting for table metadata lock | select * from tb_user_tmp limit 0,1|
到这,我们就把线上的问题重现了。通过 information_schema.innodb_trx 中的 trx_mysql_thread_id 的值知道了正在运行的事务的线程 id,然后在 show processlist 中通过查找对应 id 的 User, Host, db 信息定位到是哪个程序引起的。把这个线程 kill 掉就恢复正常了。最后我们去分析这个程序发现这个程序没有显示的启用事务,都是一些简单的查询语句,进一步分析发现是 pymysql 链接 MySQL 的时候默认 autocommit 设置为 0,而这个程序没有单独设置这个值,所以就出现这样的结果。
这里还有一个问题如果我们不做 DDL 操作,那其它的会话会被阻塞吗?测试一下我们修改表名,发现其它的会话不会被阻塞,这是为什么呢?因为多个事务可以同时持有同一个 database object 上的 metadata 锁,DML 语句只是修改表数据而不会修改表结构所以同时持有同一个 database object 上的 metadata 锁并不会有什么问题,还可以提高并发。那问题又来了,为什么在这些 DML 语句之前执行了一个 DDL 操作就会阻塞其后面的 DML 语句呢?网上有同学推测,对 metadata 锁的获取维护了一个先进先出的队列,这样可以避免 DDL 操作一直获取不到 metadata 锁的问题。
到这里我们是不是可以说如果在 information_schema.innodb_trx 中没有看到长时间运行的事务,则我们就可以放心的执行 DDL 操作呢?看下面的例子,我们查询了一个不存在的列
MySQL [ucs]> select test from tb_user_tmp limit 0,1;
ERROR 1054 (42S22): Unknown column 'test' in 'field list'
然后我们再去查看 information_schema.innodb_trx 中没有数据,
MySQL [(none)]> select * from information_schema.innodb_trx \G;
Empty set (0.00 sec)
现在我们去执行重命名操作,发现还是出现 Waiting for table metadata lock 的问题,这是为什么呢?
MySQL [(none)]> show processlist;
| 713819 | root | localhost| NULL| Query| 19 | Waiting for table metadata lock | ALTER TABLE `ucs`.`tb_user_tmp` RENAME TO `ucs`.`tb_user_tmp_2` |
因为 information_schema.innodb_trx 中不会记录执行失败的事务,但是在这个执行失败的事务回滚前,它依然持有 metadata lock,所以 DDL 操作依然会被阻塞。这个时候我们可以通过查找 performance_schema.events_statements_current 表来找到相关的语句和会话信息,将其杀死。
MySQL [(none)]> select * from performance_schema.events_statements_current \G;
SQL_TEXT: select test from tb_user_tmp limit 0,1
DIGEST: 95702e43cf29493b3aa1a091c79bc802
DIGEST_TEXT: SELECT `test` FROM `tb_user_tmp` LIMIT ?, ...
......
MYSQL_ERRNO: 1054
RETURNED_SQLSTATE: 42S22
MESSAGE_TEXT: Unknown column 'test' in 'field list'
......
为了减少 metadata lock 带来的危害,设置一个合理的 lock_wait_timeout 比较重要,这个值默认是 365 天,我们可以根据自身业务来考虑,避免长时间的 metadata lock 等待。
最后我们简单总结一下:
1、长事务很危险,运行需谨慎。
2、即使你没有显示的开启事务只是执行简单的查询语句,你也需要关注 autocommit 的值
3、使用事务需要小心,记得 commit,捕获异常 rollback
4、做 DDL 操作前先检查一遍 innodb_trx,实在不行先把 DDL 操作干掉,再排查问题
5、设置合理的 lock_wait_timeout