🛠️ После deadlock транзакцию повторили. Почему внешний эффект мог выполниться дважды?

UPDATE в БД → HTTP-вызов → следующий SQL → 40P01 deadlock_detected → ROLLBACK → retry

PostgreSQL отменяет изменения своей транзакции. Но он не может отменить письмо, платёжный запрос или сообщение, уже отправленное другой системе.

Причём результат внешнего вызова может быть неизвестен: действие могло выполниться, а ответ — потеряться.

После 40P01 часто повторяют всю локальную транзакцию БД: — заново читают данные; — повторно принимают решение; — снова формируют SQL. Повторять только последний statement недостаточно. Но внешний вызов нельзя слепо повторять вместе с транзакцией.

Что помогает: Идемпотентный ключ. Одна бизнес-команда использует один ключ во всех retry. Новый UUID на каждую попытку создаёт новую операцию. Получатель должен хранить ключ и корректно возвращать результат повтора. Transactional outbox. Бизнес-изменение и запись события сохраняются одной транзакцией. После commit отдельный relay публикует событие. Outbox не гарантирует exactly-once: сообщение может быть отправлено повторно, поэтому consumer должен учитывать дубликаты.

Проверьте: — какие ошибки разрешено повторять; — повторяется ли транзакция целиком; — есть ли внутри внешние эффекты; — ограничено ли число попыток; — используются ли backoff и журналирование; — стабилен ли идемпотентный ключ; — что происходит при неизвестном результате вызова; — готов ли consumer к повторной доставке.

Вывод: ROLLBACK защищает состояние PostgreSQL, но не отменяет действия в другой системе. Retry нужно проектировать на уровне всей бизнес-операции.

🔹🔹🔹🔹

🛠️ После deadlock транзакцию повторили | Сетка — социальная сеть от hh.ru