При автокоммитах запрос вызывает Deadlock. MySQL. InnoDB
В попытках побороть взаимоблокировки (дедлоки) на боевом сервере зашёл в тупик. Есть 2 таблицы:
Первая:
create table table_1
(
id int auto_increment
primary key,
data1 text null,
data2 text null
);
Вторая:
create table table_2
(
id int auto_increment
primary key,
t1_id int null,
data1 text null,
data2 text null,
constraint table_2_table_1_id_fk
foreign key (t1_id) references table_1 (id)
on update cascade on delete cascade
);
Для тестов, в table_1 30 записей, в table_2 60 (каждые 2 записи из table_2 ссылаются по ключу к 1 запись в table_1).
Далее, простой php скрипт, который обновляет некоторые записи из таблицы table_1, по условию из table_2 в бесконечном цикле:
<?php
$db = new PDO("mysql:host=127.0.0.1;dbname=test_db;", 'debian-sys-maint', 'pass', [
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$random = rand(0, 9);
while (true) {
$db->exec("
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%$random%'
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1;");
}
Так вот, запускаю этот PHP скрипт в 150 экземплярах, и генерируется ошибка Deadlock. Я пытался её устранить, модифицировав запрос следующим образом:
UPDATE table_1
INNER JOIN (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id) table_2 on table_1.id = table_2.t1_id
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1;
WITH tmp (id) AS (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id)
UPDATE table_1
INNER JOIN tmp on table_1.id = tmp.id
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1;
UPDATE table_1
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1
WHERE table_1.id in (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id);
UPDATE table_1, (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id) tmp1
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1
WHERE table_1.id = tmp1.t1_id;
Собственно первый вопрос: Я не понимаю, откуда берётся дедлок, если записи сортируются всегда в одном и том же порядке, состояние гонки исключено. Вот что возвращает SHOW ENGINE INNODB STATUS; при поимке дедлока:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2022-05-10 01:56:01 140233769219840
*** (1) TRANSACTION:
TRANSACTION 3529589, ACTIVE 1 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 7 lock struct(s), heap size 1128, 90 row lock(s)
MySQL thread id 20, OS thread handle 140233708357376, query id 173 localhost 127.0.0.1 debian-sys-maint executing
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%9%'
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 2 page no 4 n bits 224 index PRIMARY of table `test_db`.`table_1` trx id 3529589 lock_mode X locks rec but not gap
.....................
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2 page no 4 n bits 224 index PRIMARY of table `test_db`.`table_1` trx id 3529589 lock_mode X locks rec but not gap waiting
.....................
*** (2) TRANSACTION:
TRANSACTION 3529887, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1128, 52 row lock(s)
MySQL thread id 118, OS thread handle 140229428811520, query id 444 localhost 127.0.0.1 debian-sys-maint executing
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%7%'
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2 page no 4 n bits 224 index PRIMARY of table `test_db`.`table_1` trx id 3529887 lock_mode X locks rec but not gap
.....................
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2 page no 4 n bits 224 index PRIMARY of table `test_db`.`table_1` trx id 3529887 lock_mode X locks rec but not gap waiting
.....................
*** WE ROLL BACK TRANSACTION (2)
Единственный способ, которым у меня получилось решить взаимоблокировку, это создание временной таблицы, которая бы содержала готовый набор ключей, которые будут затронуты UPDATE сразу из table_1:
DROP TABLE IF EXISTS tmp1;
CREATE TEMPORARY TABLE tmp1
SELECT table_2.t1_id FROM table_2 WHERE table_2.data2 like '%$random%'
UPDATE table_1
INNER JOIN tmp1 on table_1.id = tmp1.t1_id
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1;
Или с тем же успехом я могу создать копию table_2 в временную таблицу, оно тоже не вызывает дедлоки:
DROP TABLE IF EXISTS tmp1;
CREATE TEMPORARY TABLE tmp1
SELECT * FROM table_2 WHERE 1;
UPDATE table_1
INNER JOIN tmp1 on table_1.id = tmp1.t1_id and tmp1.data2 like '%$random%'
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1;
Второй вопрос: Почему этот вариант не вызывает дедлоки? Я так понимаю, блокируются именно записи из таблицы table_1, в данном случае они точно так же блокируются, только поиск происходит не по данным таблицы table_2, а по данным временной таблицы tmp1.
Казалось бы, моя проблема решена, дедлоки не вызываются, но мне не понравилось решение с временной таблицей и я продолжил свои тесты в ходе которых я наткнулся на очень странную вещь, которая окончательно загнала меня в тупик. Если самому начать и завершить транзакцию, то дедлоки не появляются:
BEGIN;
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%$random%'
SET table_1.data1 = table_1.data1 + 1,
table_1.data2 = table_1.data2 + 1;
COMMIT;
Это, пожалуй, последний и наиболее волнующий меня вопрос... У меня включен автокоммит, почему если я явно не начинаю и не завершая транзакцию, то вылазиют дедлоки? PHP как известно работает в одном потоке, на 1 скрипт ровно 1 подключение к БД, все 150 скриптов работают параллельно