10.11.4 元数据锁定
MySQL 使用元数据锁定来管理对数据库对象的并发访问和确保数据一致性。元数据锁定不仅适用于表,也适用于架构、存储程序(过程、函数、触发器、计划事件)、表空间、用户锁(使用GET_LOCK()
函数获取,见第14.14节,“锁定函数”)、使用locking服务描述在第7.6.9.1节,“锁定服务”中获取的锁。
性能_schema中的metadata_locks
表公开元数据锁定信息,这可以有助于查看哪些会话持有锁、阻塞等待锁等信息。详细信息见第29.12.13.3节,“metadata_locks表”。
元数据锁定涉及一些开销,这些开销随着查询 volume 的增加而增加。元数据争用随着多个查询尝试访问同一对象的次数增加而增加。
元数据锁定不是表定义缓存的替代品,它们的互斥体和锁不同于LOCK_open
互斥体。以下讨论提供了关于元数据锁定的工作原理的一些信息。
如果有多个等待者请求同一个锁,最高优先级的锁请求将首先被满足,除非与max_write_lock_count
系统变量相关的异常。如果max_write_lock_count
设置为较低值(例如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_新
开始。三个客户端发出涉及这些表的语句:
客户端 1:
LOCK TABLE x WRITE, x_new WRITE;
该语句在名称顺序上请求和获取对x
和x_新
的写锁。
客户端2:
INSERT INTO x VALUES(1);
该语句请求对x
的写锁,并等待该锁的释放。
客户端3:
RENAME TABLE x TO x_old, x_new TO x;
该语句在名称顺序上请求独占锁对x
、x_新
和x_旧
,但等待对x
的锁释放。
客户端1:
UNLOCK TABLES;
该语句释放了对x
和x_新
的写锁。客户端3对x
的独占锁请求优先于客户端2对x
的写锁请求,因此客户端3首先获取对x
的锁,然后是对x_新
和x_旧
,执行重命名操作,并释放其锁。然后客户端2获取对x
的锁,执行插入操作,并释放其锁。
锁定顺序导致RENAME TABLE
语句在INSERT
语句之前执行。插入操作将发生在客户端2对x_新
的插入和客户端3对其重命名到x
时:
mysql> SELECT * FROM x;
+------+
| i |
+------+
| 1 |
+------+
mysql> SELECT * FROM x_old;
Empty set (0.01 sec)
现在开始使用名称相同的表x
和new_x
,它们具有 identical 结构。再次,三个客户端发出了涉及这些表的语句:
客户端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
oleh 重命名操作:
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
语句中获取的元数据锁,在语句准备完成后即被释放,即使准备过程发生在多语句事务中。
对于XA事务在PREPARED
状态下,元数据锁将保持客户端断开和服务器重启期间不变,直到执行XA COMMIT
或XA ROLLBACK
语句。