8.11.4 Metadata Locking

209 阅读6分钟

dev.mysql.com/doc/refman/…

8.11.4 Metadata Locking

MySQL使用元数据锁定来管理对数据库对象的并发访问,并确保数据一致性。元数据锁定不仅适用于表,还适用于模式和存储程序(过程、函数、触发器和计划事件)。

元数据锁定确实涉及一些开销,这些开销会随着查询量的增加而增加。当多个查询试图访问相同的对象时,元数据争用就会增加。

元数据锁定不是表定义缓存的替代品,它的互斥锁和锁不同于LOCK_open互斥锁。下面的讨论提供了一些关于元数据锁定如何工作的信息。

Metadata Lock Acquisition

如果一个给定的锁有多个等待者,那么优先级最高的锁请求将首先得到满足,但会出现与max_write_lock_count系统变量相关的异常。写锁请求的优先级高于读锁请求。但是,如果max_write_lock_count被设置为一个较低的值(比如10),那么读锁请求可能会优先于挂起的写锁请求,如果读锁请求已经被传递给了10个写锁请求。通常情况下,这种行为不会发生,因为max_write_lock_count默认值非常大。

语句逐个获取元数据锁,而不是同时获取,并在此过程中执行死锁检测。

DML语句通常按照语句中提到表的顺序获取锁。

DDL语句、LOCK TABLES和其他类似语句试图通过按名称顺序获取显式命名表上的锁来减少并发DDL语句之间可能的死锁数量。对于隐式使用的表(例如外键关系中的表,也必须被锁定),可能以不同的顺序获得锁。

例如,RENAME TABLE是一个DDL语句,它按名称顺序获取锁:

  • RENAME TABLE语句将tbla重命名为其他名称,并将tblc重命名为tbla:

    RENAME TABLE tbla TO tbld, tblc TO tbla;
    

    该语句在tbla、tblc和tbld上按顺序获取元数据锁(因为tbld在名称顺序上紧随tblc):

  • 这个稍有不同的语句还将tbla重命名为其他东西,并将tblc重命名为tbla:

    RENAME TABLE tbla TO tblb, tblc TO tbla;
    

    在这种情况下,该语句在tbla、tblb和tblc上按顺序获取元数据锁(因为tblb在名称顺序上先于tblc):

这两个语句都按此顺序获取tbla和tblc上的锁,但不同的是,在tblc之前还是之后获取其余表名上的锁。

当多个事务并发执行时,元数据锁获取顺序会影响操作结果,如下面的示例所示。

从两个具有相同结构的表x和x_new开始。三个客户端发出包含以下表的语句:

Client 1:

LOCK TABLE x WRITE, x_new WRITE;

The statement requests and acquires write locks in name order on x and x_new.

Client 2:

INSERT INTO x VALUES(1);

The statement requests and blocks waiting for a write lock on x.

Client 3:

RENAME TABLE x TO x_old, x_new TO x;

语句按名称顺序请求对x、x_new和x_old的排它锁,但阻塞等待对x的锁。

Client 1:

UNLOCK TABLES;

该语句释放对x和x_new的写锁。Client 3对x的排它锁请求比Client 2的写锁请求具有更高的优先级,因此Client 3获得它对x的锁,然后是对x_new和x_old的锁,执行重命名,并释放它的锁。然后客户端2获取它对x的锁,执行插入,然后释放它的锁。

锁获取顺序导致RENAME TABLE在INSERT之前执行。插入发生的x是Client 2发出插入时命名为x_new的表,被Client 3重命名为x:

mysql> SELECT * FROM x;
+------+
| i    |
+------+
|    1 |
+------+
​
mysql> SELECT * FROM x_old;
Empty set (0.01 sec)

现在从具有相同结构的名为x和new_x的表开始。同样,有三个客户端发出涉及以下表的语句:

Client 1:

LOCK TABLE x WRITE, new_x WRITE;

该语句在new_x和x上按名称顺序请求和获取写锁。

Client 2:

INSERT INTO x VALUES(1);

语句请求和阻塞等待x上的写锁。

Client 3:

RENAME TABLE x TO old_x, new_x TO x;

该语句按名称顺序请求new_x、old_x和x上的排它锁,但阻塞了等待new_x上的锁。

Client 1:

UNLOCK TABLES;

该语句释放x和new_x上的写锁。对于x,唯一挂起的请求是客户机2,因此客户机2获得它的锁,执行插入,然后释放锁。对于new_x,唯一挂起的请求来自Client 3,它被允许获得该锁(以及old_x上的锁)。重命名操作仍然会阻塞x上的锁,直到Client 2插入完成并释放锁。然后Client 3获取x上的锁,执行重命名,并释放它的锁。

在这种情况下,锁获取顺序导致INSERT在RENAME TABLE之前执行。插入到的x是原来的x,现在通过重命名操作重命名为old_x:

mysql> SELECT * FROM x;
Empty set (0.01 sec)
​
mysql> SELECT * FROM old_x;
+------+
| i    |
+------+
|    1 |
+------+

Metadata Lock Release

为了确保事务的可序列化性,服务器必须不允许一个会话在另一个会话中未完成的显式或隐式启动的事务中使用的表上执行数据定义语言(DDL)语句。服务器通过获取事务中使用的表上的元数据锁并将这些锁的释放推迟到事务结束来实现这一点。表上的元数据锁可以防止对表结构的更改。这种锁定方法意味着,在事务结束之前,由一个会话中的事务使用的表不能被其他会话在DDL语句中使用。

这个原则不仅适用于事务性表,也适用于非事务性表。假设一个会话开始一个使用事务表t和非事务表nt的事务,如下所示:

START TRANSACTION;
SELECT * FROM t;
SELECT * FROM nt;

服务器在t和nt上都持有元数据锁,直到事务结束。如果另一个会话试图在任何一个表上执行DDL或写锁操作,它将阻塞,直到事务端释放元数据锁。例如,如果第二个会话尝试以下任何操作,它就会阻塞:

DROP TABLE t;
ALTER TABLE t ...;
DROP TABLE nt;
ALTER TABLE nt ...;
LOCK TABLE t ... WRITE;

如果服务器为语法有效但在执行过程中失败的语句获取元数据锁,则不会提前释放锁。锁释放仍然延迟到事务结束,因为失败的语句被写入二进制日志,而锁保护日志一致性。

在自动提交模式中,每条语句实际上都是一个完整的事务,因此为语句获取的元数据锁只保存到语句的末尾。

在PREPARE语句期间获取的元数据锁在准备好语句后被释放,即使准备是在多语句事务中进行的。

总结

本文内容时讲元数据锁定。分为2个模块讲了下。包括元数据锁获取和元数据锁释放。

注意这个元数据锁跟InnoDB没关系。

  • 元数据锁释放

    这块用2个重命名表的sql例子说明了阻塞获取锁是按照表名的顺序来的。

  • 元数据锁释放

    直接说明了MySQL服务器会给1个事务用到的表都加上元数据锁(包括非事务表)。加上这个锁后在这个事务提交前,其他会话执行DDL语句都会阻塞。

    我按文中例子试过确实这样。

    会话1:

    START TRANSACTION; -- 手动开启1个事务
    select * from t;
    COMMIT;
    

    会话2:

    DROP TABLE t;
    

    文中也提到这保证了事务的可序列化(To ensure transaction serializability)