Documentation Home
MySQL 8.4 Reference Manual
Related Documentation Download this Manual
PDF (US Ltr) - 39.8Mb
PDF (A4) - 39.9Mb
Man Pages (TGZ) - 257.9Kb
Man Pages (Zip) - 364.9Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb


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;

    该语句在顺序上获取元数据锁定,分别对 tblatblctbld(因为 tbld 在名称顺序中跟随 tblc):

  • 这个略微不同的语句也将 tbla 重命名为其他名称,并将 tblc 重命名为 tbla

    RENAME TABLE tbla TO tblb, tblc TO tbla;

    在这种情况下,语句获取元数据锁定,顺序对 tblatblbtblc(因为 tblb 在名称顺序中前置 tblc):

这两个语句都获取了对 tblatblc 的锁定,但它们之间的差异在于,是否在获取剩余表名锁定之前或之后获取 tblc 的锁定。

元数据锁定顺序可能会在多个事务并发执行时影响操作结果,如以下示例所示。

从两个结构相同的表 xx_新 开始。三个客户端发出涉及这些表的语句:

客户端 1:

LOCK TABLE x WRITE, x_new WRITE;

该语句在名称顺序上请求和获取对xx_新的写锁。

客户端2:

INSERT INTO x VALUES(1);

该语句请求对x的写锁,并等待该锁的释放。

客户端3:

RENAME TABLE x TO x_old, x_new TO x;

该语句在名称顺序上请求独占锁对xx_新x_旧,但等待对x的锁释放。

客户端1:

UNLOCK TABLES;

该语句释放了对xx_新的写锁。客户端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)

现在开始使用名称相同的表xnew_x,它们具有 identical 结构。再次,三个客户端发出了涉及这些表的语句:

客户端1:

LOCK TABLE x WRITE, new_x WRITE;

该语句在名称顺序上请求和获取对 new_xx 的写锁。

客户端 2:

INSERT INTO x VALUES(1);

该语句请求并阻塞,等待对 x 的写锁。

客户端 3:

RENAME TABLE x TO old_x, new_x TO x;

该语句在名称顺序上请求独占锁对 new_xold_xx,但阻塞等待对 new_x 的锁。

客户端 1:

UNLOCK TABLES;

该语句释放了对 xnew_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;

服务器将在事务结束时释放对tnt的元数据锁定。如果另一个会话尝试在任何表上执行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 COMMITXA ROLLBACK语句。