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