Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Що таке патерн Transactional Outbox і яку проблему він розв'язує?

Проблема подвійного запису. Сервіс має зробити дві речі: зберегти зміну в базі і опублікувати подію в брокер повідомлень. Це дві різні системи, і атомарно зробити обидві дії неможливо:

DB::transaction(fn () => $order->markPaid());
$broker->publish(new OrderPaid($order->id));   // а якщо брокер недоступний чи процес впав тут?
  • база оновилася, подія не опублікована - інші сервіси ніколи не дізнаються про оплату;
  • якщо спершу публікувати, а потім зберігати, - подія є, а зміна в базі відкотилася.

Transactional Outbox: подія записується в таблицю тієї ж бази, у тій самій транзакції, що й зміна даних:

DB::transaction(function () use ($order) {
    $order->markPaid();

    DB::table('outbox')->insert([
        'id' => (string) Str::uuid7(),
        'type' => 'order.paid',
        'payload' => json_encode(['order_id' => $order->id]),
        'created_at' => now(),
    ]);
});

Тепер зміна і запис про подію або збережені обидва, або жоден.

Окремий процес-ретранслятор (relay) читає таблицю outbox і публікує записи в брокер, позначаючи опубліковані:

  • опитування таблиці - простий варіант (команда в планувальнику чи постійний воркер);
  • читання журналу змін бази (CDC, наприклад Debezium) - без навантаження опитуванням, з мінімальною затримкою.

Що варто врахувати:

  • at-least-once: ретранслятор може опублікувати подію, впасти до позначки «опубліковано» і опублікувати знову. Споживачі мають бути ідемпотентними - ідентифікатор запису outbox стає ідентифікатором повідомлення;
  • порядок: публікувати в порядку створення, якщо споживачам важлива послідовність;
  • очищення: опубліковані записи видаляти чи архівувати;
  • моніторинг затримки: записи, що висять неопублікованими, - сигнал проблеми з брокером чи ретранслятором.

Аналог у межах Laravel-застосунку: джоба, поставлена в чергу database у тій самій транзакції, або afterCommit() / ShouldDispatchAfterCommit - щоб подія й джоба відправлялися лише після успішного коміту. Але afterCommit не захищає від падіння процесу між комітом і відправкою в зовнішню чергу - Outbox захищає.

Дзеркальний патерн на стороні споживача - Inbox: записувати отримані повідомлення в таблицю для дедуплікації й надійної обробки.

Докладніше в документації: Microservices.io: Transactional Outbox

Схожі питання