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

Як узгодити події, черги й транзакції бази даних у Laravel?

Проблема: всередині транзакції відправляються події чи ставляться задачі в чергу, але транзакція ще не закомічена.

DB::transaction(function () use ($data) {
    $order = Order::create($data);
    OrderPlaced::dispatch($order);              // слухачі виконуються ЗАРАЗ
    SendInvoice::dispatch($order);              // воркер може взяти задачу ДО коміту
    $this->payments->charge($order);            // а тут виняток - усе відкочено
});

Що піде не так:

  • лист «Ваше замовлення оформлено» про замовлення, якого немає - транзакцію відкочено, а лист уже надіслано;
  • воркер не знаходить запис: задача з черги стартує до коміту, Order::find() повертає null - ModelNotFoundException;
  • зовнішні системи отримали дані, яких у базі немає (вебхук, синхронізація з CRM).

Інструменти Laravel:

  • ShouldDispatchAfterCommit на класі події - подія відправляється лише після успішного коміту (і зовсім не відправляється при відкоті);
  • ShouldHandleEventsAfterCommit на слухачі - слухач виконується після коміту;
  • afterCommit() на джобі чи after_commit => true у конфігурації з'єднання черги - задача потрапляє в чергу лише після коміту;
  • DB::afterCommit(fn () => ...) - будь-яка дія після коміту поточної транзакції.
final class OrderPlaced implements ShouldDispatchAfterCommit { /* ... */ }

SendInvoice::dispatch($order)->afterCommit();

Чого це не вирішує: «після коміту» - не те саме, що «гарантовано». Якщо процес упаде між комітом і відправкою в чергу, подія втрачена: дані в базі є, а реакції - ні. Для критичних інтеграцій (оплати, облік) потрібен transactional outbox:

  1. в тій самій транзакції, що й зміна даних, записати повідомлення в таблицю outbox;
  2. окремий процес читає outbox і відправляє повідомлення (в чергу, брокер, вебхук), позначаючи відправлені;
  3. споживачі - ідемпотентні, бо повідомлення може прийти двічі.

Так зміна даних і факт «треба повідомити» атомарні - або обидва є, або жодного.

Інші правила:

  • зовнішні виклики (HTTP, платежі) не робити всередині транзакції - транзакція тримає блокування весь час очікування відповіді, а відкотити зовнішню дію неможливо;
  • транзакція - якомога коротша і лише навколо змін бази;
  • тести: RefreshDatabase обгортає тест у транзакцію, тож «після коміту» в тестах поводиться особливо - Laravel це враховує, але сценарії з відкотом варто перевіряти явно.

Докладніше в документації: Laravel: події після транзакцій

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