为什么 MySQL InnoDB 在重复索引记录发生重复键错误时设置 S 或 X Next-Key 锁?

Why MySQL InnoDB set an S or X Next-Key lock on the duplicate index record when a duplicate-key error occurs?

MySQL 文档 (https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html) 提到,

If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock. ...

...

INSERT ... ON DUPLICATE KEY UPDATE differs from a simple INSERT in that an exclusive lock rather than a shared lock is placed on the row to be updated when a duplicate-key error occurs.

我看过源码(https://github.com/mysql/mysql-server/blob/f8cdce86448a211511e8a039c62580ae16cb96f5/storage/innobase/row/row0ins.cc#L1930)对应这种情况,InnoDB确实在出现duplicate-key错误时设置了S或X锁

if (flags & BTR_NO_LOCKING_FLAG) {
    /* Set no locks when applying log
    in online table rebuild. */
} else if (allow_duplicates) {
... ...
      
    /* If the SQL-query will update or replace duplicate key we will take
     X-lock for duplicates ( REPLACE, LOAD DATAFILE REPLACE, INSERT ON
     DUPLICATE KEY UPDATE). */
    err = row_ins_set_rec_lock(LOCK_X, lock_type, block, rec, index, offsets, thr);
 } else {
... ...
    err = row_ins_set_rec_lock(LOCK_S, lock_type, block, rec, index, offsets, thr);
}

但是我想知道为什么InnoDB要设置这样的锁,看来这些锁带来的问题比他们解决的要多(他们解决了这个问题:MySQL duplicate key error causes a shared lock set on the duplicate index record?)。

首先,它很容易导致死锁,同样的MySQL文档给出了2个关于死锁的例子。

更糟糕的是,S 或 X 锁不是单个索引记录锁,它是 Next Key 锁,并且可能拒绝插入多个值而不是仅插入一个重复值。

例如

CREATE TABLE `t` (
  `id` int NOT NULL AUTO_INCREMENT,
  `c` int DEFAULT NULL,
  `d` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_idx_c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4

mysql> select * from t;
+----+------+------+
| id | c    | d    |
+----+------+------+
| 30 |   10 |   10 |
| 36 |  100 |  100 |
+----+------+------+

mysql> show variables like '%iso%';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.41 sec)

# Transaction 1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t values (null, 100, 100);
ERROR 1062 (23000): Duplicate entry '100' for key 't.uniq_idx_c'

# not commit
# Transcation 2
mysql> insert into t values(null, 95, 95);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(null, 20, 20);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(null, 50, 50);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

# All c in [10, 100] can not be inserted 

A​​CID 数据库的目标是,如果您再次尝试 运行,会话中的查询将获得相同的结果。

示例:您 运行 一个导致重复键错误的 INSERT 查询。如果您重试该 INSERT 查询,您会期望它再次失败并出现相同的错误。

但是,如果另一个会话更新了导致冲突的行并更改了唯一值怎么办?然后,如果您重试 INSERT,它会成功,这出乎意料。

InnoDB 无法在您的语句锁定时实现真正的 REPEATABLE-READ 事务。例如。 INSERT/UPDATE/DELETE,甚至 SELECT,带有锁定选项 FOR UPDATE、FOR SHARE 或 LOCK IN SHARE MODE。 InnoDB 中的锁定 SQL 语句始终作用于行的最新提交版本,而不是会话可见的该行的版本。

那么InnoDB如何模拟REPEATABLE-READ,确保受锁定语句影响的行与最新提交的行相同?

通过锁定您的锁定语句间接引用的行,防止它们被其他并发会话更改。

我在 MySQL 源代码中找到的另一个可能的解释是 row0ins.cc Line 2141

We set a lock on the possible duplicate: this is needed in logical logging of MySQL to make sure that in roll-forward we get the same duplicate errors as in original execution