锁定读取、UPDATE
或 DELETE
通常在处理 SQL 语句时设置每个索引记录的锁。无论语句中是否有 WHERE
条件,它们都不会被排除。InnoDB 不记忆确切的 WHERE
条件,只知道哪些索引范围被扫描。锁通常是 next-key 锁,它们也阻止了紧挨着记录的 “间隙” 中的插入。然而,间隙锁 可以被明确禁用,从而不使用 next-key 锁。有关更多信息,请参阅 第 17.7.1 节,“InnoDB 锁定”。事务隔离级别也可以影响设置哪些锁;请参阅 第 17.7.2.1 节,“事务隔离级别”。
如果在搜索中使用了辅助索引,并且要设置的索引记录锁是独占的,InnoDB 也会检索相应的聚簇索引记录,并设置锁。
如果您没有合适的索引来满足语句,MySQL 必须扫描整个表来处理语句,这将锁定表中的每一行,从而阻止其他用户对表的插入。因此,创建良好的索引非常重要,以便您的查询不扫描不必要的行。
InnoDB
按照以下方式设置特定的锁类型。
-
SELECT ... FROM
是一致的读取,读取数据库的快照,不设置锁,除非事务隔离级别设置为SERIALIZABLE
。对于SERIALIZABLE
级别,搜索设置共享的 next-key 锁在它遇到的索引记录上。然而,只需要一个索引记录锁来锁定使用唯一索引搜索唯一行的行。 -
SELECT ... FOR UPDATE
和SELECT ... FOR SHARE
语句使用唯一索引锁定扫描的行,并释放不符合结果集的行的锁(例如,如果它们不满足WHERE
子句中的条件)。然而,在某些情况下,行可能不会立即解锁,因为结果行与其原始来源之间的关系在查询执行期间丢失了。例如,在UNION
中,扫描(和锁定)表中的行可能会被插入到临时表中,以评估它们是否符合结果集。在这种情况下,临时表中的行与原始表中的行之间的关系丢失了,直到查询执行结束时原始表中的行才被解锁。 -
对于 锁定读取(
SELECT
带FOR UPDATE
或FOR SHARE
)、UPDATE
和DELETE
语句,锁的设置取决于语句是否使用唯一索引和唯一搜索条件或范围搜索条件。-
对于唯一索引和唯一搜索条件,
InnoDB
只锁定找到的索引记录,而不是锁定它之前的 间隙。 -
对于其他搜索条件和非唯一索引,
InnoDB
锁定扫描的索引范围,使用 间隙锁 或 next-key 锁 来阻止其他会话中的插入到扫描范围内的间隙中。有关间隙锁和 next-key 锁的信息,请参阅 第 17.7.1 节,“InnoDB 锁定”。
-
-
对于索引记录,搜索遇到的记录,
SELECT ... FOR UPDATE
阻止其他会话执行SELECT ... FOR SHARE
或在某些事务隔离级别下读取。一致读取忽略读视图中存在的记录锁。 -
UPDATE ... WHERE ...
在搜索遇到的每个记录上设置独占的 next-key 锁。然而,只需要索引记录锁来锁定使用唯一索引搜索唯一行的行。 -
当
UPDATE
修改聚簇索引记录时,隐式锁定受影响的辅助索引记录。UPDATE
操作还在执行重复检查扫描之前锁定受影响的辅助索引记录,并在插入新辅助索引记录时锁定。 -
DELETE FROM ... WHERE ...
在搜索遇到的每个记录上设置独占的 next-key 锁。然而,只需要索引记录锁来锁定使用唯一索引搜索唯一行的行。 -
INSERT
在插入的行上设置独占锁。这锁是一个索引记录锁,而不是 next-key 锁(即,不是间隙锁),并且不阻止其他会话在插入行之前的间隙中插入。在插入行之前,一个类型的间隙锁称为插入意图间隙锁被设置。这锁信号意图插入,以便多个事务插入同一个索引间隙不需要等待对方,如果它们不是在同一位置插入间隙。假设有索引记录的值为 4 和 7。单独的事务尝试插入值 5 和 6,每个锁定索引间隙的插入意图锁之前获得插入行的独占锁,但它们不阻塞对方,因为行不是冲突的。
如果发生重复键错误,设置共享锁在重复索引记录上。这使用共享锁可能导致死锁,如果有多个会话尝试插入同一行,而另一个会话已经拥有独占锁。这可能发生在另一个会话删除该行时。假设
InnoDB
表t1
具有以下结构:CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
现在假设三个会话按顺序执行以下操作:
会话 1:
START TRANSACTION; INSERT INTO t1 VALUES(1);
会话 2:
START TRANSACTION; INSERT INTO t1 VALUES(1);
会话 3:
START TRANSACTION; INSERT INTO t1 VALUES(1);
会话 1:
ROLLBACK;
会话 1 的第一个操作获取该行的独占锁。会话 2 和 3 的操作都导致重复键错误,并且它们都请求该行的共享锁。当会话 1 回滚时,它释放该行的独占锁,并且会话 2 和 3 的共享锁请求被授予。此时,会话 2 和 3 死锁:它们都不能获取该行的独占锁,因为对方持有共享锁。
类似情况也会发生,如果表已经包含键值为 1 的行,并且三个会话按顺序执行以下操作:
会话 1:
START TRANSACTION; DELETE FROM t1 WHERE i = 1;
会话 2:
START TRANSACTION; INSERT INTO t1 VALUES(1);
会话 3:
START TRANSACTION; INSERT INTO t1 VALUES(1);
会话 1:
COMMIT;
会话 1 的第一个操作获取该行的独占锁。会话 2 和 3 的操作都导致重复键错误,并且它们都请求该行的共享锁。当会话 1 提交时,它释放该行的独占锁,并且会话 2 和 3 的共享锁请求被授予。此时,会话 2 和 3 死锁:它们都不能获取该行的独占锁,因为对方持有共享锁。
-
INSERT ... ON DUPLICATE KEY UPDATE
与简单的INSERT
不同的是,当重复键错误发生时,独占锁而不是共享锁被设置在要更新的行上。当重复主键值时,独占索引记录锁被设置;当重复唯一键值时,独占 next-key 锁被设置。 -
INSERT INTO T SELECT ... FROM S WHERE ...
在插入到T
时设置独占索引记录锁(不包括间隙锁)在每一行上。如果事务隔离级别是READ COMMITTED
,InnoDB
将在S
上执行一致读取(不锁定)。否则,InnoDB
将在S
上设置共享 next-key 锁。在回滚恢复使用基于语句的二进制日志时,每个 SQL 语句都必须以完全相同的方式执行。CREATE TABLE ... SELECT ...
执行SELECT
,使用共享 next-key 锁或一致读取,类似于INSERT ... SELECT
。当
SELECT
用于构造REPLACE INTO t SELECT ... FROM s WHERE ...
或UPDATE t ... WHERE col IN (SELECT ... FROM s ...)
时,InnoDB
将在表s
上设置共享 next-key 锁。 -
InnoDB
在初始化之前指定的AUTO_INCREMENT
列时,设置关联索引的独占锁。使用
innodb_autoinc_lock_mode=0
,InnoDB
使用特殊的AUTO-INC
表锁模式,其中锁被获取并持有到当前 SQL 语句的结尾(而不是整个事务的结尾),同时访问自动递增计数器。其他客户端不能在AUTO-INC
表锁被持有时插入表中。同样的行为也出现在 “批量插入” 中,使用innodb_autoinc_lock_mode=1
。表级AUTO-INC
锁不用于innodb_autoinc_lock_mode=2
。更多信息,请参阅 第 17.6.1.6 节,“InnoDB 中的 AUTO_INCREMENT 处理”。InnoDB
在不设置锁的情况下获取之前初始化的AUTO_INCREMENT
列的值。 -
如果在表上定义了
FOREIGN KEY
约束,那么任何插入、更新或删除操作都需要检查约束条件,InnoDB
将在检查约束时设置共享记录级锁。在约束失败的情况下,InnoDB
也设置这些锁。 -
LOCK TABLES
设置表锁,但这是 MySQL 层以上的InnoDB
层设置这些锁。InnoDB
如果innodb_table_locks = 1
(默认)和autocommit = 0
,则知道表锁。MySQL 层以上的InnoDB
知道行级锁。否则,
InnoDB
的自动死锁检测无法检测涉及表锁的死锁。此外,因为在这种情况下 MySQL 层不知道行级锁,因此可能在另一个会话当前拥有行级锁的表上获取表锁。但这并不危及事务完整性,如 第 17.7.5.2 节,“死锁检测” 中所讨论的那样。 -
LOCK TABLES
如果innodb_table_locks=1
(默认),则在每个表上获取两个锁。在 MySQL 层上的表锁之外,还获取InnoDB
表锁。要避免获取InnoDB
表锁,请设置innodb_table_locks=0
。如果不获取InnoDB
表锁,LOCK TABLES
将完成,即使某些记录被其他事务锁定。在 MySQL 8.3 中,
innodb_table_locks=0
对于使用LOCK TABLES ... WRITE
显式锁定的表没有效果。但是,它对通过LOCK TABLES ... WRITE
隐式锁定以供读取或写入的表(例如,通过触发器)或通过LOCK TABLES ... READ
锁定的表有影响。 -
所有由事务持有的
InnoDB
锁在事务提交或回滚时释放。因此,在autocommit=1
模式下对InnoDB
表调用LOCK TABLES
没有太多意义,因为获取的InnoDB
表锁将立即释放。 -
您不能在事务中间锁定其他表,因为
LOCK TABLES
执行隐式COMMIT
和UNLOCK TABLES
。