Ответ
Transactional Outbox — это архитектурный паттерн, используемый в системах, основанных на событиях (Event-Driven Architecture), для гарантированной доставки сообщений при наличии транзакций базы данных. Он решает проблему атомарности обновления состояния приложения и публикации соответствующего события.
Проблема: В распределённой системе типичная операция «сохранить данные в БД и отправить событие в брокер сообщений (Kafka, RabbitMQ)» не является атомарной. Может произойти сбой между этими шагами, приводящий к несогласованности: данные сохранены, но событие не отправлено, или наоборот.
Решение: Паттерн предлагает использовать таблицу в БД как промежуточный буфер (Outbox).
Как это работает:
- В рамках одной транзакции с основными бизнес-данными приложение сохраняет событие в специальную таблицу
Outbox(обычно с полямиId,Type,Payload,CreatedAt,Processed). - Транзакция фиксируется. Гарантируется, что либо и данные, и событие сохранены, либо ни то, ни другое.
- Отдельный фоновый процесс (Outbox Processor / Publisher) периодически опрашивает таблицу
Outboxна наличие необработанных сообщений. - Для каждого такого сообщения процессор публикует его в брокер сообщений и помечает сообщение как обработанное (или удаляет его).
Пример реализации на C# (Entity Framework Core):
// 1. Модель сообщения Outbox
public class OutboxMessage
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Type { get; set; } // e.g., "OrderCreated"
public string Payload { get; set; } // JSON сериализованное событие
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ProcessedAt { get; set; }
}
// 2. Сохранение в рамках транзакции
public async Task PlaceOrder(Order order, AppDbContext context)
{
using var transaction = await context.Database.BeginTransactionAsync();
try
{
// Сохраняем бизнес-сущность
context.Orders.Add(order);
await context.SaveChangesAsync();
// Сохраняем событие в Outbox в той же транзакции
var outboxMessage = new OutboxMessage
{
Type = "OrderPlaced",
Payload = JsonSerializer.Serialize(new OrderPlacedEvent(order.Id))
};
context.OutboxMessages.Add(outboxMessage);
await context.SaveChangesAsync();
await transaction.CommitAsync(); // Всё или ничего
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
Преимущества:
- Гарантированная доставка: Решает проблему атомарности «БД -> Брокер».
- Устойчивость к сбоям: Сообщения не теряются при падении приложения между шагами.
- Отделение ответственности: Основная бизнес-логика не заботится о деталях доставки в брокер.
Недостатки / Компромиссы:
- Сложность: Требует дополнительной таблицы и фонового сервиса.
- Задержка (Near Real-Time): События публикуются не мгновенно, а с задержкой опроса процессора.
- Возможные дубликаты: Процессор должен быть идемпотентным на случай повторной публикации одного и того же сообщения.