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中,扫描(并锁定)表中的行可能会在评估它们是否符合结果集的条件之前被插入到临时表中。在这种情况下,临时表中的行与原始表中的行之间的关系丢失,直到查询执行结束时才解锁这些行。 -
对于 读取锁定 (
SELECTwithFOR 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。