Transactional Outbox паттерн используется для надежной публикации событий, когда нужно одновременно сохранить изменения в БД и отправить сообщение в брокер.

В распределенных системах нельзя объединить в транзацию сохранение данных в БД и отправку события в брокер сообщений, т.к. это 2 разных системы не объединенные общей системой транзакционности. И правило атомарности для двух операций вместе не действует.

Для решения этой проблемы нам приходит на помощь Transactional Outbox паттерн.

Проблема

Вам нужно сохранить заказ в БД и отправить сообщение об этом в RabbitMQ или Kafka.

Объединить это в транзацию как мы уже обсуждали - не получится.
Значит нужно сделать 2 отдельные атомарные операции.

Вначале сохраняем в DB, потом в Message брокер. 2 разные Атомарные операции.
Вначале сохраняем в DB, потом в Message брокер. 2 разные Атомарные операции.

Но и тут нас ждет проблема.

А что если данные сохранятся в БД, но потом произойдет сбой микросервиса, брокера сообщений или оборвется соединение?

Если не использовать Transactional Outbox паттерн. Где могут быть проблемы.
Где могут быть проблемы.

В этом случае данные в нашей распреденной системе будут неконсистентны. Т.к. повторить отправку мы уже не сможем.

Здесь нам и пригодится паттерн Transactional Outbox.

Решение

Для решения этой проблемы воспользуемся транзакционными возможностями реляционной БД. Реляционная БД поддерживает транзакции и соответствует ACID требованиям.
Самое важное требование которое нам необходимо - это (А) Атомарность.

Атомарность гарантирует что операции в транзакции выполнятся все или транзакция не будет применена вообще и произойдет rollback.

Как раз то что мы не могли сделать с двумя системами: БД и брокером сообщений.

Для реализации этого в реляционной БД нам нужно будет создать outbox таблицу.

Эта таблица необхоима для записи events которые необхоимо отправить в брокер сообщений.

Рассмотрим как это выглядит на диаграммах

transactional outbox паттерн
transactional outbox паттерн
transactional outbox паттерн диаграмма последовательности
transactional outbox паттерн диаграмма последовательности

На стороне Order Service приложения оборачиваем все в одну транзакцию: вставку в orders и создание event в outbox таблице.

BEGIN TRANSACTION;
  INSERT INTO orders (id, customer_id, total) VALUES ('order-123', 456, 1000);
  INSERT INTO outbox_events (id, aggregate_type, aggregate_id, event_type, payload)
    VALUES (uuid_generate_v4(), 'Order', 'order-123', 'OrderCreated', 
            '{"orderId": "order-123", "total": 1000}');
COMMIT;

Эта транзакция выполнится атомарно либо все, либо ничего и тогда клиент получит ошибку и будет вынужден повторить запрос.

Далее Publisher (отд. worker) берет сообщения из outbox и отправляет их в Брокер сообещний.

Publisher это отдельный сервис работающий в беграунде.

Может быть:
- cron job
- polling worker
- CDC (Change Data Capture)

Только после подтверждения что сообщение получено и сохранено, publisher идет в outbox таблицу и удаляет event.

Подводные камни

Сообщение в Message Broker может быть отправлено дважды.

Например из-за проблем:

  • с сетью (брокер сообщений получит мессадж отправит ack но сообщение не дойдет)

  • или если worker упадет в момент получения ack, но запись из outbox не успеет удалить

Для этого используйте идемпотентный ключ и обрабатывайте event на стороне Consumer - подробнее Idempotent Consumer

Где применять

Любая Event Driven architecture, где необходима гарантия доставки сообщений.

Ниже представил пример использования Outbox и CQRS вместе, для построения проекции.

CQRS подход с transactional outbox паттерном для построения проекции.
CQRS подход с transactional outbox паттерном для построения проекции.

Полезные ссылки