Этот вопрос проверяет понимание того, почему возникают дедлоки в реляционных БД и как проектировать запросы так, чтобы их избегать.
Deadlock появляется, когда два запроса блокируют ресурсы друг друга в пересекающемся порядке: один заблокировал строку A и ждёт строку B, а другой — наоборот. Причины: несогласованный порядок обновлений, долгие транзакции, отсутствие индексов, смешивание SELECT FOR UPDATE и UPDATE на разных путях. Чтобы избегать deadlock: соблюдать единый порядок обновления сущностей, делать транзакции короткими, использовать правильные индексы, избегать сканирования больших таблиц под блокировками и по возможности минимизировать конкурирующие UPDATE по одним и тем же строкам.
Определение.
Deadlock (взаимная блокировка) — это ситуация, когда две или более транзакций ждут ресурсы, удерживаемые друг другом, и никто не может продолжать выполнение.
Пример:
Транзакция T1: обновляет строку A → ждёт B.
Транзакция T2: обновляет строку B → ждёт A.
Обе транзакции «висят», и Postgres завершает одну из них с ошибкой deadlock.
Классическая проблема:
В одном месте кода обновляем (account_1 → account_2).
В другом — (account_2 → account_1).
При больших нагрузках это почти гарантированно даёт дедлок.
Если транзакция держит блокировку слишком долго:
Другие запросы накапливаются.
Вероятность циклической блокировки растёт.
Часто это связано с:
Долгими вычислениями внутри транзакции.
Большими SELECT без индекса.
Ненужными UPDATE.
Если запрос:
SELECT * FROM accounts WHERE status = 'active' FOR UPDATE;
и нет индекса по статусу, Postgres делает seq scan и блокирует очень много строк → растёт шанс дедлока.
Если приложение в асинхронном режиме делает несколько параллельных запросов в одной транзакции, это тоже может вызвать deadlock.
Если два сервиса обновляют один и тот же баланс/заказ/запись — конфликт почти неизбежен.
Главное правило:
Всегда обновлять записи в одинаковом порядке.
Например, при переводе денег:
Определяем порядок: обновляем сначала аккаунт с меньшим ID, затем с большим.
Тогда независимо от направления перевода запросы блокируют строки в одном порядке.
Python (псевдокод)
first, second = sorted([src_account, dst_account])
update(first)
update(second)
Принципы:
Транзакции содержат только операции с БД.
Нет сложных расчётов внутри.
Нет внешних API-вызовов.
Нет долгого ожидания.
Если запрос делает:
SELECT ... FOR UPDATE WHERE user_id = 123;
то индекс по user_id обязателен.
Без индекса Postgres может заблокировать сотни ненужных строк.
Правильно:
UPDATE только если значение поменялось.
Неправильно:
ALWAYS UPDATE, даже если данные не меняются → создаются ненужные блокировки.
Вместо UPDATE → SELECT FOR UPDATE → UPDATE можно:
Прочитать запись.
Попробовать обновить с WHERE version = X.
Если 0 обновлений — повторить.
Это снижает количество тяжёлых блокировок.
Если на уровне приложения можно добиться, чтобы только один сервис обновлял ресурс — дедлоки исключаются.
Пример:
Балансы обновляются только через один сервис, а не всеми микросервисами.
Deadlock возникает при неправильном порядке блокировок, долгих транзакциях, отсутствующих индексах и конкурирующих UPDATE. Чтобы их избежать, нужно согласовать порядок обновлений, минимизировать область транзакций, правильно индексировать запросы и по возможности уменьшать конкуренцию. Более продвинутые техники включают оптимистическую блокировку и перераспределение ответственности между сервисами.