Transactional Outbox и Inbox, идемпотентные консьюмеры
↑ 5.4 Очереди и брокеры: Kafka, RabbitMQ · ← Предыдущая · → Следующая
Зачем это на собесе
Стандартный паттерн надёжной публикации событий: «сохранили в БД, но не отправили» — как избежать.
Объяснение
Проблема dual write: сервис сохраняет заказ в БД и публикует событие в брокер — это две системы без общей транзакции. Падение между шагами → заказ есть, события нет (или наоборот).
Transactional Outbox: событие записывается в таблицу outbox в той же транзакции, что и бизнес-данные. Отдельный процесс (relay) читает таблицу и публикует в брокер, помечая отправленное.
sequenceDiagram participant S as Сервис participant DB as БД participant R as Relay participant K as Kafka S->>DB: BEGIN, insert order, insert outbox, COMMIT R->>DB: выбрать неотправленные R->>K: publish R->>DB: пометить отправленными
await using var tx = await db.Database.BeginTransactionAsync(ct);
db.Orders.Add(order);
db.Outbox.Add(new OutboxMessage(Guid.NewGuid(), "OrderCreated", JsonSerializer.Serialize(evt)));
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);Relay: фоновый сервис с запросом select ... for update skip locked либо CDC (Debezium читает WAL и публикует в Kafka).
Inbox — зеркальный паттерн на стороне потребителя: таблица обработанных message_id + эффект в одной транзакции (см. Гарантии доставки: at-most-once, at-least-once, exactly-once).
| Вариант relay | Плюсы | Минусы |
|---|---|---|
| Polling + skip locked | просто | задержка, нагрузка на БД |
| CDC (Debezium) | почти в реальном времени, без запросов | инфраструктура |
| MassTransit/Wolverine outbox | готовое решение | зависимость от библиотеки |
Нюансы и подводные камни
- Relay даёт at-least-once: дубли возможны, потребители обязаны быть идемпотентными.
- Порядок событий одного агрегата: сортируйте по sequence/времени создания.
- Очищайте обработанные записи (архив/TTL), иначе таблица разрастается.
- Несколько инстансов relay должны не мешать друг другу (
skip locked). - Не публикуйте в брокер внутри транзакции БД: сначала коммит.
Практика
- Реализуйте outbox и relay на
skip locked. - Сымитируйте падение после коммита и убедитесь, что событие уходит позже.
- Добавьте Inbox в потребителе.
Вопросы с ответами
Какую проблему решает Outbox?
Гарантирует, что событие будет опубликовано тогда и только тогда, когда транзакция с бизнес-данными зафиксирована.
Даёт ли Outbox exactly-once?
Нет, это at-least-once; нужна идемпотентность потребителя (Inbox).
Polling или CDC?
Polling проще и достаточен на средней нагрузке; CDC — минимальная задержка и нагрузка при высоких объёмах.