Sobes.tech
Назад к вопросам
Junior — Middle
63

Имели ли вы опыт работы с механизмом Transactional Outbox для обеспечения надежной передачи сообщений?

Компании, где спрашивали
Банк ПСБ

Ответ от нейросети

sobes.tech AI

Что хотят услышать интервьюеры:

Да, знаком с паттерном Transactional Outbox и понимаю, зачем он нужен. Он помогает атомарно сохранять бизнес-данные и факт отправки события, чтобы не терять сообщения при сбоях. Обычно это решение используют вместе с фоновой публикацией сообщений в брокер после записи в базу.

Определение:

Transactional Outbox — это паттерн, при котором приложение сначала в одной транзакции записывает бизнес-изменения и сообщение в отдельную таблицу outbox, а затем отдельный процесс или воркер надежно публикует эти сообщения во внешнюю систему.
Смысл в том, чтобы не пытаться сделать распределённую транзакцию между БД и брокером сообщений, а гарантировать, что событие либо сохранено вместе с данными, либо не сохранено вовсе.

Пример использования:

Например, при создании заказа в C# сервис сначала сохраняет заказ и запись об событии OrderCreated в одной транзакции базы данных. После этого background service читает непросланные записи из outbox и отправляет их в Kafka или RabbitMQ.

public async Task CreateOrderAsync(CreateOrderRequest request)
{
    using var tx = await _db.Database.BeginTransactionAsync();

    var order = new Order
    {
        Id = Guid.NewGuid(),
        CustomerId = request.CustomerId,
        Total = request.Total
    };

    _db.Orders.Add(order);

    var outboxMessage = new OutboxMessage
    {
        Id = Guid.NewGuid(),
        Type = "OrderCreated",
        Payload = JsonSerializer.Serialize(new { order.Id, order.CustomerId, order.Total }),
        CreatedAt = DateTime.UtcNow,
        ProcessedAt = null
    };

    _db.OutboxMessages.Add(outboxMessage);

    await _db.SaveChangesAsync();
    await tx.CommitAsync();
}
public class OutboxPublisher : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var messages = await _db.OutboxMessages
                .Where(x => x.ProcessedAt == null)
                .OrderBy(x => x.CreatedAt)
                .Take(100)
                .ToListAsync(stoppingToken);

            foreach (var message in messages)
            {
                await _broker.PublishAsync(message.Type, message.Payload, stoppingToken);
                message.ProcessedAt = DateTime.UtcNow;
            }

            await _db.SaveChangesAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
        }
    }
}

Пояснение кода:

Код показывает два шага.
Сначала в транзакции одновременно сохраняются заказ и запись в таблицу outbox — это гарантирует согласованность.
Потом отдельный фоновый процесс выбирает неотправленные сообщения, публикует их в брокер и помечает как обработанные.
Если сервис упадёт после коммита БД, сообщение не потеряется, потому что оно уже лежит в outbox и будет отправлено позже.

Ключевые моменты:

  • Pattern Transactional Outbox решает проблему согласованности между БД и брокером сообщений.
  • Запись в outbox должна происходить в одной транзакции с бизнес-данными.
  • Отправка сообщений выполняется отдельно, обычно фоновым воркером или отдельным сервисом.
  • Подход обычно дает at-least-once доставку, поэтому потребители должны быть готовы к дублям.
  • Для надежности часто добавляют идемпотентность и уникальные идентификаторы сообщений.
  • Это альтернатива распределённым транзакциям, а не их полная замена во всех случаях.