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
。