一个 锁定读取、一个 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
设置为 0。如果没有获取InnoDB
表级锁,即使表中的一些记录被其他事务锁定,LOCK TABLES
也会完成。在 MySQL 9.0 中,
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
。