Как обработать ситуацию, если база упала после записи, но до записи в очередь
Что проверяют
Это вопрос о проблеме двойной записи: данные в базе сохранены, а событие в брокер не ушло, и соседние сервисы о нём не узнают. Интервьюер ждёт паттерн Transactional Outbox и понимание, почему TransactionScope тут не спасает. Следом спрашивают про дубли сообщений и идемпотентность.
Как отвечать
Сначала объясните, почему очевидные варианты не работают:
- одна транзакция на базу и брокер: клиенты RabbitMQ и Kafka в
TransactionScopeне входят. Распределённые транзакции через MSDTC в .NET 7 и новее доступны только на Windows и в облаке почти не применяются; - обратный порядок: сначала публикация, потом запись в базу. При сбое получите событие о заказе, которого нет, это ещё хуже;
- повтор в
catch: если процесс упал целиком, повторять будет некому.
Решение, Transactional Outbox:
- запись: в той же транзакции, что и бизнес-данные, в таблицу
OutboxMessagesпишется сообщение (тип, JSON,CreatedAt,ProcessedAt). В EF Core это один вызовSaveChangesAsync, поэтому сохраняется либо всё, либо ничего; - отправка: фоновый
BackgroundServiceчитает неотправленные строки пачками, публикует их и проставляетProcessedAt. При нескольких экземплярах сервиса строки блокируются, например черезFOR UPDATE SKIP LOCKEDв PostgreSQL; - гарантия: получается at-least-once. Если процесс упал между публикацией и отметкой, сообщение уйдёт повторно;
- идемпотентность потребителя: у сообщения стабильный
MessageId, потребитель хранит обработанные ID в таблице Inbox или делает операцию идемпотентной по смыслу.
Готовые реализации: Outbox в MassTransit и NServiceBus, библиотека CAP. Альтернатива опросу таблицы: CDC через Debezium, который читает журнал транзакций базы и сам публикует изменения.
Где спотыкаются
Оборачивают запись в базу и публикацию в RabbitMQ в TransactionScope и считают задачу решённой. Код скомпилируется, но атомарности не будет.
Обещают доставку exactly-once. Честный ответ: at-least-once плюс идемпотентная обработка, и этого достаточно.
Забывают про чистку outbox-таблицы и мониторинг её хвоста. Если отправщик встал, таблица растёт, а события копятся незаметно для всех.