Зачем нужен Outbox Pattern

Ответ

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

Проблема, которую решает Outbox:

В распределенных системах часто возникает необходимость изменить данные в базе данных и одновременно отправить сообщение (например, в брокер сообщений, такой как Kafka или RabbitMQ). Если эти две операции выполняются отдельно, существует риск несогласованности: либо данные изменятся, но сообщение не отправится (например, из-за сбоя сети или брокера), либо сообщение отправится, но транзакция с данными откатится.

Суть паттерна:

  1. Атомарная запись: Вместо прямой отправки сообщения, оно записывается в специальную таблицу outbox в той же транзакции базы данных, что и изменение бизнес-данных. Это гарантирует, что либо оба действия (изменение данных и запись в outbox) успешно завершатся, либо ни одно из них.
  2. Фоновая отправка: Отдельный фоновый процесс (например, поллер или механизм Change Data Capture - CDC) периодически считывает сообщения из таблицы outbox, отправляет их в брокер сообщений и помечает как отправленные (или удаляет).

Пример на Go (псевдокод):

func CreateOrder(order Order) error {
    tx := db.Begin() // Начинаем транзакцию
    if tx.Error != nil {
        return tx.Error
    }

    // 1. Сохраняем бизнес-данные
    err := tx.Create(&order).Error
    if err != nil {
        tx.Rollback()
        return err
    }

    // 2. Создаем сообщение для Outbox в той же транзакции
    outboxMsg := OutboxMessage{
        Topic:   "order.created",
        Payload: order.ToJSON(), // Сериализованные данные заказа
        Status:  "pending",
    }
    err = tx.Create(&outboxMsg).Error
    if err != nil {
        tx.Rollback()
        return err
    }

    // 3. Коммитим транзакцию. Если здесь произойдет сбой, ни данные, ни сообщение не будут сохранены.
    return tx.Commit()
}

// Фоновый процесс (пример логики)
func ProcessOutbox() {
    // Периодически читаем неотправленные сообщения
    messages := db.Where("status = ?", "pending").Find(&[]OutboxMessage{}).Error
    for _, msg := range messages {
        err := messageBroker.Publish(msg.Topic, msg.Payload)
        if err == nil {
            // Если успешно отправлено, помечаем как отправленное
            db.Model(&msg).Update("status", "sent")
        } else {
            // Логируем ошибку, сообщение останется в статусе 'pending' для повторной попытки
        }
    }
}

Плюсы Outbox Pattern:

  • Атомарность: Гарантирует, что изменение данных и публикация сообщения происходят как единая атомарная операция.
  • Надежность: Предотвращает потерю сообщений даже при сбоях системы или брокера сообщений.
  • At-Least-Once Delivery: Обеспечивает, что сообщение будет отправлено как минимум один раз. Важно: Потребители сообщений должны быть идемпотентными, чтобы корректно обрабатывать возможные дубликаты, так как фоновый процесс может повторно отправить сообщение в случае временных сбоев.
  • Простота отладки: Все связанные данные (бизнес-данные и сообщения) находятся в одной базе данных, что упрощает отладку и мониторинг.

Минусы Outbox Pattern:

  • Дополнительная таблица в БД: Требует создания и управления дополнительной таблицей outbox.
  • Фоновый обработчик: Необходимость реализации и поддержки отдельного фонового процесса для отправки сообщений.
  • Задержка: Сообщения отправляются не мгновенно, а с небольшой задержкой, зависящей от частоты работы фонового обработчика.