🛠️ После 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 нужно проектировать на уровне всей бизнес-операции.
🔹🔹🔹🔹