Как решить проблему, когда транзакция в БД выполнена, но события не были отправлены в RabbitMQ?

«Как решить проблему, когда транзакция в БД выполнена, но события не были отправлены в RabbitMQ?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Классическое решение — паттерн Transaction Outbox. В рамках одной транзакции с БД вы сохраняете не только бизнес-данные, но и событие в специальную таблицу-очередь (outbox). Затем отдельный фоновый процесс (например, воркер) забирает события из этой таблицы и гарантированно отправляет их в брокер.

Пример реализации на PHP (упрощённо):

// Внутри сервиса создания заказа
$entityManager->beginTransaction();

try {
    // 1. Сохраняем бизнес-сущность
    $order = new Order($data);
    $entityManager->persist($order);
    $entityManager->flush(); // Получаем ID

    // 2. Сохраняем событие в outbox в той же транзакции
    $outboxMessage = new OutboxMessage(
        topic: 'order.created',
        payload: json_encode(['orderId' => $order->getId()]),
        status: OutboxMessage::STATUS_PENDING
    );
    $entityManager->persist($outboxMessage);

    $entityManager->commit(); // Оба сохранения атомарны
} catch (Exception $e) {
    $entityManager->rollback();
    throw $e;
}

// 3. Отдельный воркер отправляет события из outbox
// Он периодически выбирает сообщения со статусом PENDING,
// отправляет их в RabbitMQ и помечает как отправленные.

Альтернативы и дополнения:

  • CDC (Change Data Capture): Использование инструментов вроде Debezium для отслеживания изменений в логах БД и автоматической публикации событий. Это выносит логику отправки полностью за пределы приложения.
  • Двухфазный коммит (2PC): Сложный и менее производительный подход, пытающийся сделать commit в БД и брокер атомарным. На практике используется редко.

Почему Outbox — хороший выбор:

  1. Гарантирует атомарность: Событие будет сохранено, только если основная транзакция успешна.
  2. Устраняет двойную отправку (в идеале): Воркер может использовать идемпотентность или блокировки записей в outbox.
  3. Повышает отказоустойчивость: Если RabbitMQ недоступен в момент транзакции, событие остаётся в outbox и будет отправлено позже.