文档首页
MySQL 9.0 参考手册
相关文档 下载此手册
PDF (US Ltr) - 40.0Mb
PDF (A4) - 40.1Mb
Man Pages (TGZ) - 258.2Kb
Man Pages (Zip) - 365.3Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb


MySQL 9.0 参考手册  /  ...  /  InnoDB 中不同 SQL 语句设置的锁

17.7.3 InnoDB 中不同 SQL 语句设置的锁

一个 锁定读取、一个 UPDATE 或一个 DELETE 通常会对 SQL 语句处理过程中扫描的每个索引记录设置记录锁。无论语句中是否存在排除行的 WHERE 条件都无关紧要。 InnoDB 不记得确切的 WHERE 条件,只知道扫描了哪些索引范围。锁通常是 下一个键锁,它也会阻止插入到记录之前的 间隙 中。但是,可以显式地禁用 间隙锁定,这会导致不使用下一个键锁定。有关更多信息,请参见 第 17.7.1 节,“InnoDB 锁定”。事务隔离级别也会影响设置的锁;请参见 第 17.7.2.1 节,“事务隔离级别”.

如果在搜索中使用了二级索引,并且要设置的索引记录锁是排他的,InnoDB 还会检索相应的聚簇索引记录并在其上设置锁。

如果您没有适合您语句的索引,并且 MySQL 必须扫描整个表来处理语句,则表的每一行都会被锁定,这反过来会阻止其他用户向表中插入数据。创建良好的索引非常重要,这样您的查询就不会扫描比必要更多的行。

InnoDB 按如下方式设置特定类型的锁。

  • SELECT ... FROM 是一个一致性读取,读取数据库的快照,并且不会设置任何锁,除非事务隔离级别设置为 SERIALIZABLE。对于 SERIALIZABLE 级别,搜索会在其遇到的索引记录上设置共享下一个键锁。但是,对于使用唯一索引搜索唯一行的语句,只需要索引记录锁。

  • SELECT ... FOR UPDATESELECT ... FOR SHARE 语句使用唯一索引获取扫描行的锁,并释放不符合结果集包含条件的行的锁(例如,如果它们不满足 WHERE 子句中给出的条件)。但是,在某些情况下,行可能不会立即被解锁,因为结果行与其原始来源之间的关系在查询执行过程中会丢失。例如,在 UNION 中,扫描(并锁定)的表行可能会插入到临时表中,然后再评估它们是否符合结果集。在这种情况下,临时表中的行与原始表中的行之间的关系会丢失,后者的行在查询执行结束之前不会被解锁。

  • 对于 锁定读取 (SELECT 使用 FOR UPDATEFOR SHARE)、UPDATEDELETE 语句,获取的锁取决于语句是否使用具有唯一搜索条件的唯一索引或范围类型搜索条件。

    • 对于具有唯一搜索条件的唯一索引,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 之间的间隙,但不会互相阻塞,因为行不冲突。

    如果发生重复键错误,则会在重复的索引记录上设置共享锁。如果有多个会话尝试插入同一行,而另一个会话已经拥有排他锁,则这种共享锁的使用会导致死锁。如果另一个会话删除了该行,则可能会发生这种情况。假设 InnoDBt1 具有以下结构

    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 不同,当发生重复键错误时,会在要更新的行上放置排他锁而不是共享锁。会为重复的主键值获取一个排他索引记录锁。会为重复的唯一键值获取一个排他下一个键锁。

  • REPLACE 如果在唯一键上没有冲突,则会像 INSERT 一样进行。否则,会在要替换的行上放置一个排他下一个键锁。

  • INSERT INTO T SELECT ... FROM S WHERE ... 会在插入到 T 中的每一行上设置一个排他索引记录锁(没有间隙锁)。如果事务隔离级别为 READ COMMITTEDInnoDB 会将 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 = 0InnoDB 会知道表级锁,并且 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 会执行隐式 COMMITUNLOCK TABLES