前言:
当我们在 MySQL 中执行 DDL 语句时,经常会发现语句没有在你预期的时间完成,这时候我们通常会在mysql输入终端输入show full processlist ,来看看发生了什么状况。当你看到 waiting for table metadata lock 时,那就碰到元数据锁了。那元数据锁是怎样产生的又应该怎样避免呢?让我们从这篇文章开始了解它。
一、什么是元数据锁(metadata lock)
在MySQL5.5.3之前,有一个著名的bug#989(bug链接: MySQL Bugs: #989: If DROP TABLE while there's an active transaction, wrong binlog order),大致如下:
# session1:
BEGIN;
INSERT INTO t ... ;
COMMIT;
# session2:
DROP TABLE t;
# 如果user1在一个表上有一个活动的事务,然后user2删除这个表,那么user1会提交,那么在binlog中我们有如下内容(binlog记录的操作顺序顺序):
1.
DROP TABLE t;
2.
BEGIN;
INSERT INTO t ... ;
COMMIT;
很显然mysql执行时会先删除表t,然后执行insert 会报1032 error。
再举一个简单的例子,如果你在查询一个表的过程中,另外一个session对该表删除了一个列,那前面的查询到底该显示什么呢?如果在RR隔离级别下,事物中再次执行相同的语句还会和之前结果一致吗?
答案是否定的,RR隔离级别下,会话A在2次查询期间,会话B对表结构做了修改,两次查询结果就会不一致,无法满足可重复读的要求。
为了防止这种情况,所以在5.5.3版本引入了MDL锁(表查询开始MySQL会在表上加一个锁,来防止被别的session修改了表定义,)。其实5.5也有类似保护元数据的机制,只是没有明确提出MDL概念而已。但是5.5之前版本(比如5.1)与5.5之后版本在保护元数据这块有一个显著的不同点是,5.1对于元数据的保护是语句级别的,5.5对于Metadata的保护是事务级别的。所谓语句级别,即语句执行完成后,无论事务是否提交或回滚,其表结构可以被其他会话更新;而事务级别则是在事务结束后才释放MDL。引入MDL锁主要是为了解决两个问题:
-
DDL操作与MetaData Lock
-
Metadata lock 机制是为了保证数据一致性存在的,在有session操作时候某张表时,需要首先获得Metadata lock ,然后操作,如果这个时候,又来了一个session要DDL操作同一个表,就会出现 waiting for table metadata lock -
自动提交模式下,单语句就是一个事务,执行完了,事务也就结束了。 -
preparestatement 会获得 metalock,一旦 prepare 完毕, metalock 就释放了。 -
online DDL应该是指在alter table进行的时候, 插入/修改/删除数据的sql语句(DML)不会Waiting for table metadata lock。一旦alter table TableA的操作停滞在Waiting for table metadata lock的状态,后续对TableA的任何操作(包括读)都无法进行,也会在Opening tables的阶段进入Waiting for table metadata lock的队列。
二、metadata lock和行锁有什么区别?
元数据锁是server层的锁,表级锁,每执行一条DML、DDL语句时都会申请metadata锁,DML操作需要metadata读锁锁,DDL操作需要metadata写锁,metadata加锁过程是系统自动控制,无法直接干预,读锁和写锁的阻塞关系如下:
-
读锁和写锁之间相互阻塞,即同一个表上的DML和DDL之间互相阻塞。 -
写锁和写锁之间互相阻塞,即两个session不能对表同时做表定义变更,需要串行操作。 -
读锁和读锁之间不会产生阻塞。也就是增删改查不会因为metadata lock产生阻塞,可以并发执行,日常工作中大家看到的DML之间的锁等待是innodb行锁引起的,和metadata lock无关。
申请metadata锁的操作会形成一个队列,队列中写锁获取优先级高于读锁。一旦出现写锁等待,不但当前操作会被阻塞,同时还会阻塞后续该表的所有操作。事务一旦申请到metadata锁锁后,直到事务执行完才会将锁释放。(这里有种特殊情况如果事务中包含DDL操作,mysql会在DDL操作语句执行前,隐式提交commit,以保证该DDL语句操作作为一个单独的事务存在,同时也保证元数据排他锁的释放)。
熟悉innodb行锁的同学这里可能有点疑困惑,因为行锁分类和metadata lock很类似,也主要分为读锁和写锁,或者叫共享锁和排他锁,读写锁之间阻塞关系也一致。二者最重要的区别一个是表锁,一个是行锁,且行锁中的读写操作对应在metadata lock中都属于读锁。
注意: 支持事务的InnoDB引擎表和不支持事务的MyISAM引擎表,都会出现Metadata Lock Wait等待现象。一旦出现Metadata Lock Wait等待现象,后续所有对该表的访问都会阻塞在该等待上,导致连接堆积,业务受影响。
metadata lock锁通常发生在DDL操作挂起的时候,原因是有未提交的事务对该表进行DML操作。而MySQL的会话那么多,不知道哪个会话的操作没有及时提交影响了DDL。通常我们排查这类问题,往往需要从information_schema.innodb_trx表中查询当前在执行的事务,但当SQL已经执行过了,没有commit,这个时候这个表中是看不到SQL的。
大家也许会奇怪,以前听说普通查询不加锁的,怎么这里又说要加表锁,我们做一个简单测试:
1.模拟与查找MDL锁
在MySQL5.7中,performance_schema库中新增了metadata_locks表,专门记录MDL的相关信息。首先要开启metadata_locks锁记录,执行如下SQL开启:
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';
session1:查询前,先看一下metadata_locks表,这个表位于performance_schema下,记录了metadata lock的加锁信息。
mysql> select * from performance_schema.metadata_locks;
+-------------+--------------------+----------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS | SOURCE | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| TABLE | performance_schema | metadata_locks | 2459204563344 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6020 | 36 | 3 |
+-------------+--------------------+----------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
1 row in set (0.00 sec)
session2:执行简单查询。
mysql> begin;
mysql> SELECT * from students; # 注意事务未提交
+----+-----------+------+--------+--------+--------+-----------+
| id | name | age | height | gender | cls_id | is_delete |
+----+-----------+------+--------+--------+--------+-----------+
| 1 | 小明 | 18 | 180.00 | 女 | 1 | |
| 2 | 小月月 | 18 | 180.00 | 女 | 1 | |
| 3 | 彭于晏 | 29 | 185.00 | 男 | 1 | |
| 4 | 刘德华 | 59 | 185.00 | 男 | 1 | |
| 5 | 黄蓉 | 38 | 160.00 | 女 | 1 | |
| 6 | 凤姐 | 28 | 150.00 | 保密 | 2 | |
| 7 | 王祖贤 | 18 | 172.00 | 女 | 1 | |
| 8 | 周杰伦 | 36 | NULL | 男 | 1 | |
| 9 | 程坤 | 27 | 181.00 | 男 | 2 | |
| 10 | 刘亦菲 | 25 | 166.00 | 女 | 2 | |
| 11 | 金星 | 33 | 162.00 | 中性 | 3 | |
| 12 | 静香 | 12 | 180.00 | 女 | 4 | |
| 13 | 郭靖 | 12 | 170.00 | 男 | 4 | |
| 14 | 周杰 | 34 | 176.00 | 女 | 5 | |
+----+-----------+------+--------+--------+--------+-----------+
14 rows in set (0.00 sec)
session1: 在会话1中查看metadata_locks表
mysql> select * from performance_schema.metadata_locks;
+-------------+--------------------+----------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS | SOURCE | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| TABLE | report-vision | students | 2459204563152 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6020 | 37 | 17 |
| TABLE | performance_schema | metadata_locks | 2459204563440 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6020 | 36 | 4 |
+-------------+--------------------+----------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
2 rows in set (0.00 sec)
此时再次查看metadata_lock表,发现多了一条students的加锁记录,加锁类型为SHARED_READ,且状态是已授予(GRANTED)。大家通常理解的查询不加锁,是指不在表上加innodb行锁。
如果在session2执行select期间且事务未提交,另外一个session执行了一个DDL操作,此时就会产生互斥的metadata lock:
session3:执行删除该表操作(DDL),发现该语句执行状态挂起
mysql> drop table students;
阻塞中......
session1:查询所有会话 发现发生MDL锁
mysql> show processlist;
+----+------+-----------------+---------------+---------+------+---------------------------------+---------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------------+---------------+---------+------+---------------------------------+---------------------+
| 2 | root | localhost:64908 | NULL | Sleep | 4991 | | NULL |
| 3 | root | localhost:64940 | report-vision | Sleep | 4991 | | NULL |
| 4 | root | localhost:64970 | report-vision | Sleep | 4981 | | NULL |
| 5 | root | localhost:65012 | report-vision | Sleep | 433 | | NULL |
| 6 | root | localhost:65449 | report-vision | Sleep | 4799 | | NULL |
| 7 | root | localhost:65454 | report-vision | Sleep | 4797 | | NULL |
| 8 | root | localhost:50270 | report-vision | Sleep | 438 | | NULL |
| 9 | root | localhost:51163 | report-vision | Sleep | 3869 | | NULL |
| 11 | root | localhost:59351 | NULL | Query | 0 | starting | show processlist |
| 12 | root | localhost:59625 | report-vision | Sleep | 121 | | NULL |
| 13 | root | localhost:59689 | report-vision | Query | 14 | Waiting for table metadata lock | drop table students |
+----+------+-----------------+---------------+---------+------+---------------------------------+---------------------+
11 rows in set (0.00 sec)
显然,id为13的线程还未执行drop操作,状态为‘Waiting for table metadata lock’,也就是在等待session2的事务提交操作完成。
session1:查看metadata_locks表记录 发现students表有MDL锁冲突
mysql> select * from performance_schema.metadata_locks;
+-------------+--------------------+----------------+-----------------------+---------------------+---------------+-------------+-------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS | SOURCE | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-----------------------+---------------------+---------------+-------------+-------------------+-----------------+----------------+
| TABLE | report-vision | students | 2459204563152 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6020 | 37 | 17 |
| GLOBAL | NULL | NULL | 2459204563440 | INTENTION_EXCLUSIVE | STATEMENT | GRANTED | sql_base.cc:5497 | 38 | 6 |
| SCHEMA | report-vision | NULL | 2459204563728 | INTENTION_EXCLUSIVE | TRANSACTION | GRANTED | sql_base.cc:5482 | 38 | 6 |
| TABLE | report-vision | students | 2459204564400 | EXCLUSIVE | TRANSACTION | PENDING | sql_parse.cc:6020 | 38 | 6 |
| TABLE | performance_schema | metadata_locks | 2459204563920 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6020 | 36 | 6 |
+-------------+--------------------+----------------+-----------------------+---------------------+---------------+-------------+-------------------+-----------------+----------------+
5 rows in set (0.00 sec)
当 session2 的事务提交后:
session2:
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
?session3:
mysql> drop table students;
Query OK, 0 rows affected (4 min 11.77 sec)
三、medadata lock为什么会造成系统崩溃?
举一个简单例子,session1启动一个事务,对表students执行一个简单的查询;session2对students加一个字段;session3来对students做一个查询;session4来对students做一个update,各个session串行操作。
session1:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * from students where id=1;
+----+--------+------+--------+--------+--------+-----------+
| id | name | age | height | gender | cls_id | is_delete |
+----+--------+------+--------+--------+--------+-----------+
| 1 | 小明 | 18 | 180.00 | 女 | 1 | |
+----+--------+------+--------+--------+--------+-----------+
1 row in set (0.00 sec)
session2:
mysql> alter table students add column col1 int;?
阻塞中...
session3:
mysql> select sleep(10) from students; # 开始执行后还会阻塞一段时间
阻塞中...
session4:
mysql> update students set name='aaaa' where id=1;
阻塞中...
也就是由于session1的一个事务没有提交,导致session2的DDL操作被阻塞,session3和session4本身不会被session1阻塞,但由于在锁队列中,session2排队更早,它准备加的是metadata lock写锁,阻塞了session3和session4的读锁。如果students是一个执行频繁的表,show processlist会发现大量‘waiting for table metadata lock’的线程,数据库连接很快就会消耗完,导致业务系统无法正常响应。
此时如果session1提交,是session2的alter语句先执行还是session3和session4先执行呢?
之前一直以为先到的先执行,当然是session2先执行,但经过测试,在5.7中,session3和session4先执行,session2最后执行(如果为drop语句依然是session2最先执行),也就会出现alter(或其他DDL语句)长时间无法执行的情况;而在8.0中,session2先执行,session3和session4后执行,由于5.6以后DDL是online的(drop 相关的DDL除外),session2并不会阻塞session3和session4,感觉这样才是合理的,alter不会被‘饿死’。
五、如何快速找到阻塞源头?
监控元数据锁
快速解决问题永远是第一位的,一旦出现长时间的metadata lock,尤其是在访问频繁的业务表上产生,通常会导致表无法访问,读写全被阻塞,此时找到阻塞源头是第一位的。这里最重要的表就是前面提到过的performance_schema.metadata_locks表。
metadata_locks是5.7中被引入,记录了metadata lock的相关信息,包括持有对象、类型、状态等信息。但5.7默认设置是关闭的(8.0默认打开),需要通过下面命令打开设置:
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES'WHERE NAME = 'wait/lock/metadata/sql/mdl';
如果要永久生效,需要在配置文件中加入如下内容:
[mysqld]performance-schema-instrument='wait/lock/metadata/sql/mdl=ON'
单纯查询这个表无法得出具体的阻塞关系,也无法得知什么语句造成的阻塞,这里要关联另外两个表performance_schema.thread 和performance_schema.events_statements_history,thread 表可以将线程id和show processlist 中id关联,events_statements_history 表可以得到事务的历史sql,关联得到一条监控元数据锁的sql语句。
关联后的完整sql如下:
SELECT locked_schema,
locked_table,
locked_type,
waiting_processlist_id,
waiting_age,
waiting_query,
waiting_state,
blocking_processlist_id,
blocking_age,
substring_index(sql_text,"transaction_begin;" ,-1) AS blocking_query,
sql_kill_blocking_connection
FROM
(
SELECT
b.OWNER_THREAD_ID AS granted_thread_id,
a.OBJECT_SCHEMA AS locked_schema,
a.OBJECT_NAME AS locked_table,
"Metadata Lock" AS locked_type,
c.PROCESSLIST_ID AS waiting_processlist_id,
c.PROCESSLIST_TIME AS waiting_age,
c.PROCESSLIST_INFO AS waiting_query,
c.PROCESSLIST_STATE AS waiting_state,
d.PROCESSLIST_ID AS blocking_processlist_id,
d.PROCESSLIST_TIME AS blocking_age,
d.PROCESSLIST_INFO AS blocking_query,
concat('KILL ', d.PROCESSLIST_ID) AS sql_kill_blocking_connection
FROM performance_schema.metadata_locks a JOIN performance_schema.metadata_locks b ON a.OBJECT_SCHEMA = b.OBJECT_SCHEMA AND a.OBJECT_NAME = b.OBJECT_NAME
AND a.lock_status = 'PENDING'
AND b.lock_status = 'GRANTED'
AND a.OWNER_THREAD_ID <> b.OWNER_THREAD_ID
AND a.lock_type = 'EXCLUSIVE'
JOIN performance_schema.threads c ON a.OWNER_THREAD_ID = c.THREAD_ID JOIN performance_schema.threads d ON b.OWNER_THREAD_ID = d.THREAD_ID
) t1,
(
SELECT thread_id, group_concat( CASE WHEN EVENT_NAME = 'statement/sql/begin' THEN "transaction_begin" ELSE sql_text END ORDER BY event_id SEPARATOR ";" ) AS sql_text
FROM
performance_schema.events_statements_history
GROUP BY thread_id
) t2
WHERE t1.granted_thread_id = t2.thread_id \G
*************************** 1. row ***************************
locked_schema: report-vision
locked_table: students
locked_type: Metadata Lock
waiting_processlist_id: 13
waiting_age: 938
waiting_query: alter table students add column col1 int
waiting_state: Waiting for table metadata lock
blocking_processlist_id: 12
blocking_age: 947
blocking_query: SELECT * from students where id=1
sql_kill_blocking_connection: KILL 12
1 row in set, 2 warnings (0.01 sec)
根据显示结果,processlist_id为12的线程阻塞了13的线程,我们需要kill 12即可解锁。
实际上,MySQL也提供了一个类似的视图来解决metadata lock问题,视图名称为sys.schema_table_lock_waits,但此视图查询结果有bug,不是很准确,建议大家还是参考上面sql。
六、如何避免元数据锁阻塞
MDL 锁一旦发生会对业务造成极大影响,因为后续所有对该表的访问都会被阻塞,造成连接积压。我们日常要尽量避免 MDL 锁阻塞的发生,下面给出几点优化建议可供参考:
-
开启 metadata_locks 表记录 MDL 锁。 -
设置参数 lock_wait_timeout 为较小值,使被阻塞端主动停止。 -
规范使用事务,及时提交事务,避免使用大事务。 -
增强监控告警,及时发现 MDL 锁。 -
DDL 操作及备份操作放在业务低峰期执行。 -
对于表结构经常变化且不关心原有表名只查询实时最新数据的特殊业务场景,可以在每次变更表结构时,用不同的表名(表名为添加后缀,如时间戳或uuid的表名)保存最新数据,随机定时异步删除原有的表名。来避免DDL操作与DML操作阻塞。
|