一文搞懂MySQL元数据锁(MDL)

目录
  • 一、什么是metadata lock
  • 二、MDL和行锁有什么区别
  • 三、MDL为什么会造成系统崩溃
  • 四、MDL的生命周期有多长
  • 五、如何快速找到阻塞源头
  • 六、本文开始的案例最终如何解决
  • 小结

某日,路上收到用户咨询,为了清除空间,想删除某200多G大表数据,且已经确认此表不再有业务访问,于是执行了一条命令‘delete from bigtable’,但好长时间也没删完,经过咨询后,获知drop table删除表速度快,而且能彻底释放空间,于是又在另外一个session中执行了‘drop table bigtable’命令,但是这个命令并没有快速返回结果,光标一直hang在原地不动。最后找我们协助,在登录数据库执行‘show processlist’后发现drop语句的状态是‘waiting for table metadata lock’,而之前执行的另外一个delete语句依旧能看到,状态为‘updating’,截图如下:

到底什么是metadata lock?这个锁等待是如何产生的?会带来什么影响?最后又如何来解决?今天我们挑6个常见问题给大家解答一下。

一、什么是metadata lock

在MySQL5.5.3之前,有一个著名的bug#989,大致如下:

 session1:
 BEGIN;
 INSERT INTO t ... ;
 COMMIT;  

 session2:
 DROP TABLE t;

然而上面的操作流程在binlog记录的顺序是

 DROP TABLE t;
 BEGIN;
 INSERT INTO t ... ;
 COMMIT;

很显然备库执行binlog时会先删除表t,然后执行insert 会报1032 error,导致复制中断。为了解决该bug,MySQL 在5.5.3引入了MDL锁(metadata lock),来保护表的元数据信息,用于解决或者保证DDL操作与DML操作之间的一致性。

再举一个简单的例子,如果你在查询一个表的过程中,另外一个session对该表删除了一个列,那前面的查询到底该显示什么呢?如果在RR隔离级别下,事物中再次执行相同的语句还会和之前结果一致吗?为了防止这种情况,表查询开始MySQL会在表上加一个锁,来防止被别的session修改了表定义,这个锁就叫‘metadata lock’,简称MDL,翻译成中文也叫‘元数据锁’。

二、MDL和行锁有什么区别

metadata lock是表级锁,是在server层加的,适用于所有存储引擎。所有的dml操作都会在表上加一个metadata读锁;所有的ddl操作都会在表上加一个metadata写锁。读锁和写锁的阻塞关系如下:

  • 读锁和写锁之间相互阻塞,即同一个表上的dml和ddl之间互相阻塞。
  • 写锁和写锁之间互相阻塞,即两个session不能对表同时做表定义变更,需要串行操作。
  • 读锁和读锁之间不会产生阻塞。也就是增删改查不会因为metadata lock产生阻塞,可以并发执行,日常工作中大家看到的dml之间的锁等待是innodb行锁引起的,和metadata lock无关。

熟悉innodb行锁的同学这里可能有点困惑,因为行锁分类和metadata lock很类似,也主要分为读锁和写锁,或者叫共享锁和排他锁,读写锁之间阻塞关系也一致。二者最重要的区别一个是表锁,一个是行锁,且行锁中的读写操作对应在metadata lock中都属于读锁。

大家也许会奇怪,以前听说普通查询不加锁的,怎么这里又说要加表锁,我们做一个简单测试:

session1:查询前,先看一下metadata_locks表,这个表位于performance_schema下,记录了metadata lock的加锁信息。

mysql> select * from performance_schema.metadata_locks ;
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA      | OBJECT_NAME    | COLUMN_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE   | LOCK_DURATION | LOCK_STATUS | SOURCE            | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| TABLE       | performance_schema | metadata_locks | NULL        |       139776223308432 | SHARED_READ | TRANSACTION   | GRANTED     | sql_parse.cc:6014 |              54 |             12 |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
1 row in set (0.00 sec)

session2:执行简单查询,为了让表处于执行状态,这里使用了sleep函数。

mysql> select sleep(10) from t1;
+-----------+
| sleep(10) |
+-----------+
|         0 |
|         0 |
|         0 |
+-----------+
3 rows in set (30.00 sec)

session1:

mysql> select * from performance_schema.metadata_locks ;
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA      | OBJECT_NAME    | COLUMN_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE   | LOCK_DURATION | LOCK_STATUS | SOURCE            | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| TABLE       | db1                | t1             | NULL        |       139776154308336 | SHARED_READ | TRANSACTION   | GRANTED     | sql_parse.cc:6014 |              53 |             22 |
| TABLE       | performance_schema | metadata_locks | NULL        |       139776223308432 | SHARED_READ | TRANSACTION   | GRANTED     | sql_parse.cc:6014 |              54 |             13 |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
2 rows in set (0.00 sec)

此时再次查看metadata_lock表,发现多了一条t1的加锁记录,加锁类型为SHARED_READ,且状态是已授予(GRANTED)。大家通常理解的查询不加锁,是指不在表上加innodb行锁。

如果在执行sleep期间,另外一个session执行了一个加字段操作,此时就会产生metadata lock锁等待:

session2:

mysql> select sleep(10) from t1;

执行中......

session3:

mysql> alter table t1 add col1 int;

阻塞中......

session1:

mysql> show processlist;
+----+-----------------+-----------+------+---------+--------+---------------------------------+-----------------------------+
| Id | User            | Host      | db   | Command | Time   | State                           | Info                        |
+----+-----------------+-----------+------+---------+--------+---------------------------------+-----------------------------+
|  4 | event_scheduler | localhost | NULL | Daemon  | 861577 | Waiting on empty queue          | NULL                        |
| 18 | root            | localhost | db1  | Sleep   |     50 |                                 | NULL                        |
| 19 | root            | localhost | NULL | Query   |      0 | starting                        | show processlist            |
| 20 | root            | localhost | db1  | Query   |     11 | Waiting for table metadata lock | alter table t1 add col1 int |
+----+-----------------+-----------+------+---------+--------+---------------------------------+-----------------------------+
4 rows in set (0.00 sec)

显然,id为20的线程还未执行alter操作,状态为‘Waiting for table metadata lock’,也就是在等待session2的sleep操作完成。

三、MDL为什么会造成系统崩溃

举一个简单例子:

  • session1启动一个事务,对表t1执行一个简单的查询;
  • session2对t1加一个字段;
  • session3来对t1做一个查询;
  • session4来对t1做一个update;

各个session串行操作。

session1:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1 where id=1;
+----+------+------+-------+
| id | name | age  | birth |
+----+------+------+-------+
|  1 | aa   |   10 | NULL  |
+----+------+------+-------+
1 row in set (0.00 sec)

session2:

mysql> alter table t1 add col1 int;

阻塞中...

session3:

mysql> select sleep(10) from t1 ;

阻塞中...

session4:

mysql> update t1 set name='aaaa' where id=2;

阻塞中...

也就是由于session1的一个事务没有提交,导致session2的ddl操作被阻塞,session3和session4本身不会被session1阻塞,但由于在锁队列中,session2排队更早,它准备加的是metadata lock写锁,阻塞了session3和session4的读锁。如果t1是一个执行频繁的表,show processlist会发现大量‘waiting for table metadata lock’的线程,数据库连接很快就会消耗完,导致业务系统无法正常响应。

此时如果session1提交,是session2的alter语句先执行还是session3和session4先执行呢?之前一直以为先到的先执行,当然是session2先执行,但经过测试,在5.7中,session3和session4先执行,session2最后执行,也就会出现alter长时间无法执行的情况;而在8.0中,session2先执行,session3和session4后执行,由于5.6以后ddl是online的,session2并不会阻塞session3和session4,感觉这样才是合理的,alter不会被‘饿死’。

四、MDL的生命周期有多长

事务!事务!事务! 重要的事情说三遍,表上的metadata lock的生命周期从事务中的第一条涉及自身的语句开始,到整个事务结束而结束。而5.5之前是基于语句的,事务中执行完语句就释放,如果此时另外一个session对表做了一个删字段操作,那么就会造成两个问题:

  • ddl操作如果先于事务完成,那么binlog中ddl就会排在事务之前,明显和逻辑不符,触发了本文开始提到的bug。
  • 如果是RR隔离级别,那么事务中此表第二次执行将无法返回同样的结果,无法满足可重复读的要求。

所以,如果要降低metadata lock的锁等待时间,最好要及时提交事务,同时尽量避免大事务。

那么如果发生metadata lock锁等待,等待锁的session会等待多长时间呢?大家都知道MySQL里面行锁等待有个超时时间(参数innodb_lock_wait_timeout),默认50s。metadata lock也有类似参数控制:

mysql> show variables like 'lock_wait_timeout'      ;
+-------------------+----------+
| Variable_name     | Value    |
+-------------------+----------+
| lock_wait_timeout | 31536000 |
+-------------------+----------+
1 row in set (0.00 sec)

这么长的数字,掰着指头算了半天,居然真的是......一年,环游世界一圈回来还得接着等!!!

当然,生产环境中,我们很少会等待metadata lock超时,更多的是要想办法把产生metadata lock的源头找到,快速提交或者回滚,或者想办法kill掉。那么如何找到阻塞的源头呢?

五、如何快速找到阻塞源头

快速解决问题永远是第一位的,一旦出现长时间的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如下:

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

对于前面的例子执行此sql,得到一个清晰的阻塞关系:

               locked_schema: db1
                locked_table: t1
                 locked_type: Metadata Lock
      waiting_processlist_id: 28
                 waiting_age: 227
               waiting_query: alter table t1 add cl3 int
               waiting_state: Waiting for table metadata lock
     blocking_processlist_id: 27
                blocking_age: 252
              blocking_query: select * from t1
sql_kill_blocking_connection: KILL 27
1 row in set, 1 warning (0.00 sec)

根据显示结果,processlist_id为27的线程阻塞了28的线程,我们需要kill 27即可解锁。

实际上,MySQL也提供了一个类似的视图来解决metadata lock问题,视图名称为sys.schema_table_lock_waits,但此视图查询结果有bug,不是很准确,建议大家还是参考上面sql。

六、本文开始的案例最终如何解决

通过前面的介绍,本文开始的案例产生的过程就很简单了:用户执行了一个全表delete,在目标表上加了metadata读锁,由于表很大,读锁长时间无法释放,后来另外一个session执行了drop table操作,又需要在表上加metadata写锁,由于读写锁互相阻塞,drop操作只能等待delete操作完成才能获得写锁,因此从表面来看,二个命令都长时间没有响应,其实内部一个在执行,一个在等待。

那怎么来解决呢?因为从show processlist以及客户描述可以很清楚的知道故障机制,当时建议客户将delete操作kill掉,等数据回滚完后再执行drop操作因为delete已经执行了一段时间,回滚过程可能会较长,客户最终kill delete后顺利drop成功。

小结

生产环境大多是dml操作,metadata读锁之间不会产生锁等待,而目前MySQL的ddl操作大多可以online执行,因此即使有写锁,也会很快降级为读锁,所以ddl执行期间阻塞dml的几率也很小。最容易出现的情况是由于有未完成的事务,导致ddl metadata 写锁无法加上,只能在锁队列等待,而一旦进入锁队列,写锁又会阻塞其他的读锁,导致数据库连接快速增长,直至消耗殆尽,最终业务受到影响。

为了尽可能避免类似问题,下面是几个小建议:

  • 生产环境的任何大表或频繁操作的小表,ddl都要非常慎重,最好在业务低峰期执行。
  • 设计上要尽可能避免大事务,大事务不仅仅会带来各种锁问题,还好引起复制延迟/回滚空间爆满等各类问题。
  • 要及时提交事务,经常发现客户端设置了事务手工提交,但sql执行后忘记点击提交按钮,导致事务长时间无法提交。建议监控实例中的长事务,避免由于各种原因导致事务没有及时提交。

到此这篇关于一文搞懂MySQL元数据锁(MDL)的文章就介绍到这了,更多相关MySQL元数据锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详细分析mysql MDL元数据锁

    前言: 当你在MySQL中执行一条SQL时,语句并没有在你预期的时间内执行完成,这时候我们通常会登陆到MySQL数据库上查看是不是出了什么问题,通常会使用的一个命令就是 show processlist,看看有哪些session,这些session在做什么事情.当你看到 waiting for table metadata lock 时,那就是遇到MDL元数据锁了.本篇文章将会介绍MDL锁的产生与排查过程. 1.什么是MDL锁 MDL全称为metadata lock,即元数据锁.MDL锁主要作用

  • MYSQL METADATA LOCK(MDL LOCK)MDL锁问题分析

    一.前言 MYSQL中MDL锁一直是一个比较让人比较头疼的问题,我们谈起锁一般更加倾向于INNODB下层的gap lock.next key lock.row lock等,因为它很好理解,也很好观察,而对于MDL LOCK却了解得很少,因为它实在不好观察,只有出现问题查看show processlist勉强可以看到 简单的所谓的Waiting for table metadata lock之类的状态,其实MDL LOCK是MYSQL上层一个非常复杂的子系统,有自己的死锁检测机制 (无向图?)而大

  • MYSQL METADATA LOCK(MDL LOCK) 理论及加锁类型测试

    目录 MYSQL METADATA LOCK(MDL LOCK)学习 理论知识和加锁类型测试 一.初步了解 二.基础重要的数据结构(类)和概念 1.MDL TYPE 2.MDL NAMESPACE 3.实现分类 4.MDL兼容矩阵 5.MDL duration及MDL持续到什么时候 6.MDL LOCK FAST PATH(unobtrusive) OR SLOW PATH(obtrusive) 7.MDL_request类 7.MDL_key类 8.MDL_ticket 9.MDL_lock

  • 一文搞懂MySQL元数据锁(MDL)

    目录 一.什么是metadata lock 二.MDL和行锁有什么区别 三.MDL为什么会造成系统崩溃 四.MDL的生命周期有多长 五.如何快速找到阻塞源头 六.本文开始的案例最终如何解决 小结 某日,路上收到用户咨询,为了清除空间,想删除某200多G大表数据,且已经确认此表不再有业务访问,于是执行了一条命令‘delete from bigtable’,但好长时间也没删完,经过咨询后,获知drop table删除表速度快,而且能彻底释放空间,于是又在另外一个session中执行了‘drop ta

  • 一文搞懂Mysql中的共享锁、排他锁、悲观锁、乐观锁及使用场景

    目录 一.常见锁类型 二.Mysql引擎介绍 三.常用引擎间的区别 四.共享锁与排他锁 五.排他锁的实际应用 六.共享锁的实际应用 七.死锁的发生 八.另一种发生死锁的情景 九.死锁的解决方式 十.意向锁和计划锁 十一.乐观锁和悲观锁 总结 一.常见锁类型 表级锁,锁定整张表 页级锁,锁定一页 行级锁,锁定一行 共享锁,也叫S锁,在MyISAM中也叫读锁 排他锁,也叫X锁,在MyISAM中也叫写锁 悲观锁,抽象性质,其实不真实存在 乐观锁,抽象性质,其实不真实存在 常见锁类型 二.Mysql引擎

  • 一文搞懂MySQL预编译

    1.预编译的好处 大家平时都使用过JDBC中的PreparedStatement接口,它有预编译功能.什么是预编译功能呢?它有什么好处呢? 当客户发送一条SQL语句给服务器后,服务器总是需要校验SQL语句的语法格式是否正确,然后把SQL语句编译成可执行的函数,最后才是执行SQL语句.其中校验语法,和编译所花的时间可能比执行SQL语句花的时间还要多. 如果我们需要执行多次insert语句,但只是每次插入的值不同,MySQL服务器也是需要每次都去校验SQL语句的语法格式,以及编译,这就浪费了太多的时

  • 一文搞懂MySQL XA如何实现分布式事务

    目录 前言 XA 协议 如何通过MySQL XA实现分布式事务 前言 MySQL支持单机事务的良好表现毋庸置疑,那么在分布式系统中,涉及多个节点,MySQL又是如何实现分布式事务的呢?比如开发一个业务系统,它接受外部的请求,然后访问多个内部其它系统才能执行该请求.执行时我们需要同时更新多个数据库的值(D1,D2,D3).由于系统必须处于一个一致性,也就是这三个数据库的值要么同时更新成功,要么全部不更新.不然会造成子系统有些指令成功了,有些指令尚未执行.导致对结果理解混乱. 那么,MySQL如何实

  • 一文搞懂MySQL持久化和回滚的原理

    目录 redo log 为什么要先更新内存数据,不直接更新磁盘数据? 为什么需要redo log? redo log是如何实现的? 为什么一个block设计成512字节? 为什么要两段式提交? crash后是如何恢复的? undo log 什么情况下会生成undo log? undo log是如何回滚的? undo log存在什么地方? redo log 事务的支持是数据库区分文件系统的重要特征之一,事务的四大特性: 原子性:所有的操作要么都做,要么都不做,不可分割. 一致性:数据库从一种状态变

  • 一文搞懂MySQL脏读,幻读和不可重复读

    目录 MySQL 中事务的隔离 1.READ UNCOMMITTED 2.READ COMMITTED 3.REPEATABLE READ 4.SERIALIZABLE 前置知识 1.事务相关的常用命令 2.MySQL 8 之前查询事务的隔离级别 3.MySQL 8 之后查询事务的隔离级别 4.查看连接的客户端详情 5.查询连接客户端的数量 6.设置客户端的事务隔离级别 7.新建数据库和测试数据 8.名称约定 脏读 1.脏读演示步骤1 2.脏读演示步骤2 3.脏读演示步骤3 4.不可重复读 5.

  • 一文搞懂MySQL运行机制原理

    目录 前言 MySQL服务器体系架构 网络连接层 服务层 存储引擎层 系统文件层 服务器处理客户端请求 连接管理 解析与优化 查询缓存 语法解析 查询优化 存储引擎 小结 前言 前文我们了解了MySQL采用客户端/服务器架构,用户通过客户端程序发送增删改查需求,服务器程序收到请求后处理,并且把处理结果返回给客户端.这篇文章主要看下MySQL服务端是如何处理客户端的请求,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助. MySQL服务器体系架构 MySQL Server架构自顶向下大致可以分网

  • 一文搞懂MySQL索引页结构

    目录 1.前言 2.索引页结构 2.1FileHeader 2.2PageHeader 2.3UserRecords 2.4Infimum&Supremum 2.5PageDirectory 2.6FileTrailer 3.总结 1. 前言 「页」是InnoDB管理存储空间的基本单位,也是内存和磁盘交互的基本单位.也就是说,哪怕你需要1字节的数据,InnoDB也会读取整个页的数据,下次读取的数据如果恰巧也在这个页里,就能命中缓存了.写也是一样的,写数据前要先把页加载到内存,然后在内存中修改,该

  • 一文搞懂什么是MySQL前缀索引

    目录 一.什么是前缀索引 二.为什么要用前缀索引 三.怎么创建前缀索引 四.使用前缀索引需要注意的事项 五.小结 一.什么是前缀索引 所谓前缀索引,说白了就是对文本的前几个字符建立索引(具体是几个字符在建立索引时去指定),比如以产品名称的前 10 位来建索引,这样建立起来的索引更小,查询效率更快! 有点类似于 Oracle 中对字段使用 Left 函数来建立函数索引,只不过 MySQL 的这个前缀索引在查询时是内部自动完成匹配的,并不需要使用 Left 函数. 二.为什么要用前缀索引 可能有的同

  • 一文带你搞懂Redis分布式锁

    目录 1.分布式锁简介 2.setnx 3.Redis-分布式锁-阶段1 4.Redis-分布式锁-阶段2 5.Redis-分布式锁-阶段3 6.Redis-分布式锁-阶段4 7.Redis-分布式锁-阶段5 1.分布式锁简介 分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现.如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性. 业界流行的分布式锁实现,一般有这3种方式: 基于数据库实现的分布式锁 基于Redis实现的分布式锁 基于

随机推荐