Что такое паттерн Transactional Outbox и какую проблему он решает?

Ответ

Transactional Outbox — это паттерн надежной доставки сообщений, который решает проблему двойной записи (dual write). Проблема возникает, когда нужно атомарно изменить состояние в базе данных и опубликовать событие в брокере сообщений (например, Kafka или RabbitMQ).

Если делать это последовательно, система становится ненадежной:

  • Запись в БД успешна, а отправка в брокер — нет. Результат: состояние изменилось, но никто об этом не узнал.
  • Отправка в брокер успешна, а запись в БД — нет. Результат: разослано ложное событие.

Как работает паттерн:

  1. В той же базе данных, где хранятся основные данные, создается специальная таблица outbox (почтовый ящик).
  2. Изменение данных и запись события в таблицу outbox происходят в рамках одной транзакции БД.
  3. Отдельный асинхронный процесс (называемый Relay или Publisher) периодически опрашивает таблицу outbox.
  4. Обнаружив новые события, Relay пытается доставить их в брокер сообщений.
  5. После успешной доставки Relay помечает событие в outbox как обработанное (или удаляет его).

Пример на Go:

func CreateOrder(db *sql.DB, order Order) error {
    tx, err := db.Begin()
    if err != nil { return err }
    defer tx.Rollback() // Безопасный откат по умолчанию

    // 1. Сохраняем заказ в основной таблице
    _, err = tx.Exec("INSERT INTO orders...", order.ID, order.Details)
    if err != nil { return err }

    // 2. Сериализуем событие и записываем его в outbox-таблицу
    eventPayload, _ := json.Marshal(map[string]interface{}{"order_id": order.ID})
    _, err = tx.Exec(
        "INSERT INTO outbox (event_type, payload) VALUES (?, ?)", 
        "OrderCreated", 
        eventPayload,
    )
    if err != nil { return err }

    // 3. Коммитим транзакцию. Только после этого изменения станут видны
    return tx.Commit()
}

Преимущества:

  • Атомарность и консистентность: Гарантирует, что событие будет создано тогда и только тогда, когда основная операция с данными успешна.
  • Отказоустойчивость: Если брокер сообщений временно недоступен, события остаются в outbox и будут отправлены позже, когда связь восстановится. Потеря сообщений исключена.
  • Разделение ответственностей: Основной сервис отвечает только за бизнес-логику, а доставкой сообщений занимается отдельный компонент (Relay).