17.7.3 InnoDB 中不同 SQL 语句设置的锁定
一个locking read、一个UPDATE
或一个DELETE
通常在处理 SQL 语句时对每个索引记录设置记录锁定。是否存在WHERE
条件都无关紧要。InnoDB
不记忆exact WHERE
条件,但只知道哪些索引范围被扫描。锁定通常是next-key 锁定,也阻止在记录前面的gap中插入记录。但是,可以explicitly disable gap locking,从而使 next-key 锁定不被使用。更多信息,请见第17.7.1节,“InnoDB Locking”。事务隔离级别也可以影响设置的锁定;请见第17.7.2.1节,“Transaction Isolation Levels”。
如果在搜索中使用了次级索引,并且要设置的索引记录锁定是独占的,InnoDB
还将检索对应的聚簇索引记录并设置锁定于它们上。
如果您没有适合语句的索引,MySQL必须扫描整个表来处理语句,每个表行都将被锁定,从而阻止其他用户对该表进行插入。因此,创建良好的索引非常重要,以避免查询扫描更多行。
InnoDB
设置以下类型的锁定。
-
SELECT ... FROM
是一致读取,读取数据库快照,并且在事务隔离级别设置为SERIALIZABLE
时设置锁定。对于SERIALIZABLE
级别,搜索将在遇到索引记录时设置共享next-key锁定。然而,对于使用唯一索引来搜索唯一行的语句,只需要索引记录锁定。 -
SELECT ... FOR UPDATE
和SELECT ... FOR SHARE
语句使用唯一索引获取扫描行的锁定,并在结果集中不符合条件的行上释放锁定(例如,如果它们不满足 WHERE 子句中的条件)。然而,在某些情况下,行可能不会立即解锁,因为查询执行过程中结果行与原始源之间的关系被丢失。例如,在UNION
中,扫描(并锁定)表中的行可能会在评估它们是否符合结果集的条件之前被插入到临时表中。在这种情况下,临时表中的行与原始表中的行之间的关系丢失,直到查询执行结束时才解锁这些行。 -
对于 读取锁定 (
SELECT
withFOR UPDATE
或FOR SHARE
),UPDATE
和DELETE
语句,取决于语句是否使用唯一索引与唯一搜索条件或范围类型搜索条件。-
对于具有唯一搜索条件的唯一索引,
InnoDB
只锁定找到记录,而不是空隙。 -
对于其他搜索条件和非唯一索引,
InnoDB
锁定扫描的索引范围,使用空隙锁或next-key 锁来阻止其他会话在范围内的空隙中插入记录。关于空隙锁和next-key 锁的信息,请见第17.7.1节,“InnoDB Locking”。
-
-
SELECT ... FOR UPDATE
对于搜索到的索引记录,阻止其他会话执行SELECT ... FOR SHARE
或在某些事务隔离级别下读取。consistent reads忽略锁定在读视图中的记录。 -
UPDATE ... WHERE ...
对于搜索到的每个记录设置独占的next-key 锁。然而,对于使用唯一索引来搜索唯一行的语句,只需要索引记录锁定。 -
当
UPDATE
修改聚簇索引记录时,隐式锁定影响的次要索引记录。UPDATE操作还在执行重复检查扫描和插入新次要索引记录之前,获取共享锁定对影响的次要索引记录,并在插入新次要索引记录时也获取共享锁定。 -
DELETE FROM ... WHERE ...
对每个搜索到的记录设置独占的下一个键锁定。然而,对于使用唯一索引来搜索唯一行的语句,只需要索引记录锁定。 -
INSERT
对插入的行设置独占锁定。这是索引记录锁定,而不是下一个键锁定(即没有gap锁),因此不阻止其他会话在插入之前的gap中插入数据。在插入行之前,设置一种类型的间隙锁称为插入意图间隙锁。这类锁表明了插入的意图,使得多个事务在插入同一个间隙时,如果它们不在间隙内的相同位置插入,则无需相互等待。例如,如果索引记录的值是 4 和 7,分别有两个事务尝试插入值 5 和 6,每个事务在获取插入行的排他锁之前,都会先锁定间隙之间的插入意图锁,但不会阻止对方,因为这些行并不冲突。
如果出现重复键错误,共享锁将被设置到重复索引记录上。这使用共享锁可能会导致死锁,如果有多个会话尝试插入相同的行,而另一个会话已经持有排他锁。这种情况可能发生在另一个会话删除该行。假设有一个
InnoDB
表t1
,其结构如下:CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
现在假设三个会话按顺序执行以下操作:
Session 1:
START TRANSACTION; INSERT INTO t1 VALUES(1);
Session 2:
START TRANSACTION; INSERT INTO t1 VALUES(1);
Session 3:
START TRANSACTION; INSERT INTO t1 VALUES(1);
Session 1:
ROLLBACK;
会话1的第一个操作获取了该行的排他锁。会话2和会话3都导致重复键错误,并请求对该行的共享锁。当会话1回滚时,它释放了对该行的排他锁,会话2和会话3的队列共享锁请求被授予。在这个点上,会话2和会话3死锁:它们不能获取该行的排他锁,因为另一个会话持有共享锁。
类似情况发生在表中已经包含键值为1的行,并三个会话按顺序执行以下操作:
Session 1:
START TRANSACTION; DELETE FROM t1 WHERE i = 1;
Session 2:
START TRANSACTION; INSERT INTO t1 VALUES(1);
Session 3:
START TRANSACTION; INSERT INTO t1 VALUES(1);
Session 1:
COMMIT;
会话1的第一个操作获取了该行的排他锁。会话2和会话3都导致重复键错误,并请求对该行的共享锁。当会话1提交时,它释放了对该行的排他锁,会话2和会话3的队列共享锁请求被授予。在这个点上,会话2和会话3死锁:它们不能获取该行的排他锁,因为另一个会话持有共享锁。
-
INSERT ... ON DUPLICATE KEY UPDATE
与简单的INSERT
不同的是,在出现重复键错误时,会将独占锁置于要更新的行上,而不是共享锁。对于重复主键值,会获取独占索引记录锁;对于重复唯一键值,会获取独占下一个键锁。 -
INSERT INTO T SELECT ... FROM S WHERE ...
对每个插入到T
中的行设置独占索引记录锁(不包括gap锁)。如果事务隔离级别为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 ...)
时,InnoDB
对表s
的行设置共享下一个键锁定。 -
InnoDB
在初始化之前指定的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
设置表锁,但是在 MySQL 高层次上设置这些锁的是InnoDB
层。InnoDB
如果innodb_ table_locks = 1
(默认)和autocommit = 0
,那么它会知道表锁。如果 MySQL 高层次上InnoDB
知道行级锁。否则,
InnoDB
自动死锁检测不能检测涉及这些表锁的死锁。另外,因为在这种情况下 MySQL 高层次上不知道行级锁,所以可能会对当前有行级锁的会话获取表锁。但是,这不会危害事务一致性,如第17.7.5.2节,“死锁检测”中讨论的那样。 -
LOCK TABLES
对每个表获取两个锁,如果innodb_ table_locks=1
(默认)。此外,它还获取一个InnoDB
表锁。如果想避免获取InnoDB
表锁,可以将innodb_ table_locks=0
设置。如果没有获取InnoDB
表锁,LOCK TABLES
将完成,即使一些记录正在被其他事务锁定。在 MySQL 8.4 中,
innodb_ table_locks=0
对于使用LOCK TABLES ... WRITE
显式锁定的表无效。它对隐式锁定表(例如,通过触发器)或LOCK TABLES ... READ
有效。 -
所有
InnoDB
锁由事务持有,直到事务提交或回滚。因此,在autocommit=1
模式下,不建议在InnoDB
表上调用LOCK TABLES
,因为获得的InnoDB
表锁将立即释放。 -
在事务中不能锁定额外表,因为
LOCK TABLES
执行隐式COMMIT
和UNLOCK TABLES
。