Была ли важна атомарность изменений в хранилище и отправки сообщений
Что проверяют
Знаете ли вы проблему двойной записи: база и брокер это две разные системы, и общей транзакции у них нет. Интервьюер ждёт, что вы назовёте transactional outbox и объясните, почему «отправим внутри транзакции и откатим при ошибке» не работает. Следом спрашивают про дубли и идемпотентность потребителя.
Как отвечать
Отвечайте на своём примере: что меняли в базе, какое событие отправляли, что сломалось бы при расхождении. Если такой задачи не было, скажите честно и разберите типовой кейс «заказ создан, событие ушло в Kafka».
Сначала покажите, почему наивные варианты ломаются:
- отправка после
Commit: процесс упал между коммитом и публикацией, данные есть, события нет; - отправка внутри транзакции: сообщение ушло, а
Commitупал, или брокер ответил таймаутом, хотя сообщение принял. К тому же транзакция держится открытой на время сетевого вызова.
Рабочее решение, transactional outbox:
- в одной транзакции
db.BeginTx(ctx, nil)пишем бизнес-строку и строку в таблицуoutbox(тип события, ключ, payload, время); - отдельный воркер выбирает пачки через
SELECT ... FOR UPDATE SKIP LOCKED, публикует и помечает строки отправленными. Вместо поллинга можно читать WAL через CDC, например Debezium; - доставка получается at-least-once, поэтому потребитель обязан быть идемпотентным: хранит id обработанных сообщений или пишет через upsert;
- порядок событий одной сущности держится ключом партиции в Kafka.
Упомяните альтернативы: 2PC между базой и брокером на практике не используют, а транзакции Kafka атомарны только внутри самой Kafka.
Где спотыкаются
Считают, что транзакция базы покрывает и отправку. Откатить уже доставленное сообщение нельзя.
Делают outbox, но забывают про дубли. Воркер может опубликовать сообщение и упасть до пометки строки, после рестарта оно уйдёт повторно.
Не думают про рост таблицы outbox: без очистки отправленных строк и индекса по статусу воркер через месяц сканирует миллионы записей.