MySQL: A deadlock occurred when deleting the same row - mysql

Recently I encountered a deadlock when deleting records (Note that the isolation level is REPEATABLE READ, MySQL 5.7)
Here is the repro steps
1 Create a new table
CREATE TABLE `t` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
KEY `p_name` (`name`)
) ENGINE=InnoDB CHARSET=utf8;
2 Prepare 3 records
insert into t (name) value ('A'), ('C'), ('D');
3
+====================================+============================================================+
| Session A | Session B |
+====================================+============================================================+
| begin; | |
+------------------------------------+------------------------------------------------------------+
| | begin; |
+------------------------------------+------------------------------------------------------------+
| delete from t where name = 'C'; | |
+------------------------------------+------------------------------------------------------------+
| | delete from t where name = 'C'; --Blocked! |
+------------------------------------+------------------------------------------------------------+
| insert into t (name) values ('B'); | |
+------------------------------------+------------------------------------------------------------+
| | ERROR 1213 (40001): Deadlock found when trying to get lock |
+------------------------------------+------------------------------------------------------------+
The result of show engine innodb status as below shown (LATEST DETECTED DEADLOCK section)
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 3631, ACTIVE 21 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 13, OS thread handle 123145439432704, query id 306 localhost root updating
delete from t where name = 'C'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3631 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 1; hex 43; asc C;;
1: len 8; hex 8000000000000018; asc ;;
*** (2) TRANSACTION:
TRANSACTION 3630, ACTIVE 29 sec inserting
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
MySQL thread id 14, OS thread handle 123145439711232, query id 307 localhost root update
insert into t (name) values ('B')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 1; hex 43; asc C;;
1: len 8; hex 8000000000000018; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 1; hex 43; asc C;;
1: len 8; hex 8000000000000018; asc ;;
As the Innodb status shown, session B is waiting next-key lock C, and session A hold a record lock C and waiting gap lock on C;
As we all know that
DELETE FROM ... WHERE ... sets an exclusive next-key lock on every record the search encounters
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.
Q1: I guess if session B firstly got the gap lock (part of next-key), and then waiting for the record lock. Thereby, the latter insert in session A was blocked by session B (due to the gap lock), and eventually result in a dead lock. Right?
Q2: As the C is purged from an index, does the gap lock hold by session B should be ('A', 'D')? If so, why the session A is waiting the insert intension lock on range (, 'C')?
Q3: Why session B has 1 row lock(s), and session A has 4 row lock(s)?
Q4: When change index p_name to a unique index, we still get the deadlock due to gap lock, it's weird. It behaves different from official doc which states only record lock is required.
DELETE FROM ... 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.
However, it is okay when using primary key id to execute delete (steps as below shown). Is this a bug in MySQL?
1 Prepare data
delete from t;
insert into t (id, name) value (1, 'A'), (3, 'C'), (5, 'D');
2
+-------------------------------------------+--------------------------------------+
| Session A | Session B |
+-------------------------------------------+--------------------------------------+
| begin; | |
| | begin; |
| delete from t where id = 3; | |
| | delete from t where id = 3; Blocked! |
| insert into t (id, name) values (2, 'B'); | |
| | |
| commit; | |
+-------------------------------------------+--------------------------------------+

From "WAITING FOR THIS LOCK TO BE GRANTED" part of transaction 3631, we could see:
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3631 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
3631 is waiting a record lock. The corresponding index content is {"name":"C", "id": 24}.
The index name is p_name in table t.
lock's mode is "lock_mode X"
From "WAITING FOR THIS LOCK TO BE GRANTED" part of transaction 3630, we could see:
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 1; hex 43; asc C;;
1: len 8; hex 8000000000000018; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 1; hex 43; asc C;;
1: len 8; hex 8000000000000018; asc ;;
3630 is waiting a record lock. The corresponding index content is {"name":"C", "id": 24}. waiting lock's mode is "lock_mode X locks gap"
3630 is holding a record lock. The corresponding index content is {"name":"C", "id": 24}. Holding lock's mode is "lock_mode X locks"
The index name is p_name in table t.
This deadlock is caused by executing "insert into t (name) values ('B')"
According to your reproduce step, session A will send a delete from t where name = 'C'; first, this will lock:
('A', 'C'] and ('C', 'D'): next-key lock 'C' and gap lock before 'D';
DELETE FROM ... 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.
add a record lock for 'C' corresponding primary index id. here id value should be "26".
Then session B will start and delete from t where name = 'C'; will be executed again. However. For session B, because session A hadn't been committed, 'C' had been locked by session A. However, if execute a delete sql, session B will try to add lock in the following sequence:
gap lock before 'C': Success, because innodb could add multi gap lock in the same position.
record lock 'C': Blocked, because session A have holded that lock. session B have to wait it released by session A.
gap lock before 'D':
At last, session A send insert into t (name) values ('B');. For table t, there are 2 indexes, which are id and name. id is a auto increasement primary integer key, and for name, this sql will try to add a insert intention lock. However, there have been a gap lock which is holded by session B, therefor session A have to wait session B for releasing that gap lock. Now we could see how this dead lock occur. Innodb will choose a session to rollback base on cost. Here session B will be rolled back.
For Q1, the anwser is yes.
For Q2, actually, the deleted record wouldn't be purged from an index before its session commit.
For Q3, row lock number is equal to trx_rows_locked, and in mysql website, its:
TRX_ROWS_LOCKED
The approximate number or rows locked by this transaction. The value
might include delete-marked rows that are physically present but not
visible to the transaction.
From this article, we could know:
For non-clustered unique index filtering, due to the need to return tables, the number of filtered rows is locked as the unique index plus
the number of returned rows.
For non-clustered non-unique index filtering, the gap lock is involved, so more records are locked.
So, trx_rows_locked (gap lock + next-key lock + return table) is 3 after delete in session A. final trx_rows_locked value should be 3 + 1 (insert key lock) after trying to insert.
The following are for the new update questions:
I didn't notice delete primary key and unique secondary key before.
After some investigating, I found:
When deleting a primary key, which have been deleted and not commited yet, the new delete operation will only require record lock instead of next-key lock.
When deleting a secondary unique key, which have been deleted and not commited yet, the new delete operation will require next-key lock.
You could use set GLOBAL innodb_status_output_locks=ON; show engine innodb status to see the detail lock status for running trasactions.

For Q4, I finally found a comment in MySQL 5.7 source code that explain why using next-key locks, just for reference.
In a search where at most one record in the index may match, we
can use a LOCK_REC_NOT_GAP type record lock when locking a
non-delete-marked matching record.
Note that in a unique secondary index there may be different
delete-marked versions of a record where only the primary key
values differ: thus in a secondary index we must use next-key
locks when locking delete-marked records
Note above that a UNIQUE secondary index can contain many
rows with the same key value if one of the columns is the SQL
null. A clustered index under MySQL can never contain null
columns because we demand that all the columns in primary key
are non-null.

Related

MySQL Deadlock: Transaction holds and waits for the same lock [duplicate]

I have very simple table:
CREATE TABLE `d` (
`id` int(11) DEFAULT NULL,
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
without records:
select * from d;
Empty set (0,01 sec)
Then I try to open two transactions in different sessions:
Session #1:
begin;
Query OK, 0 rows affected (0,00 sec)
select * from d where id = 100 for update;
Empty set (0,00 sec)
Session #2:
begin;
Query OK, 0 rows affected (0,00 sec)
select * from d where id = 700 for update;
Empty set (0,00 sec)
Now I try to insert new record in Session #2 and session "freezes":
insert into d values (700);
And when I try to do the same (with another id field) in Session #1 it crashes:
insert into d values (100); --> ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction in Session #1
insert into d values (700); --> Query OK, 1 row affected (4,08 sec) in Session #2
How can I to fix the deadlock? InnoDB status is:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2017-07-06 15:59:25 0x70000350d000
*** (1) TRANSACTION:
TRANSACTION 43567, ACTIVE 15 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 4, OS thread handle 123145358217216, query id 89 localhost root update
insert into d values (700)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 126 page no 4 n bits 72 index id of table `trx`.`d` trx id 43567 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 43568, ACTIVE 7 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 3, OS thread handle 123145357938688, query id 90 localhost root update
insert into d values (100)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 126 page no 4 n bits 72 index id of table `trx`.`d` trx id 43568 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 126 page no 4 n bits 72 index id of table `trx`.`d` trx id 43568 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
This deadlock error is a bug in the MySQL InnoDB engine that has not been fixed for 12 years. (Bug #25847: https://bugs.mysql.com/bug.php?id=25847, workaround: How do I lock on an InnoDB row that doesn't exist yet?) It is not related to Unique Key. Running this query will also result in the same Deadlock error.
Session #1: CREATE TABLE t (id int) ENGINE=InnoDB;
Session #1: SET AUTOCOMMIT = 0;
Session #1: SELECT id FROM t WHERE id = 1 FOR UPDATE;
Session #2: SET AUTOCOMMIT = 0;
Session #2: SELECT id FROM t WHERE id = 2 FOR UPDATE;
Session #1: INSERT INTO t (id) VALUES (1); -- Hang
Session #2: INSERT INTO t (id) VALUES (2); -- Session #1: OK, Session #2: Deadlock found when trying to get lock; try restarting transaction
InnoDB Status is the same:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-10-24 00:25:31 0x1638
*** (1) TRANSACTION:
TRANSACTION 1287, ACTIVE 62 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 7, OS thread handle 9444, query id 143 localhost ::1 root update
INSERT INTO t (id) VALUES (1)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t` trx id 1287 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 1288, ACTIVE 19 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 9, OS thread handle 5688, query id 145 localhost ::1 root update
INSERT INTO t (id) VALUES (2)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t` trx id 1288 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t` trx id 1288 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
Note that starting with MySQL 8.0.1,
the performance schema exposes innodb data locks.
See https://dev.mysql.com/doc/refman/8.0/en/data-locks-table.html
See https://dev.mysql.com/doc/refman/8.0/en/data-lock-waits-table.html
In this example, after the first select in session 1 alone the locks are:
mysql> select * from performance_schema.data_locks \G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 1808:76
ENGINE_TRANSACTION_ID: 1808
THREAD_ID: 35
EVENT_ID: 13081
OBJECT_SCHEMA: test
OBJECT_NAME: d
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139756088373592
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 1808:2:5:1
ENGINE_TRANSACTION_ID: 1808
THREAD_ID: 35
EVENT_ID: 13111
OBJECT_SCHEMA: test
OBJECT_NAME: d
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: id
OBJECT_INSTANCE_BEGIN: 139756088370552
LOCK_TYPE: RECORD
LOCK_MODE: X
LOCK_STATUS: GRANTED
LOCK_DATA: supremum pseudo-record <--- HERE
2 rows in set (0.00 sec)
This is not a solution to the deadlock itself, but having visibility on the locks taken goes a long way to understand the problem.
Here, both SELECT FOR UPDATE lock the "suprenum" record, because id 100 and 700 are greater than the biggest ID in the table (it's empty).
Once more records are present (say at ID = 500), both queries should execute concurrently, as a different gap in IDs will be locked.
I suspect the deadlock happens because InnoDB is conservative on dealing with "gaps". Note that 100 and 700 are both in the same nebulous area of untouched land. InnoDB can't (or at least does not) deal with the fact that "100" and "700" are different. InnoDB would like to tag individual rows but there are no rows already in the table with those ids.
Transaction 2 was probably going to timeout (see innodb_lock_wait_timeout). When you poked Transaction 1 a second time with #2 still wanting a lock, InnoDB punted and gave up.
Bottom Line: Live with deadlocks. When they happen, start back at the BEGIN. You have found yet-another obscure case where an unnecessary deadlock happens.
Furthermore, I suspect that fixing the code for this case would slow down most other cases, and lead to a series of errors that would take several releases to discover and fix.

Why concurrent "Delete...Insert" statements cause a deadlock?

Consider the following schema in mysql:
create table foo(
id int not null primary key auto_increment,
name varchar(32) not null,
unique key(name)
);
And there is a record with name "abc" in the table.
I have a transaction (RC):
start transaction;
delete from foo where name = "abc";
insert into foo(name) values("abc");
commit;
If there are two concurrent transactions, the dead lock will happen.
| TX A | TX B
---------------------------------------------------------------------
Step 1 | start transaction; |
| delete name="abc"; |
---------------------------------------------------------------------
Step 2 | | start transaction;
| | delete name="abc";
| | <wait for lock>
---------------------------------------------------------------------
Step 3 | insert name="abc"; | <deadlock detected, exit>
---------------------------------------------------------------------
Step 4 | commit; |
---------------------------------------------------------------------
I'm wondering why this sequence causes the deadlock.
In the mysql doc says (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. This can occur if another session
deletes the row.
I suppose when transaction A runs the "delete" statement, it has acquired the X lock of the record "abc". When the "insert" statement executes, it tries to acquire the S lock due to the "duplicate key error". Shouldn't it get the S lock since it has got the X lock of the same record? Why deadlock happens here?
I reproduced the deadlock, and got the innoDB status log as follow:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-10-18 18:35:14 0x7f1dfc738700
*** (1) TRANSACTION:
TRANSACTION 26547965, ACTIVE 6 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
/* ApplicationName=DataGrip 2019.1.1 */ delete from foo where name='abc'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3011 page no 4 n bits 224 index IDX_NAME of table `foo` trx id 26547965 lock_mode X locks rec but not gap waiting
Record lock, heap no 153 PHYSICAL RECORD: n_fields 2; ....
*** (2) TRANSACTION:
TRANSACTION 26547960, ACTIVE 10 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 2
/* ApplicationName=DataGrip 2019.1.1 */ INSERT INTO foo(id, name)
VALUES (1, 'abc')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3011 page no 4 n bits 224 index IDX_NAME of table `foo` trx id 26547960 lock_mode X locks rec but not gap
Record lock, heap no 153 PHYSICAL RECORD: ...
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3011 page no 4 n bits 224 index IDX_NAME of table `foo` trx id 26547960 lock mode S waiting
Record lock, heap no 153 PHYSICAL RECORD: ....
*** WE ROLL BACK TRANSACTION (1)
The log explains the reason clearly, TX B waiting X lock held by TX A, at the same time, TX A waiting S lock which is block by TX B's lock request.
According to Mysql doc:
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 statement does acquire S lock at some point, so the reason for why deadlock happens is very clear.
But problem is:
according to the mysql doc, insert statement will acquire a S lock if a duplicate-key error occurs, which is not happened in current case we discuss
why insert statement still acquire a S lock when current transaction already holds the X lock, X lock is enough for doing a current read to check the duplicate key error. so what does it use for ?

MySQL Deadlock using an index with a new value

Table:
create table properties
(
id int auto_increment primary key,
other_id int null
);
create index index_properties_on_other_id
on properties (other_id);
TX 1:
start transaction;
SET #last_id = 1;
delete from `properties` WHERE `properties`.`other_id` = #last_id;
INSERT INTO `properties` (`other_id`) VALUES (#last_id);
commit
TX 2:
start transaction;
SET #last_id = 2;
delete from `properties` WHERE `properties`.`other_id` = #last_id;
INSERT INTO `properties` (`other_id`) VALUES (#last_id);
commit
Assume table is empty prior to running transactions.
My application has 2 use cases. Sometimes last_id would already be used by another row, hence it would be indexed prior; but sometimes it would be generated in same transaction by a previous insert query, and in that case I get a deadlock.
I need to run both transactions until after delete statement. And when I run insert on tx1, it waits to get a lock, then I run insert on tx2, tx2 gets a deadlock and rollsback.
mysql | LATEST DETECTED DEADLOCK
mysql | ------------------------
mysql | 2019-06-03 21:01:05 0x7f0ba4052700
mysql | *** (1) TRANSACTION:
mysql | TRANSACTION 320051, ACTIVE 12 sec inserting
mysql | mysql tables in use 1, locked 1
mysql | LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql | MySQL thread id 286, OS thread handle 139687839577856, query id 17804 172.18.0.1 root update
mysql | INSERT INTO `properties` (`other_id`) VALUES (#last_id)
mysql | *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
mysql | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320051 lock_mode X insert intention waiting
mysql | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql | 0: len 8; hex 73757072656d756d; asc supremum;;
mysql |
mysql | *** (2) TRANSACTION:
mysql | TRANSACTION 320052, ACTIVE 8 sec inserting
mysql | mysql tables in use 1, locked 1
mysql | 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql | MySQL thread id 287, OS thread handle 139687973168896, query id 17814 172.18.0.1 root update
mysql | INSERT INTO `properties` (`other_id`) VALUES (#last_id)
mysql | *** (2) HOLDS THE LOCK(S):
mysql | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320052 lock_mode X
mysql | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql | 0: len 8; hex 73757072656d756d; asc supremum;;
mysql |
mysql | *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
mysql | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320052 lock_mode X insert intention waiting
mysql | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql | 0: len 8; hex 73757072656d756d; asc supremum;;
mysql |
mysql | *** WE ROLL BACK TRANSACTION (2)
Status of locks after delete statements:
mysql | ---TRANSACTION 320066, ACTIVE 90 sec
mysql | 2 lock struct(s), heap size 1136, 1 row lock(s)
mysql | MySQL thread id 287, OS thread handle 139687973168896, query id 18076 172.18.0.1 root
mysql | TABLE LOCK table `properties` trx id 320066 lock mode IX
mysql | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320066 lock_mode X
mysql | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql | 0: len 8; hex 73757072656d756d; asc supremum;;
mysql |
mysql | ---TRANSACTION 320065, ACTIVE 95 sec
mysql | 2 lock struct(s), heap size 1136, 1 row lock(s)
mysql | MySQL thread id 286, OS thread handle 139687839577856, query id 18039 172.18.0.1 root
mysql | TABLE LOCK table `properties` trx id 320065 lock mode IX
mysql | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table ``properties` trx id 320065 lock_mode X
mysql | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql | 0: len 8; hex 73757072656d756d; asc supremum;;
So two transactions are deleting/inserting different other_ids, I wasn't expecting them to get into a deadlock. I want to learn why exactly this is happening.
MySQL does not lock something that is not there, e.g. locks rows that you didn't delete. It also does not store that you tried to delete rows with the specific value "1". What it does instead is that it marks the space where the "1" should have been if it would have been there, and locks it with a gap lock, which has the following characteristics:
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.
In an empty table, the place that the 1 would have been is "anywhere in the table" (or anywhere from the start to the "supremum" mentioned in the deadlock) - which is consequently gaplocked by the delete. The same is true for the 2. And those locks do not conflict each other by definition.
But the insert does. The first insert will have to wait for the gaplock that the second transaction issued for its delete. If the second transaction now tries to also insert into the gap, this would require the gaplock from the first transaction to be lifted, but this cannot happen because the first transaction already waits for the second gaplock to be lifted. So you get a deadlock.
Once you fill your table, this will happen less often, as the gaplocks do not need to span the whole table anymore. If you e.g. already have other_id 1 and 3 in your table, delete/inserting value 2 and 4 will not deadlock each other.
In general, empty tables are a rarity, and you cannot and should not infer anything from such a special case to the normal behaviour. You basically have to accept edge cases:
Gap locks are part of the tradeoff between performance and concurrency
So in the general use case, you just need to be prepared that occasionally a deadlock may happen (and then repeat the transaction). If your expected use case is that you have a basically empty table, or are mostly adding at the end of the values, or oftentimes add 2 values into the same gap, you may need a different solution (and should ask a question on how to proceed in this specific use case). You may e.g. be able to use a unique index (that does not require gap locks), recode/hash your value to sit randomly in the index, or you let all transactions lock something that you know exists, so they wait for each other.

Why do two concurrent delete + insert statements deadlock on an empty table?

I'm curious to know why two concurrent DELETE followed by INSERT statements that use primary keys causes a deadlock in MySQL when the primary keys don't exist. The example is contrived to illustrate the issue in it's simplest form.
Here is the setup.
> SELECT ##GLOBAL.tx_isolation, ##tx_isolation;
+-------------------------+------------------+
| ##GLOBAL.tx_isolation | ##tx_isolation |
|-------------------------+------------------|
| REPEATABLE-READ | REPEATABLE-READ |
+-------------------------+------------------+
1 row in set
Time: 0.002s
> select version();
+-------------+
| version() |
|-------------|
| 5.7.12 |
+-------------+
1 row in set
Time: 0.002s
create table lock_test ( id int(11) not null, primary key (`id`) );
Below, 1> represents one mysql terminal and 2> represents another.
1> begin;
1> delete from lock_test where id = 1;
2> begin;
2> delete from lock_test where id = 2;
1> insert into lock_test values (1); -- hangs
2> insert into lock_test values (2);
*** deadlock ***
Here's the show engine innodb status output:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-06-06 16:15:18 0x70000ba52000
*** (1) TRANSACTION:
TRANSACTION 807765, ACTIVE 46 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 620, OS thread handle 123145496289280, query id 43097 localhost ::1 root update
insert into lock_test values (1)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6819 page no 3 n bits 72 index PRIMARY of table `content_graph`.`lock_test` trx id 807765 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 807766, ACTIVE 37 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 617, OS thread handle 123145497681920, query id 43099 localhost ::1 root update
insert into lock_test values (2)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 6819 page no 3 n bits 72 index PRIMARY of table `content_graph`.`lock_test` trx id 807766 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6819 page no 3 n bits 72 index PRIMARY of table `content_graph`.`lock_test` trx id 807766 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
Note that if you first insert records with ids 1, and 2 then repeat the sequence above there is no deadlock.
My sense is that because the key is not in the index (both are appending) the delete has to lock more of the index (more of the page the primary would land in) but I want to make sure I have my understanding correct.
The "gap" is locked in anticipation that someone may try to insert the row I am trying to delete.
Or, to look at it another way... It would be too slow to perfectly handle every odd case. So, InnoDB chooses to handle most cases efficiently and punt on the rare oddball case.
Bottom line: Live with it. You will get deadlocks. You won't necessarily be able to understand them. But your code needs to recover -- by simply rolling back and going back to the BEGIN.

Solution for Insert Intention Locks in MySQL

I have very simple table:
CREATE TABLE `d` (
`id` int(11) DEFAULT NULL,
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
without records:
select * from d;
Empty set (0,01 sec)
Then I try to open two transactions in different sessions:
Session #1:
begin;
Query OK, 0 rows affected (0,00 sec)
select * from d where id = 100 for update;
Empty set (0,00 sec)
Session #2:
begin;
Query OK, 0 rows affected (0,00 sec)
select * from d where id = 700 for update;
Empty set (0,00 sec)
Now I try to insert new record in Session #2 and session "freezes":
insert into d values (700);
And when I try to do the same (with another id field) in Session #1 it crashes:
insert into d values (100); --> ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction in Session #1
insert into d values (700); --> Query OK, 1 row affected (4,08 sec) in Session #2
How can I to fix the deadlock? InnoDB status is:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2017-07-06 15:59:25 0x70000350d000
*** (1) TRANSACTION:
TRANSACTION 43567, ACTIVE 15 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 4, OS thread handle 123145358217216, query id 89 localhost root update
insert into d values (700)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 126 page no 4 n bits 72 index id of table `trx`.`d` trx id 43567 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 43568, ACTIVE 7 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 3, OS thread handle 123145357938688, query id 90 localhost root update
insert into d values (100)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 126 page no 4 n bits 72 index id of table `trx`.`d` trx id 43568 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 126 page no 4 n bits 72 index id of table `trx`.`d` trx id 43568 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
This deadlock error is a bug in the MySQL InnoDB engine that has not been fixed for 12 years. (Bug #25847: https://bugs.mysql.com/bug.php?id=25847, workaround: How do I lock on an InnoDB row that doesn't exist yet?) It is not related to Unique Key. Running this query will also result in the same Deadlock error.
Session #1: CREATE TABLE t (id int) ENGINE=InnoDB;
Session #1: SET AUTOCOMMIT = 0;
Session #1: SELECT id FROM t WHERE id = 1 FOR UPDATE;
Session #2: SET AUTOCOMMIT = 0;
Session #2: SELECT id FROM t WHERE id = 2 FOR UPDATE;
Session #1: INSERT INTO t (id) VALUES (1); -- Hang
Session #2: INSERT INTO t (id) VALUES (2); -- Session #1: OK, Session #2: Deadlock found when trying to get lock; try restarting transaction
InnoDB Status is the same:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-10-24 00:25:31 0x1638
*** (1) TRANSACTION:
TRANSACTION 1287, ACTIVE 62 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 7, OS thread handle 9444, query id 143 localhost ::1 root update
INSERT INTO t (id) VALUES (1)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t` trx id 1287 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 1288, ACTIVE 19 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 9, OS thread handle 5688, query id 145 localhost ::1 root update
INSERT INTO t (id) VALUES (2)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t` trx id 1288 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t` trx id 1288 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
Note that starting with MySQL 8.0.1,
the performance schema exposes innodb data locks.
See https://dev.mysql.com/doc/refman/8.0/en/data-locks-table.html
See https://dev.mysql.com/doc/refman/8.0/en/data-lock-waits-table.html
In this example, after the first select in session 1 alone the locks are:
mysql> select * from performance_schema.data_locks \G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 1808:76
ENGINE_TRANSACTION_ID: 1808
THREAD_ID: 35
EVENT_ID: 13081
OBJECT_SCHEMA: test
OBJECT_NAME: d
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139756088373592
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 1808:2:5:1
ENGINE_TRANSACTION_ID: 1808
THREAD_ID: 35
EVENT_ID: 13111
OBJECT_SCHEMA: test
OBJECT_NAME: d
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: id
OBJECT_INSTANCE_BEGIN: 139756088370552
LOCK_TYPE: RECORD
LOCK_MODE: X
LOCK_STATUS: GRANTED
LOCK_DATA: supremum pseudo-record <--- HERE
2 rows in set (0.00 sec)
This is not a solution to the deadlock itself, but having visibility on the locks taken goes a long way to understand the problem.
Here, both SELECT FOR UPDATE lock the "suprenum" record, because id 100 and 700 are greater than the biggest ID in the table (it's empty).
Once more records are present (say at ID = 500), both queries should execute concurrently, as a different gap in IDs will be locked.
I suspect the deadlock happens because InnoDB is conservative on dealing with "gaps". Note that 100 and 700 are both in the same nebulous area of untouched land. InnoDB can't (or at least does not) deal with the fact that "100" and "700" are different. InnoDB would like to tag individual rows but there are no rows already in the table with those ids.
Transaction 2 was probably going to timeout (see innodb_lock_wait_timeout). When you poked Transaction 1 a second time with #2 still wanting a lock, InnoDB punted and gave up.
Bottom Line: Live with deadlocks. When they happen, start back at the BEGIN. You have found yet-another obscure case where an unnecessary deadlock happens.
Furthermore, I suspect that fixing the code for this case would slow down most other cases, and lead to a series of errors that would take several releases to discover and fix.