前言:

当我们在 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锁主要是为了解决两个问题:

事务隔离问题:比如在可重复隔离级别下,会话A在2次查询期间,会话B对表结构做了修改,两次查询结果就会不一致,无法满足可重复读的要求。

数据复制问题:比如会话A执行了多条更新语句期间,另外一个会话B做了表结构变更并且先提交,就会导致slave在重做时,先重做alter,再重做update时就会出现复制错误的现象。也就是上面提到的bug#989。

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)

三、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-visionlocked_table: studentslocked_type: Metadata Lockwaiting_processlist_id: 13waiting_age: 938waiting_query: alter table students add column col1 intwaiting_state: Waiting for table metadata lockblocking_processlist_id: 12blocking_age: 947blocking_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操作阻塞。

MySQL 元数据锁(MDL)相关推荐

  1. MySQL 实战总结:MySQL 元数据锁(MDL)

    前言: 当我们在 MySQL 中执行 DDL 语句时,经常会发现语句没有在你预期的时间完成,这时候我们通常会在mysql输入终端输入show full processlist ,来看看发生了什么状况. ...

  2. mysql MDL锁如何解决_MySQL元数据锁MDL

    [TOC] > 除了我们经常熟知的共享锁和排他锁外,MySQL数据库还有一种锁叫元数据锁,主要为了保证元数据的一致性,用于处理不同线程操作同一数据对象的同步与互斥问题. # 一.元数据 所谓元数 ...

  3. mysql元数据死锁日志,MySQL 实战笔记 第02期:MySQL 元数据锁

    当我们在 MySQL 中执行 DDL 语句时,经常会发现语句没有在你预期的时间完成,这时候我们通常会使用 show full processlist ,来看看发生了什么状况.当你看到 waiting ...

  4. MySQL的元数据锁MDL发生场景和解决方法总结

    摘要 MetaData Lock即元数据锁,在数据库中元数据即数据字典信息包括db,table,function,procedure,trigger,event等.Metadata lock主要为了保 ...

  5. mysql 表锁 MDL锁 行锁

    mysql 按照粒度分可以分为 全局锁 表锁 行锁 全局锁 给整个锁加上锁 Flush tables with read lock 让数据处于只读的状态 一般用户 场景:做全库的逻辑备份. 表锁 顾名 ...

  6. mysql数据库锁MDL锁的解释

    1.背景 在我们系统中有一张表它的查询概率非常高.最近有个需求,需要对这个表增加一个字段,然而在增加字段的时候发现系统中有多个业务出现了超时操作,那么这个是什么原因导致的呢?经过查阅资料发现是数据库的 ...

  7. MySql各种锁机制的学习

    加锁的目的: 数据库是一个多用户使用的共享资源.当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况.若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性.锁 ...

  8. mysql limit锁_我所理解的MySQL五:锁及加锁规则

    mysql教程栏目介绍MySQL的第五篇文章,关于锁及加锁规则. MySQL 系列的第五篇,主要内容是锁(Lock),包括锁的粒度分类.行锁.间隙锁以及加锁规则等. MySQL 引入锁的目的是为了解决 ...

  9. MySQL全局锁、表级锁、行级锁介绍演示(详细)

    目录 介绍 分类 1.全局锁 1.1介绍 1.2场景 1.3语法 1.4演示 2.表级锁 2.1介绍 2.2分类 2.3语法 2.4演示 3.行级锁 3.1介绍 3.2分类 3.3场景 介绍 锁是计算 ...

最新文章

  1. go run/ go install/ go build / go get的区别
  2. BeagleBone Black教程之BeagleBone Black设备的连接
  3. CHOJ# 在线编译及评判系统(OnlineJudge)2.0 Beta2
  4. 「BJOI2019」奥术神杖(AC自动机+DP)
  5. python 元类 type_Python 使用元类type创建类对象常见应用详解
  6. 十、华为鸿蒙HarmonyOS应用开发之Java UI框架、常用TabList组件使用
  7. 数据结构之内部排序算法总结笔记
  8. layuiajax提交表单控制层代码_Ninja Forms:免费的联系表单插件,却提供了付费表单才有的功能【视频+图文】...
  9. jdk1.8新特性_Lambda表达式的引入
  10. azure api 管理_通过成本管理跟踪Azure成本
  11. 天弘基金:中国的BlackRock?
  12. Excel如何制作二级下拉菜单
  13. 一文教你玩转Mybatis,超详细代码讲解与实战
  14. 私有云Openstack介绍及搭建
  15. 我,微软,700亿美金买暴雪怎么了?
  16. 带上传文件功能的写CSDN博客并发表
  17. mpu9250磁力计校准 mpl库数据校准
  18. 山那边的panpan(序言)
  19. SQL函数---SQL NOW()
  20. 计算机毕业设计(附源码)python休闲网络宾馆管理

热门文章

  1. springMVC中的ModelMap作用及用法
  2. CrackMe练习之ACG TRIAL Crackme 1.0
  3. 小白如何学好3D建模?十年经验建模师给你最好的建议,速看!
  4. html正则表达式登录验证,JavaScript正则表达式验证登录实例
  5. 小程序毕业设计 基于微信奶茶外卖商城小程序毕业设计开题报告功能参考
  6. Chrome浏览器不兼容lodop打印控件
  7. java计算机毕业设计花卉网站源码+mysql数据库+系统+lw文档+部署
  8. 使用Python的moviepy第三方库剪辑视频
  9. 如何把Android/IOS手机或者平板作为PC电脑显示器的分屏
  10. 男子被闪电击中瞬间用手机拍下照片 电流粗大宛如“上帝之剑