背景

MySQL 从5.5.3版本,对Metadata lock进行了调整,主要是MDL锁持有的周期从语句变成了事务, 其原因主要是解决两个问题:

问题1: 破坏事务隔离级别 在repeatable read的隔离级别下,多次的select语句执行过程中,会因为其它session的DDL语句,而导致select语句执行的结果不相同,破坏了RR的隔离级别。

问题2: 破坏binlog的顺序 在对表的DML过程中,会因为其它session的DDL语句,导致binlog里的event顺序在备库执行的结果和主库不一致。

从MySQL 5.5.3开始,MDL锁的持有周期变成了事务,解决了上面提到的两个问题,但在autocommit=off的情况下,也大大增加了阻塞的可能性。DBA对于阻塞的case,处理起来又比较麻烦,原因就是MDL锁的阻塞情况没有暴露明确的信息。

从MySQL 5.7.6开始,可以通过performance schema来查询MDL锁的持有情况。

在开始介绍5.7的跟踪Metadata lock之前, 小编还想讨论一下前面提到的这两个问题,在Oracle数据库中是如何处理的。

Oracle的处理方式

首先,Oracle只实现了两种隔离级别,即read committed和serializable,我们来看下serializable级别下,怎么来处理问题1:

先看如下的case:

  1. session 1: session 2:
  2. -- create table t1(id number);
  3. -- insert into t1 values(1);
  4. -- commit;
  5. SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; --
  6. TEST/TEST@ORCL>select * from t1; --
  7. ID
  8. ----------
  9. 1
  10. 1 row selected.
  11. -- alter table t1 add col number;
  12. TEST/TEST@ORCL>select * from t1; --
  13. ID COL
  14. ---------- ----------
  15. 1
  16. -- alter table t1 add col1 number default 10;
  17. TEST/TEST@ORCL>select * from t1; --
  18. ID COL COL1
  19. ---------- ---------- ----------
  20. 1

可以看到,虽然session是serializable隔离级别,但并没有产生阻塞的情况,Oracle保证了session1的多次select查询的返回结果是一样的, 但t1表数据字典的变化是马上可见的,这个也是符合serializable的要求的,因为隔离级别只定义了数据的可见性,而没有定义数据字典的可见性。

那MySQL能否不要MDL锁,来达到这样的效果?

答案是否定的,因为Oracle是堆表,alter的操作只更改了数据字典,数据记录没有发生变化,纵使加了default值,也是在原记录上进行的update,完全可以使用scn号来构建一致性读版本,这样就不会产生阻塞。 而MySQL是IOT表,alter的过程进行了表重建,无法完成read view的构建。

那我们再来看问题2,Oracle的处理方式:

对于redo日志,Oracle的处理方式和InnoDB的处理方式一致,也就是当使用redo的时候,日志的写入并不和事务的提交与否有必然的关系,也不用和提交的顺序保持一致。这一点就和binlog区别开来,也就是物理日志是可以避免使用逻辑日志(binlog)带来的问题。

MySQL如果要避免这两个问题,而不引入Metadata lock,可以有以下两个思路:

  1. DDL只更改数据字典,行记录的变更在原记录上进行,这样能够实现多版本,也就是我们常说的在线加字段;
  2. 使用物理redo日志,避免使用binlog。

这两种都会对现有的MySQL架构带来调整,仅供参考。

下面我们回来看下对5.7 MDL的tracing。

MySQL 5.7

首先,打开metadata locks的tracing功能。

  1. mysql> UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME = 'global_instrumentation';
  2. Query OK, 0 rows affected (0.00 sec)
  3. Rows matched: 1 Changed: 0 Warnings: 0
  4. mysql> UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME = 'wait/lock/metadata/sql/mdl';
  5. Query OK, 0 rows affected (0.00 sec)
  6. Rows matched: 1 Changed: 0 Warnings: 0

打开两个session,一个select,一个truncate。因为MDL锁的情况,select会阻塞truncate的操作。

session 1: 操作如下:

  1. mysql> set session autocommit=0;
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> select @@autocommit, @@tx_isolation;
  4. +--------------+----------------+
  5. | @@autocommit | @@tx_isolation |
  6. +--------------+----------------+
  7. | 0 | READ-COMMITTED |
  8. +--------------+----------------+
  9. 1 row in set (0.00 sec)
  10. mysql> select * from t limit 1;
  11. +----+------+
  12. | id | val |
  13. +----+------+
  14. | 1 | 1 |
  15. +----+------+
  16. 1 row in set (0.00 sec)

session 2: 操作如下:

  1. mysql> truncate table t;

结果看到的就是session2被阻塞, 接下来check一下performance schema的信息:

  1. mysql> select * from performance_schema.metadata_locks\G
  2. *************************** 1. row ***************************
  3. OBJECT_TYPE: TABLE
  4. OBJECT_SCHEMA: test
  5. OBJECT_NAME: t
  6. OBJECT_INSTANCE_BEGIN: 140450128308592
  7. LOCK_TYPE: SHARED_READ
  8. LOCK_DURATION: TRANSACTION
  9. LOCK_STATUS: GRANTED
  10. SOURCE: sql_parse.cc:5585
  11. OWNER_THREAD_ID: 27
  12. OWNER_EVENT_ID: 17
  13. *************************** 2. row ***************************
  14. OBJECT_TYPE: GLOBAL
  15. OBJECT_SCHEMA: NULL
  16. OBJECT_NAME: NULL
  17. OBJECT_INSTANCE_BEGIN: 140450195436144
  18. LOCK_TYPE: INTENTION_EXCLUSIVE
  19. LOCK_DURATION: STATEMENT
  20. LOCK_STATUS: GRANTED
  21. SOURCE: sql_base.cc:5224
  22. OWNER_THREAD_ID: 30
  23. OWNER_EVENT_ID: 8
  24. *************************** 3. row ***************************
  25. OBJECT_TYPE: SCHEMA
  26. OBJECT_SCHEMA: test
  27. OBJECT_NAME: NULL
  28. OBJECT_INSTANCE_BEGIN: 140450195434272
  29. LOCK_TYPE: INTENTION_EXCLUSIVE
  30. LOCK_DURATION: TRANSACTION
  31. LOCK_STATUS: GRANTED
  32. SOURCE: sql_base.cc:5209
  33. OWNER_THREAD_ID: 30
  34. OWNER_EVENT_ID: 8
  35. *************************** 4. row ***************************
  36. OBJECT_TYPE: TABLE
  37. OBJECT_SCHEMA: test
  38. OBJECT_NAME: t
  39. OBJECT_INSTANCE_BEGIN: 140450195434368
  40. LOCK_TYPE: EXCLUSIVE
  41. LOCK_DURATION: TRANSACTION
  42. LOCK_STATUS: PENDING
  43. SOURCE: sql_parse.cc:5585
  44. OWNER_THREAD_ID: 30
  45. OWNER_EVENT_ID: 8
  46. *************************** 5. row ***************************
  47. OBJECT_TYPE: TABLE
  48. OBJECT_SCHEMA: performance_schema
  49. OBJECT_NAME: metadata_locks
  50. OBJECT_INSTANCE_BEGIN: 140450128262384
  51. LOCK_TYPE: SHARED_READ
  52. LOCK_DURATION: TRANSACTION
  53. LOCK_STATUS: GRANTED
  54. SOURCE: sql_parse.cc:5585
  55. OWNER_THREAD_ID: 27
  56. OWNER_EVENT_ID: 18
  57. 5 rows in set (0.00 sec)

如上所示,在t表上,持有一个SHARE_READ lock,而且还有一个EXCULSIVE lock请求是pending状态,也就是我们被阻塞的session 2。

在5.7之前,我们可以通过show processlist,来查看MDL阻塞的情况,但无法获取session 1的信息:

  1. mysql> SELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, THREAD_ID, PROCESSLIST_ID, PROCESSLIST_INFO FROM performance_schema.metadata_locks INNER JOIN performance_schema.threads ON THREAD_ID = OWNER_THREAD_ID WHERE PROCESSLIST_ID <> CONNECTION_ID();
  2. +-------------+---------------+-------------+---------------------+-------------+-----------+----------------+------------------+
  3. | OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE | LOCK_STATUS | THREAD_ID | PROCESSLIST_ID | PROCESSLIST_INFO |
  4. +-------------+---------------+-------------+---------------------+-------------+-----------+----------------+------------------+
  5. | GLOBAL | NULL | NULL | INTENTION_EXCLUSIVE | GRANTED | 30 | 8 | truncate table t |
  6. | SCHEMA | test | NULL | INTENTION_EXCLUSIVE | GRANTED | 30 | 8 | truncate table t |
  7. | TABLE | test | t | EXCLUSIVE | PENDING | 30 | 8 | truncate table t |
  8. +-------------+---------------+-------------+---------------------+-------------+-----------+----------------+------------------+
  9. 3 rows in set (0.00 sec)
  10. mysql> show processlist;
  11. +----+------+-----------+------+---------+------+---------------------------------+------------------+
  12. | Id | User | Host | db | Command | Time | State | Info |
  13. +----+------+-----------+------+---------+------+---------------------------------+------------------+
  14. | 5 | root | localhost | test | Query | 0 | starting | show processlist |
  15. | 8 | root | localhost | test | Query | 50 | Waiting for table metadata lock | truncate table t |
  16. +----+------+-----------+------+---------+------+---------------------------------+------------------+
  17. 2 rows in set (0.00 sec)

接下来当事务提交了后,释放MDL锁再查询,就看不到MDL锁的信息了。

  1. mysql> commit;
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> SELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, THREAD_ID, PROCESSLIST_ID, PROCESSLIST_INFO FROM performance_schema.metadata_locks INNER JOIN performance_schema.threads ON THREAD_ID = OWNER_THREAD_ID WHERE PROCESSLIST_ID <> CONNECTION_ID();
  4. Empty set (0.01 sec)
  5. mysql> select * from t;
  6. Empty set (0.00 sec)

MySQL 5.7可以通过performance schema来检索MDL锁阻塞情况,方便DBA来诊断问题。