文档主页
MySQL 8.4 参考手册
相关文档 下载此手册
PDF (US Ltr) - 39.9Mb
PDF (A4) - 40.0Mb
手册页 (TGZ) - 258.5Kb
手册页 (Zip) - 365.5Kb
信息 (Gzip) - 4.0Mb
信息 (Zip) - 4.0Mb


MySQL 8.4 参考手册  /  ...  /  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 = 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 会执行隐式的 COMMITUNLOCK TABLES