Ilya Zinkovich
Ilya Zinkovich

Reputation: 4410

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

I'm implementing a custom table-based sequence generator for MySQL database v5.7.16 with InnoDB engine.
The sequence_table looks as follows:

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

sequence_name column is a primary key.
This sequence table contains multiple sequences for different consumers.

I use the following strategy for the sequence updates:

  1. Select current sequence value: select next_val from sequence_table where sequence_name=?.
  2. Add the allocation size to current sequence value.
  3. Update the sequence value if it's current value matches the value selected in the first step: update sequence_table set next_val=? where sequence_name=? and next_val=?.
  4. If the update is successful return the increased sequence value, otherwise repeat the process from step 1.

The documentation contains the following information:

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

The part in bold is a bit confusing.
As you can see, I match the primary key in the WHERE clause of the UPDATE statement.

Is it possible that the search may encounter more than one record and therefore lock multiple rows in this sequence table?

In other words, will the update in the 3rd step of the algorithm block just one or multiple rows?

Upvotes: 3

Views: 2359

Answers (2)

Rick James
Rick James

Reputation: 142208

  • Don't grab the sequence numbers inside the main transaction; do it before the START TRANSCTION.
  • Do the task in a single statement with autocommit=ON.

Both of those lead to it being much faster, less likely to block.

(You code was missing BEGIN/COMMIT and FOR UPDATE. I got rid of those rather than explaining the issues.)

Set up test:

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)

Grab 20 nums from 'first' and fetch the starting number:

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)

Grab another 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)

Upvotes: 0

Oleg Kuralenko
Oleg Kuralenko

Reputation: 11543

You didn't mention what transaction isolation level you're planning to use. Lets assume you're using repeatable read (in read committed no such a problem should exist)

From 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

and

For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it

So at least in theory it should lock only a single record and no next-key lock will be used.

More quotes from other docs pages to back my thoughts:

innodb-next-key-locks

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.

gap locks

link

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

Upvotes: 2

Related Questions