Как обеспечить консистентность между обновлением статуса заказа в базе и отправкой уведомления в Kafka
Что проверяют
Знаете ли вы проблему двойной записи: база и Kafka не участвуют в одной транзакции, и любой порядок «сначала одно, потом другое» где-то теряет данные. Ожидают transactional outbox, а следом спрашивают, как не получить дубли и сохранить порядок событий.
Как отвечать
Сначала покажите, почему наивные варианты не работают. Отправка в Kafka внутри метода с @Transactional: сообщение ушло, транзакция откатилась, потребители видят статус, которого нет. Отправка после коммита, в том числе через @TransactionalEventListener(phase = AFTER_COMMIT): коммит прошёл, процесс упал до отправки, событие потеряно. Распределённые XA-транзакции Kafka не поддерживает.
Решение:
- таблица outbox: в той же транзакции, что и
UPDATE orders, делаетсяINSERTвoutboxс идентификатором события,aggregate_id, типом и JSON-телом. Записалось либо всё, либо ничего; - релей: отдельный процесс переносит записи в Kafka. Простой вариант это задача по
@Scheduled, которая берёт пачку черезSELECT ... FOR UPDATE SKIP LOCKED, отправляет и помечает отправленными. Без опроса базы работает CDC: Debezium читает журнал базы и публикует события, для этого у него есть готовый Outbox Event Router; - подтверждение отправки:
KafkaTemplate.send()асинхронный и возвращаетCompletableFuture. Помечать запись отправленной можно только после ответа брокера, приacks=all; - порядок: ключ сообщения это
orderId, тогда все события заказа попадут в одну партицию; - идемпотентность потребителя: релей может упасть между отправкой и пометкой, и событие уйдёт дважды. Потребитель хранит обработанные
event_idили делает операцию идемпотентной по сути.
Итоговая гарантия честно называется at-least-once плюс дедупликация, а не exactly-once.
Где спотыкаются
Отправляют в Kafka и сразу удаляют запись из outbox, не дождавшись ответа брокера. При сбое сети событие исчезает, и outbox теряет смысл.
Обещают exactly-once из-за enable.idempotence=true у продюсера. Он убирает дубли от ретраев самого продюсера, но не от повторного прохода релея.
Забывают чистить outbox. Таблица растёт миллионами строк, и выборка неотправленных без индекса начинает тормозить.