Deadlock в MySQL: как разобрать взаимную блокировку и не повторить ошибку
Deadlock в MySQL возникает, когда две транзакции блокируют ресурсы и ждут друг друга. MySQL выбирает одну транзакцию жертвой, откатывает её и отдаёт ошибку. Для приложения это выглядит как случайная ошибка записи: иногда всё проходит, иногда падает на том же месте.
Deadlock — не всегда признак плохой базы. В конкурентной системе он возможен. Но если он повторяется регулярно, нужно менять порядок обновлений, индексы или длину транзакций.
Типичный пример
Первая транзакция обновляет заказ, потом остаток. Вторая — остаток, потом заказ. При одновременном запуске они могут заблокировать друг друга.
-- transaction 1
UPDATE orders SET status = 'paid' WHERE id = 10;
UPDATE stocks SET amount = amount - 1 WHERE product_id = 5;
-- transaction 2
UPDATE stocks SET amount = amount - 1 WHERE product_id = 5;
UPDATE orders SET status = 'paid' WHERE id = 10;
Решение часто простое: во всех местах обновлять сущности в одинаковом порядке.
Посмотреть последний deadlock
MySQL показывает детали последней взаимной блокировки.
SHOW ENGINE INNODB STATUS\G
В секции LATEST DETECTED DEADLOCK нужно смотреть:
- какие транзакции участвовали;
- какие SQL-запросы выполнялись;
- какие индексы использовались;
- какую строку или диапазон держала блокировка;
- какую транзакцию MySQL откатил.
Этот вывод лучше сохранить сразу. Следующий deadlock перезапишет информацию.
Проверить индексы
Если UPDATE или SELECT FOR UPDATE ищет строки без подходящего индекса, MySQL может блокировать больше строк, чем ожидается.
UPDATE orders
SET status = 'processing'
WHERE external_id = 'abc-123';
Если external_id не индексирован, запрос может просматривать таблицу и создавать лишние блокировки. Индекс уменьшает область поиска.
CREATE INDEX idx_orders_external_id
ON orders (external_id);
Транзакции должны быть короткими
Чем дольше транзакция открыта, тем выше риск блокировок. Плохой вариант — открыть транзакцию, сходить во внешний API, обработать большой файл, потом обновить базу.
$transaction = $db->beginTransaction();
try {
updateOrder();
callExternalApi();
updateStock();
$transaction->commit();
} catch (Throwable $e) {
$transaction->rollBack();
}
Внешние API лучше выносить за пределы транзакции или менять порядок так, чтобы блокировки базы держались минимальное время.
Единый порядок обновлений
Если код в разных местах обновляет одни и те же таблицы, порядок должен быть одинаковым. Например, всегда сначала order, потом order_items, потом stock. Или наоборот, но единообразно.
Опаснее всего, когда одна часть системы работает через импорт, другая через админку, третья через cron, и каждая обновляет таблицы в своём порядке.
Retry при deadlock
Deadlock можно и нужно обрабатывать повтором, если операция идемпотентна. MySQL сам откатывает одну транзакцию, а приложение может повторить её через короткую паузу.
$attempts = 0;
while ($attempts < 3) {
$attempts++;
try {
runTransaction();
break;
} catch (Throwable $e) {
if (strpos($e->getMessage(), 'Deadlock found') === false || $attempts >= 3) {
throw $e;
}
usleep(200000);
}
}
Retry не заменяет исправление причины, но делает систему устойчивее к редким конфликтам.
Логировать контекст
В логах должны быть ID заказа, товара, пользователя, название операции и номер попытки. Без этого deadlock остаётся технической ошибкой без бизнес-контекста.
[
'event' => 'deadlock_retry',
'order_id' => 10,
'product_id' => 5,
'attempt' => 2,
]
Чек-лист
- Сохранить SHOW ENGINE INNODB STATUS после deadlock.
- Найти SQL-запросы обеих транзакций.
- Проверить индексы в WHERE и JOIN.
- Сделать транзакции короче.
- Убрать внешние API из открытой транзакции.
- Привести порядок обновления таблиц к единому виду.
- Добавить retry для безопасных операций.
- Логировать бизнес-контекст deadlock.
Deadlock не лечится случайным увеличением таймаутов. Нужно понять, какие транзакции пересекаются, какие строки они блокируют и почему порядок блокировок отличается. После этого исправление обычно становится конкретным.
Комментарии (0)
Пока нет комментариев. Будьте первым!