Сервис коммитит в БД, затем падает до публикации события — БД и брокер рассинхронизированы. Найдите причину и исправьте.
Сервис заказов пишет заказ в свою базу данных, затем публикует событие OrderCreated в брокер вторым, отдельным шагом. Под нагрузкой часть заказов есть в базе, но соответствующее событие так и не дошло до брокера, и downstream-сервисы их не видят. Лог ниже показывает один такой запрос.
Ограничение: база данных и брокер — независимые системы, распределённую транзакцию между ними использовать нельзя.
12:04:01 BEGIN TX
12:04:01 INSERT orders(id=8821, status=NEW) -> ok
12:04:01 COMMIT TX -> ok
12:04:01 publish OrderCreated(id=8821) ...
12:04:02 process killed (OOM) <-- падение до ACK брокера
--- broker topic orders.events: записи для id=8821 нет ---
Определите, почему БД и брокер расходятся, и опишите исправление.
Коммит в БД и публикация в брокер — независимые записи без общей транзакции; падение между ними сохранило заказ, но потеряло событие (dual write). Лечится transactional outbox: писать событие в таблицу в той же транзакции; relay публикует его.
- ✗Считать, что повтор публикации при рестарте это чинит, хотя у сервиса нет записи, что событие не отправлено
- ✗Публиковать до коммита в БД, из-за чего выходят события для заказов, которые могут откатиться
- ✗Хвататься за распределённый two-phase commit вместо outbox в локальной транзакции
- →Как relay, читающий outbox, избегает публикации события дважды?
- →Почему чтение журнала изменений БД часто предпочтительнее опроса таблицы outbox?
Причина
Это классическая проблема двойной записи (dual write). Коммит в БД и публикация в брокер — две независимые операции без общей транзакции. Между COMMIT и publish процесс упал (OOM), поэтому заказ есть в БД, а события нет. Повтор публикации при старте не спасает: после падения сервис не помнит, что событие не ушло.
Исправление — transactional outbox
Писать событие в таблицу outbox в той же локальной транзакции, что и заказ. Тогда заказ и запись о событии коммитятся атомарно — оба или ни одного. Отдельный relay читает outbox и публикует в брокер, помечая строку как отправленную (at-least-once плюс идемпотентный потребитель на стороне читателя).
BEGIN;
INSERT INTO orders (id, status) VALUES (8821, 'NEW');
INSERT INTO outbox (id, topic, payload, published)
VALUES (gen_random_uuid(), 'orders.events', '{"orderId":8821}', false);
COMMIT;
-- relay: SELECT ... FROM outbox WHERE published = false -> publish -> UPDATE published = true
Теперь запись заказа и намерение опубликовать событие неделимы; relay гарантирует доставку даже после падения.