Что такое паттерн transaction outbox и как он решает проблему консистентности между базой и сообщениями
Что проверяют
Понимаете ли вы проблему двойной записи в микросервисах и какие гарантии паттерн даёт на самом деле. Следом почти всегда спрашивают: «А если сообщение ушло, но отметка в таблице не записалась?» Ответ ведёт к идемпотентности потребителя.
Как отвечать
Проблема: метод с @Transactional сохраняет заказ через orderRepository.save() и отправляет событие через kafkaTemplate.send(). Транзакция базы не охватывает Kafka: если отправить до коммита, а коммит откатится, в топике окажется событие о несуществующем заказе. Если отправить после коммита (например, из @TransactionalEventListener(phase = AFTER_COMMIT)), падение сервиса между двумя шагами потеряет событие. XA Kafka не поддерживает, так что общей транзакции тоже не выйдет.
Решение outbox:
- одна локальная транзакция: вместе с бизнес-данными в той же базе пишется строка в таблицу
outbox(id события, тип и id агрегата, тип события,payloadв JSONB, время создания). Либо записано всё, либо ничего; - ретранслятор: отдельный процесс публикует записи
outboxв брокер, способов два; - polling publisher: задача по
@Scheduledберёт пачку черезSELECT ... FOR UPDATE SKIP LOCKED LIMIT 100, отправляет и помечает отправленной. Просто, но добавляет задержку и нагрузку на базу; - CDC: Debezium читает WAL PostgreSQL или binlog MySQL и публикует строки outbox через Outbox Event Router, приложение о брокере не знает вовсе. В Spring похожую схему даёт реестр публикации событий Spring Modulith.
Гарантия здесь at-least-once: после сбоя между отправкой и отметкой событие уйдёт повторно. Поэтому потребитель обязан быть идемпотентным, обычно через таблицу обработанных id событий с уникальным ключом (inbox). Порядок в пределах агрегата держится, если ключ сообщения Kafka это id агрегата.
Где спотыкаются
Утверждают, что outbox даёт exactly-once. Он убирает потерю событий, но не дубли.
Запускают опрос на нескольких инстансах без SKIP LOCKED или другой блокировки: одно событие рассылается трижды, а события одного заказа приходят не по порядку.
Забывают про рост таблицы. Без очистки или партиционирования по дате outbox через полгода вырастает в самую большую таблицу схемы.