MySQL 使用元数据锁定来管理对数据库对象的并发访问,并确保数据一致性。元数据锁定不仅适用于表,还适用于架构、存储程序(过程、函数、触发器、计划事件)、表空间、使用 GET_LOCK()
函数(请参见 第 14.14 节“锁定函数”)获取的用户锁,以及在 第 7.6.9.1 节“锁定服务” 中描述的锁定服务中获取的锁。
Performance Schema metadata_locks
表公开元数据锁定信息,这对于查看哪些会话持有锁、哪些会话正在等待获取锁等等很有用。有关详细信息,请参见 第 29.12.13.3 节“metadata_locks 表”.
元数据锁定确实会产生一些开销,随着查询量的增加,开销也会增加。元数据争用在多个查询尝试访问同一个对象时会增加。
元数据锁定并非表定义缓存的替代方案,其互斥锁和锁与 LOCK_open
互斥锁不同。以下讨论提供了一些关于元数据锁定工作原理的信息。
如果有多个等待者等待同一个锁,那么优先级最高的锁定请求将首先得到满足,但与 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
开始。三个客户端发出涉及这些表的语句
客户端 1
LOCK TABLE x WRITE, x_new WRITE;
该语句请求并按名称顺序获取 x
和 x_new
的写锁。
客户 2
INSERT INTO x VALUES(1);
该语句请求写锁并阻塞,等待对 x
的写锁。
客户 3
RENAME TABLE x TO x_old, x_new TO x;
该语句请求对 x
、x_new
和 x_old
以名称顺序进行排他锁,但阻塞等待对 x
的锁。
客户端 1
UNLOCK TABLES;
该语句释放对 x
和 x_new
的写锁。客户 3 对 x
的排他锁请求优先级高于客户 2 的写锁请求,因此客户 3 获得了对 x
的锁,然后也获得了对 x_new
和 x_old
的锁,执行重命名操作,并释放其锁。然后客户 2 获得了对 x
的锁,执行插入操作,并释放其锁。
锁获取顺序导致 RENAME TABLE
在 INSERT
之前执行。插入操作发生的 x
是在客户 2 发出插入操作时名为 x_new
的表,后来被客户 3 重命名为 x
。
mysql> SELECT * FROM x;
+------+
| i |
+------+
| 1 |
+------+
mysql> SELECT * FROM x_old;
Empty set (0.01 sec)
现在,从名为 x
和 new_x
的表开始,这两个表具有相同的结构。同样,三个客户发出涉及这些表的语句。
客户端 1
LOCK TABLE x WRITE, new_x WRITE;
该语句请求并获得对 new_x
和 x
以名称顺序进行写锁。
客户 2
INSERT INTO x VALUES(1);
该语句请求写锁并阻塞,等待对 x
的写锁。
客户 3
RENAME TABLE x TO old_x, new_x TO x;
该语句请求对 new_x
、old_x
和 x
以名称顺序进行排他锁,但阻塞等待对 new_x
的锁。
客户端 1
UNLOCK TABLES;
该语句释放对 x
和 new_x
的写锁。对于 x
,唯一待处理的请求来自客户 2,因此客户 2 获得了其锁,执行插入操作,并释放锁。对于 new_x
,唯一待处理的请求来自客户 3,允许其获得该锁(以及对 old_x
的锁)。重命名操作仍然阻塞等待对 x
的锁,直到客户 2 的插入操作完成并释放其锁。然后客户 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 |
+------+
如果并发语句中锁获取顺序对应用程序的操作结果有影响,如前面的示例所示,您可以调整表名以影响锁获取顺序。
元数据锁根据需要扩展到通过外键约束相关的表,以防止在相关表上同时执行冲突的 DML 和 DDL 操作。更新父表时,会在更新外键元数据时对子表进行元数据锁。外键元数据由子表拥有。
为了确保事务可串行化,服务器必须不允许一个会话对另一个会话中未完成的显式或隐式启动的事务中使用的表执行数据定义语言 (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;
相同的行为适用于 LOCK TABLES ... READ
。也就是说,显式或隐式启动的事务更新任何表(事务表或非事务表)都会被阻塞,并且会被该表的 LOCK TABLES ... READ
阻塞。
如果服务器为语法上有效但执行期间失败的语句获取元数据锁,则不会提前释放锁。锁释放仍然延迟到事务结束,因为失败的语句被写入二进制日志,并且锁保护日志一致性。
在自动提交模式下,每个语句实际上都是一个完整的事务,因此为语句获取的元数据锁仅保留到语句结束。
在 PREPARE
语句期间获取的元数据锁在语句准备完成后立即释放,即使准备发生在多语句事务中。
对于处于 PREPARED
状态的 XA 事务,元数据锁将在客户端断开连接和服务器重启期间一直维护,直到执行 XA COMMIT
或 XA ROLLBACK
。