Transactional Outbox и Inbox, идемпотентные консьюмеры

↑ 5.4 Очереди и брокеры: Kafka, RabbitMQ · ← Предыдущая · → Следующая

Обязательно~3 мин чтенияУровень: middle

Зачем это на собесе

Стандартный паттерн надёжной публикации событий: «сохранили в БД, но не отправили» — как избежать.

Объяснение

Проблема 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).
  • Не публикуйте в брокер внутри транзакции БД: сначала коммит.

Практика

  1. Реализуйте outbox и relay на skip locked.
  2. Сымитируйте падение после коммита и убедитесь, что событие уходит позже.
  3. Добавьте Inbox в потребителе.

Вопросы с ответами

Связанные темы