Bug #120804 Unexpected deadlock under RR/SER when updating a row already locked by the same transaction
Submitted: 28 Jun 13:41
Reporter: yiran shen Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server: InnoDB storage engine Severity:S1 (Critical)
Version:mysql 8.4.9 OS:Ubuntu (20.04)
Assigned to: CPU Architecture:x86
Tags: transactions

[28 Jun 13:41] yiran shen
Description:
Description:

I found a possible InnoDB deadlock-detection issue in MySQL 8.4.9.

In the following two-transaction test case, Transaction 1 updates the only row in the table and keeps the transaction open. Then Transaction 2 executes:

SELECT * FROM t0 WHERE TRUE FOR UPDATE;

This statement is blocked because Transaction 1 still holds the lock on the row.
After that, Transaction 1 executes another UPDATE on the same row:

UPDATE t0 SET c0=1 WHERE TRUE;

Under READ COMMITTED, this statement can execute successfully. However, under REPEATABLE READ and SERIALIZABLE, a deadlock is reported when this second UPDATE is executed.

I think this behavior is suspicious. The table initially contains only one row, (9). Transaction 1 updates this row first and keeps holding the row lock. Transaction 2 starts later and is only waiting for the lock; it should not have acquired the row lock. Therefore, Transaction 1 should be able to continue updating the row it already locked. I would not expect a deadlock to be reported in this schedule.

This issue is reproducible under REPEATABLE READ and SERIALIZABLE, but not under READ COMMITTED.

How to repeat:
How to repeat:
Tested on MySQL 8.4.9 with InnoDB.

Create and initialize the table:

DROP TABLE IF EXISTS t0;
CREATE TABLE t0(
c0 INT PRIMARY KEY
);
INSERT INTO t0(c0) VALUES (9);

Use two sessions.

For the READ COMMITTED test, set both sessions to:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

For the REPEATABLE READ test, set both sessions to:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

For the SERIALIZABLE test, set both sessions to:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Transaction 1:

[1-0] BEGIN;
[1-1] UPDATE t0 SET c0=4 WHERE TRUE;
[1-2] UPDATE t0 SET c0=1 WHERE TRUE;
[1-3] COMMIT;

Transaction 2:

[2-0] BEGIN;
[2-1] SELECT * FROM t0 WHERE TRUE FOR UPDATE;
[2-2] COMMIT;

Execute the statements in the following order:

[1-0] BEGIN;
[1-1] UPDATE t0 SET c0=4 WHERE TRUE;
[2-0] BEGIN;
[2-1] SELECT * FROM t0 WHERE TRUE FOR UPDATE;
-- blocked
[1-2] UPDATE t0 SET c0=1 WHERE TRUE;
-- deadlock under REPEATABLE READ / SERIALIZABLE
[1-3] COMMIT;
[2-2] COMMIT;

Actual result:
Under READ COMMITTED, [1-2] can execute successfully.

Under REPEATABLE READ and SERIALIZABLE, a deadlock is reported when [1-2] is executed:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

Expected result:
I expected [1-2] to execute successfully under REPEATABLE READ and SERIALIZABLE as well.

Transaction 1 is the transaction that already updated and locked the only row. Transaction 2 only starts later and waits on SELECT ... FOR UPDATE. Since Transaction 2 has not acquired the row lock, its waiting lock request should not prevent Transaction 1 from updating its own locked row again.

A more reasonable behavior would be:

1. Transaction 1 executes [1-2] successfully;
2. Transaction 2 remains blocked until Transaction 1 commits;
3. after Transaction 1 commits, Transaction 2 resumes or returns an appropriate error if the row-change check requires it.

Returning a deadlock here seems unexpected because there does not appear to be a real circular dependency: Transaction 2 is waiting for Transaction 1, while Transaction 1 should already own the relevant row lock.