为什么 InnoDB 在二级索引的情况下会阻塞更多记录?

Why does InnoDB block more records in case of a secondary index?

我正在使用 MySQL InnoDB tables 并试图了解在索引范围扫描的情况下某些行级锁定的原因。我发现根据所用索引的唯一性,可能会锁定一条额外的索引记录(超出范围)。请参见下面的示例(在版本 8.0.18 中验证)。

CREATE TABLE foo (
  a INT NOT NULL,
  b INT NOT NULL,
  c CHAR(1) NOT NULL,
  PRIMARY KEY (a),
  UNIQUE KEY (b)
) ENGINE=InnoDB;

INSERT INTO foo VALUES (1,1,'A'), (3,3,'B'), (5,5,'C'), (7,7,'D'), (9,9,'E');

测试用例 1

第 1 节课:

START TRANSACTION;
SELECT * FROM foo WHERE a < 2 FOR UPDATE;

第 2 节:

DELETE FROM foo WHERE a = 3;  -- Success

测试用例 2

这使用 table 的原始行并返回删除的记录。

第 1 节课:

START TRANSACTION;
SELECT * FROM foo WHERE b < 2 FOR UPDATE;

第 2 节:

DELETE FROM foo WHERE b = 3;  -- Blocks

在第二个测试用例中用 b = 3 锁定二级索引记录看起来没有必要。

为什么在二级索引的情况下,InnoDB 会阻塞扫描范围右侧的下一个索引条目?这有什么实际原因吗? 有人可以举例说明如果 b = 3 的记录在第二个测试用例中未被阻止时可能发生的问题吗?

就像@Barmar 已经提到的那样,因为 MySQL 正在设置 GAP 或 Next-key 锁。我假设您使用的是默认的 innodb 隔离级别 REPEATABLE READ

MySQL 文档说:

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition.

  • For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it.
  • For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions by other sessions into the gaps covered by the range. For information about gap locks and next-key locks, see Section 14.7.1, “InnoDB Locking”.

参见:https://dev.mysql.com/doc/refman/5.6/en/innodb-transaction-isolation-levels.html#isolevel_repeatable-read

目前我正在寻找有关如何设置间隙锁的信息,但我也得到了这个信息,我想这是有用的信息:

请记住,锁是基于内部索引的,因为您在条件中使用的列不是唯一索引的。

A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record

Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.

Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED or enable the innodb_locks_unsafe_for_binlog system variable (which is now deprecated). Under these circumstances, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.

There are also other effects of using the READ COMMITTED isolation level or enabling innodb_locks_unsafe_for_binlog. Record locks for nonmatching rows are released after MySQL has evaluated the WHERE condition. For UPDATE statements, InnoDB does a “semi-consistent” read, such that it returns the latest committed version to MySQL so that MySQL can determine whether the row matches the WHERE condition of the UPDATE.

参见 https://dev.mysql.com/doc/refman/5.6/en/innodb-locking.html#innodb-gap-locks

终于找到答案了。简而言之,在第二个测试用例中没有这样额外锁定的重要原因。当执行二级索引范围的锁定读取时,会设置足够的锁,但不是必要的最小值。因此,设置额外的锁只是因为一些 InnoDB 程序员更容易编写代码。如果大部分情况下一切正常,谁会关心额外的锁?

我发布了关于此问题的错误报告:https://bugs.mysql.com/bug.php?id=98639 不幸的是,他们的员工不想注册这个错误。他不理解我的论点并提出错误的解释。他将我最后的论点保密并停止回应。

我也在官方论坛上问过这个问题,得到的答复如下:https://forums.mysql.com/read.php?22,684356,684482 简而言之,修复此错误需要付出巨大努力。但由于这是一个非常小且无关紧要的错误(更准确地说,是一个性能问题),他们还不想修复它。但是在8.0.18版本中,他们修复了聚集索引的类似问题,他们花了一个多月的时间。

我很惊讶优化这么简单的单级扫描算法要花这么多时间,对 MySQL 团队来说这么难。