Какой архитектурный паттерн обеспечивает атомарную запись в базу данных и отправку сообщения в очередь?

«Какой архитектурный паттерн обеспечивает атомарную запись в базу данных и отправку сообщения в очередь?» — вопрос из категории Архитектура, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для решения этой задачи используется паттерн Transactional Outbox. Его суть — избежать несогласованного состояния, когда операция в базе данных прошла успешно, а отправка сообщения в очередь — нет (или наоборот).

Принцип работы

  1. Единая транзакция: В рамках одной транзакции с основной бизнес-логикой (например, создание заказа) запись о событии помещается в специальную таблицу в той же базе данных — outbox.
  2. Асинхронный отправитель: Отдельный независимый процесс (реле, poller) периодически опрашивает таблицу outbox на наличие необработанных записей.
  3. Доставка и подтверждение: Процесс-отправитель читает событие из таблицы, отправляет его в брокер сообщений (Kafka, RabbitMQ) и только после успешного подтверждения от брокера помечает запись в outbox как обработанную (или удаляет ее).

Пример реализации (псевдокод на Python)

# Шаг 1: Основная бизнес-логика
def create_order(order_data):
    # Начало транзакции
    with db.atomic_transaction():
        # 1. Сохраняем заказ в основной таблице
        new_order = Order.create(**order_data)

        # 2. Создаем запись о событии в таблице outbox
        OutboxMessage.create(
            topic='orders.created',
            payload={'order_id': new_order.id, 'customer_id': new_order.customer_id}
        )
    # Конец транзакции. Либо обе записи сохранятся, либо ни одна.

# Шаг 2: Отдельный процесс-отправитель
def process_outbox_messages():
    while True:
        messages = OutboxMessage.get_unsent(limit=100)
        for msg in messages:
            try:
                # 3. Отправляем сообщение в брокер
                message_broker.publish(topic=msg.topic, body=msg.payload)

                # 4. Помечаем как отправленное
                msg.mark_as_sent()
            except Exception as e:
                # Логируем ошибку, сообщение будет обработано в следующий раз
                log_error(f"Failed to send message {msg.id}: {e}")
        time.sleep(5) # Пауза между опросами

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

  • Надежность: Гарантирует, что ни одно событие не будет потеряно при сбое одной из систем.
  • Разделение ответственностей: Бизнес-логика не зависит от доступности брокера сообщений.
  • Согласованность данных: Обеспечивает целостность данных между локальной БД и внешними системами.