Transactional Outbox паттерн в Event Driven системах

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

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

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

Проблема

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

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

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

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

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

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

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

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

Решение

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

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

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

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

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

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

На стороне API приложения оборачиваем все в одну транзакцию: вставку в 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 не успеет удалить

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

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

Любая Event Driven Architecture

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

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

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


Андрей Писаревский

Автор: Андрей Писаревский — Team Lead

Коммерческий опыт в программировании с 2010 года и экспертиза в полном цикле веб разработки: Frontend, Backend, QA, CI/CD, управление крупными командами и Enterprise проектами.

А так-же открыт к предложениям о работе со стеком PHP/Golang.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *