`UPDATE ... WHERE ... ` InnoDB 中的多行锁定

`UPDATE ... WHERE ... ` multiple rows locking in InnoDB

我正在使用 InnoDB 引擎为 MySQL 数据库 v5.7.16 实现基于 table 的自定义序列生成器。
sequence_table 如下所示:

+-------------+-----------+
|sequence_name|next_value |
+-------------+-----------+
|  first_seq  |     1     |
+-------------+-----------+
|  second_seq |     1     |
+-------------+-----------+

sequence_name 列是主键。
此序列 table 包含针对不同消费者的多个序列。

我使用以下策略进行序列更新:

  1. Select 当前序列值:select next_val from sequence_table where sequence_name=?
  2. 将分配大小添加到当前序列值。
  3. 如果当前值与第一步中选择的值匹配,则更新序列值:update sequence_table set next_val=? where sequence_name=? and next_val=?
  4. 如果更新成功return增加的序列值,否则从步骤1开始重复该过程。

文档包含以下信息:

UPDATE ... WHERE ... sets an exclusive next-key lock on every record the search encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row. 14.5.3 Locks Set by Different SQL Statements in InnoDB

粗体部分有点乱。
如您所见,我在 UPDATE 语句的 WHERE 子句中匹配主键。

搜索是否可能遇到多个记录并因此锁定此序列中的多行table?

换句话说,算法第3步的更新是只阻塞一行还是多行?

您没有提到您打算使用什么事务隔离级别。 假设您正在使用 repeatable read(在 read committed 中不应该存在这样的问题)

来自here

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

所以至少在理论上它应该只锁定一条记录并且不会使用 next-key 锁。

更多来自其他文档页面的引用来支持我的想法:

innodb-next-key-锁

link

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

间隙锁

link

Gap locking is not needed for statements that lock rows using a unique index to search for a unique row

  • 不要在主交易中抢序号;在 START TRANSCTION.
  • 之前做
  • 使用 autocommit=ON 在一条语句中完成任务。

两者都使它更快,更不容易阻塞。

(您的代码缺少 BEGIN/COMMITFOR UPDATE。我删除了这些而不是解释问题。)

设置测试:

mysql> CREATE TABLE so49197964 (
    ->     name VARCHAR(22) NOT NULL,
    ->     next_value INT UNSIGNED NOT NULL,
    ->     PRIMARY KEY (name)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO so49197964 (name, next_value)
    ->     VALUES
    ->     ('first', 1), ('second', 1);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM so49197964;
+--------+------------+
| name   | next_value |
+--------+------------+
| first  |          1 |
| second |          1 |
+--------+------------+
2 rows in set (0.00 sec)

从'first'中抓取20个数字并获取起始数字:

mysql> UPDATE so49197964
    ->     SET next_value = LAST_INSERT_ID(next_value) + 20
    ->     WHERE name = 'first';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM so49197964;
+--------+------------+
| name   | next_value |
+--------+------------+
| first  |         21 |
| second |          1 |
+--------+------------+
2 rows in set (0.00 sec)

再拿20个:

mysql> UPDATE so49197964
    ->     SET next_value = LAST_INSERT_ID(next_value) + 20
    ->     WHERE name = 'first';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|               21 |
+------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM so49197964;
+--------+------------+
| name   | next_value |
+--------+------------+
| first  |         41 |
| second |          1 |
+--------+------------+
2 rows in set (0.00 sec)