MySQL 使用元数据锁定来管理数据库对象的并发访问和确保数据一致性。元数据锁定不仅适用于表,还适用于模式、存储程序(过程、函数、触发器、计划事件)、表空间、使用 GET_LOCK()
函数获取的用户锁(见 第 14.14 节,“锁定函数”),以及锁定服务描述的锁(见 第 7.6.9.1 节,“锁定服务”)。
性能模式 metadata_locks
表公开了元数据锁定信息,这对于查看哪些会话持有锁、被阻塞等待锁、等等非常有用。详见 第 29.12.13.3 节,“metadata_locks 表”。
元数据锁定确实涉及一些开销,这些开销随着查询量的增加而增加。元数据争用随着多个查询尝试访问相同对象的次数增加而增加。
元数据锁定不是表定义缓存的替代品,其互斥锁和锁与 LOCK_open
互斥锁不同。以下讨论提供了一些关于元数据锁定如何工作的信息。
如果有多个等待者等待某个锁,则最高优先级锁请求首先被满足,除非与 max_write_lock_count
系统变量相关的例外情况。写锁请求的优先级高于读锁请求。然而,如果 max_write_lock_count
设置为某个低值(例如 10),读锁请求可能会优先于待定的写锁请求,如果读锁请求已经被写锁请求所跳过。
语句逐个获取元数据锁,不是同时获取,并在过程中执行死锁检测。
DML 语句通常按照语句中表的顺序获取锁。
DDL 语句、LOCK TABLES
以及其他类似语句尝试通过按照名称顺序获取锁来减少可能的死锁。
例如,RENAME TABLE
是一个 DDL 语句,它按照名称顺序获取锁:
-
这个
RENAME TABLE
语句将tbla
重命名为其他名称,并将tblc
重命名为tbla
:RENAME TABLE tbla TO tbld, tblc TO tbla;
该语句按照名称顺序获取元数据锁,在
tbla
、tblc
和tbld
上: -
这个稍有不同的语句也将
tbla
重命名为其他名称,并将tblc
重命名为tbla
:RENAME TABLE tbla TO tblb, tblc TO tbla;
在这种情况下,该语句按照名称顺序获取元数据锁,在
tbla
、tblb
和tblc
上:
这两个语句都获取了 tbla
和 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
之前执行。插入发生在客户端 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
之前执行。插入发生在客户端 2 发出插入语句时命名为 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
语句期间获取的元数据锁将在语句被准备好后释放,即使准备发生在多语句事务中。
对于 XA 事务在 PREPARED
状态,元数据锁将在客户端断开连接和服务器重新启动时保持,直到执行 XA COMMIT
或 XA ROLLBACK
。