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