Взаємоблокування (deadlock) - дві транзакції чекають одна на одну: A тримає рядок 1 і хоче рядок 2, B тримає рядок 2 і хоче рядок 1. InnoDB помічає цикл і відкочує одну з транзакцій (зазвичай ту, що змінила менше рядків). Вона отримує помилку 1213, друга продовжує роботу.
Перше правило: deadlock - не аварія. У системі з паралельними записами вони трапляються. Застосунок має бути готовим повторити транзакцію повністю:
DB::transaction(function () use ($from, $to, $amount) {
// ...
}, attempts: 3);
Laravel повторює замикання при взаємоблокуванні. Важливо: повторюється вся транзакція, тож побічні ефекти (листи, HTTP-запити) - лише після коміту (DB::afterCommit() чи afterCommit у подіях і джобах).
Друге - з'ясувати причину:
SHOW ENGINE INNODB STATUS\G
-- розділ LATEST DETECTED DEADLOCK: обидві транзакції,
-- їхні запити, які блокування тримають і чого чекають
Там видно лише останній deadlock. Щоб писати всі в журнал помилок:
SET PERSIST innodb_print_all_deadlocks = ON;
Типові причини й ліки:
- Різний порядок захоплення рядків. Переказ з A на B і з B на A одночасно. Ліки - завжди блокувати в одному порядку (наприклад, за зростанням id):
$accounts = Account::whereIn('id', [$from, $to])->orderBy('id')->lockForUpdate()->get();
- Відсутній індекс.
UPDATE ... WHEREбез індексу блокує все проскановане - ймовірність перетину різко зростає. - Блокування проміжків. Два
SELECT ... FOR UPDATEза неіснуючим ключем, а потім дваINSERT. Ліки -INSERT ... ON DUPLICATE KEY UPDATEзамість «перевірити, потім вставити», або READ COMMITTED. - Довгі транзакції. Чим довше тримаються блокування, тим більше перетинів. Важкі обчислення й зовнішні виклики - поза транзакцією.
- Масові оновлення. Великий
UPDATEпо частинах з однаковим порядком.
Що не допомагає: збільшувати innodb_lock_wait_timeout - він стосується звичайного очікування, а не циклів, які InnoDB розриває одразу.