Как избежать рассогласования данных если транзакция закоммичена, а вызов внешнего сервиса не удался
Что проверяют
Понимаете ли вы, что внешний вызов нельзя откатить, и знаете ли схемы, которые с этим живут.
Как отвечать
Сначала назовите корень: транзакция базы и вызов чужой системы не образуют общей транзакции. Значит цель не в том, чтобы откатить всё, а в том, чтобы система пришла в согласованное состояние сама.
Рабочие подходы:
- не делать внешний вызов внутри транзакции: сначала фиксируются данные с промежуточным состоянием (
PENDING), затем идёт вызов, затем состояние обновляется. Транзакция короткая, блокировки не держатся; - схема исходящих сообщений: в одной транзакции с данными пишется задание на внешний вызов, а отдельный процесс его выполняет с повторами. Так задание не потеряется даже при падении сразу после коммита;
- повторы и идемпотентность: у внешнего вызова должен быть ключ идемпотентности, иначе повтор создаст второй платёж. Если чужой API его не поддерживает, нужен собственный журнал выполненных операций;
- компенсация (сага): когда откатить нельзя, выполняется обратное действие. Списали деньги, но не смогли создать заказ, значит делается возврат;
- сверка: фоновая задача находит записи, застрявшие в промежуточном состоянии дольше срока, и доводит их до конца или отменяет. Это последний рубеж, и он нужен всегда;
- наблюдение: оповещение по числу записей, застрявших в промежуточном состоянии.
Отдельно скажите про TransactionSynchronization и transaction.on_commit-подобные механизмы: внешний вызов запускается после фиксации, а не внутри неё, иначе он произойдёт даже при откате.
Где спотыкаются
Делают внешний вызов внутри транзакции и получают долгие блокировки плюс отправку при откате.
Полагаются на повтор без ключа идемпотентности и создают дубликаты операций.
Не делают сверку, и часть записей навсегда остаётся в промежуточном состоянии.