I have run into a scenario where deleting from and inserting into a table with foreign keys in MySQL is, I believe, causing gap locks to occur that result in a deadlock situation. I am trying to trouble-shoot how to fix this, and the only solution I've come up with is an additional insert before the delete and insert. I was wondering if anyone can explain why these useless inserts are needed and why "select for update" is not similarly locking the rows. I am using MySQL version 5.7.23.
Example schema and initial rows:
CREATE TABLE parent (
id INT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE=InnoDB;
CREATE TABLE child (
name VARCHAR(127) NOT NULL,
parent INT UNSIGNED NOT NULL,
FOREIGN KEY (parent) REFERENCES parent(id)
) ENGINE=InnoDB;
INSERT INTO parent VALUES (1);
INSERT INTO parent VALUES (2);
In my code, I need to replace all of the current children for a target parent with a new group of children (zero or more rows), and there may or may not already be children present for this target parent. The current code looks like this, and causes deadlocks when there are no children present in the table for these particular parents:
transaction a:
BEGIN WORK;
DELETE FROM child WHERE parent=1;
transaction b:
BEGIN WORK;
DELETE FROM child WHERE parent=2;
transaction a:
INSERT INTO child VALUES ('a', 1);
-- client a hangs waiting for lock
transaction b:
INSERT INTO child VALUES ('b', 2);
-- client b aborts: ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
This will reliably trigger a deadlock if you run these transaction statements in this order in different client sessions. I believe this is because of gap locking. When I attempted to switch the transaction to a READ COMMITTED isolation level, it will avoid the deadlock, but potentially phantom rows will occur if the two transactions are operating on children of the same parent.
Inserting a useless row for the parent prior to the delete appears to fix the deadlock. There is no deadlock in the following scenario:
transaction a:
BEGIN WORK;
INSERT INTO child VALUES ('fake', 1);
DELETE FROM child WHERE parent=1;
transaction b:
BEGIN WORK;
INSERT INTO child VALUES ('fake', 2);
DELETE FROM child WHERE parent=2;
-- client b hangs waiting for lock
transaction a:
INSERT INTO child VALUES ('a', 1);
COMMIT;
-- no deadlock; client b now has lock
transaction b:
INSERT INTO child VALUES ('b', 2);
COMMIT;
I thought maybe instead of this insert, I could substitute a select statement to get the same locks as the useless insert, but the following does not prevent the deadlock:
transaction a:
BEGIN WORK;
SELECT * FROM child WHERE parent=1 FOR UPDATE;
DELETE FROM child WHERE parent=1;
transaction b:
BEGIN WORK;
SELECT * FROM child WHERE parent=2 FOR UPDATE;
DELETE FROM child WHERE parent=2;
transaction a:
INSERT INTO child VALUES ('a', 1);
-- client a hangs waiting for lock
transaction b:
INSERT INTO child VALUES ('b', 2);
-- client b aborts: ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Why do neither the delete nor "select for update" statements retrieve the same lock as inserting a useless row, and is there a better way to accomplish this task and avoid deadlocks? Keep in mind that the child table may or may not have one or more rows already existing for the target parent and I'd like to delete all of the current rows in the child table for the target parent and replace with zero or more new children. Thank you!