Мы начали новый проект, и одному из сервисов требовалось:
1️⃣ принимать поток данных
2️⃣ сохранять их в БД
3️⃣ некоторые из них отправлять дальше в Кафку
Идёт обсуждение сервиса. Терять данные нельзя. Как быть?
🤔 Допустим, мы будем сохранять данные в БД, комитить транзакцию, а потом отправлять в Кафку. Но если приложение вдруг упадет, не успев отправить данные, то окажется, что данные в БД есть, а дальше мы их не отправили. Так нельзя.
🤔 Ок, тогда давайте не будем закрывать транзакцию после сохранения в БД, а оставим её открытой пока не отправим в Кафку. Если с отправкой все хорошо — закоммитим транзакцию. А если брокер будет недоступен и нужны повторные попытки? Будем удерживать транзакцию? Плохо.
🤔 А если уже после отправки в момент коммита транзакции произойдет ошибка в БД, и транзакция откатится? Получается мы данные отправим, а себе не сохраним. Так тоже нельзя.
Пазл не складывается. Я понимаю, что наверняка не мы первые столкнулись с такой задачей и у меня в памяти всплывает, что есть паттерн как раз для этого. Вспоминаю суть, но не могу вспомнить название!
Свежеиспечённому лиду не пристало ударять лицом в грязь и нужно срочно выдать решение 🙈 Начинаю нервничать и от этого никак не могу поймать ускользающее название. Что-то с транзакциями и таблица там специальная.
Ситуацию осложняет то, что в тот момент я была знакома с этим паттерном в теории, но ещё не использовала его на практике.
Как же его... как он называется? Погуглить паттерн для отправки в Кафку с отдельной таблицей?
— Transactional Outbox! — выпаливаю я.
— Точно! — подхватывает коллега. — Мы как раз его на прошлом проекте использовали, подойдет для нашей ситуации.
Фууух, ура!
Потом я конечно поняла, что лид совсем не обязан всё знать и не нужно так переживать, но это уже совсем другая история 😉
Кратко, в чем идея паттерна Transactional Outbox?
У нас есть бизнес-таблица (например,
orders) и отдельная таблица outbox.➡️ В одной транзакции мы пишем данные и в таблицу
orders, и в outbox. Записав, комитим транзакцию.➡️ Отдельный процесс (например, джоба по расписанию или change data capture) читает
outbox и отправляет сообщение в брокер.➡️ После успешной отправки сообщение помечается как обработанное (может потом удаляться).
Таким образом, если при сохранении данных что-то пойдет не так, и транзакция откатится, то эти данные никак в брокер не попадут.
Если же мы всё сохраним, а потом приложение упадет, не успев отправить данные — ничего страшного 😊 Всё, что нужно отправить, будет дожидаться в таблице
outbox. Отправим после восстановления.Таблица
outbox может содержать как уже готовое к отправке сообщение "payload", так и набор полей, а также поле со статусом.Паттерн применим не только при создании записей, но и при изменении/удалении, если об этом нужно отправлять событие.
Реализация паттерна даёт at-least-once доставку, то есть сообщение не потеряется, но возможны дубликаты.
С тех пор мы успешно используем Transactional Outbox на проектах.
🤔 А зачем отдельная таблица? Можно же сделать поле со статусом в таблице
orders: NEW, PROCESSED. И пусть фоновый процесс берет данные из самой orders и меняет статус после отправки.Да, так можно сделать, это проще. Но у такого подхода есть минусы:
➖ мы смешаем бизнес-логику (данные
orders) с логикой интеграции: статус отправки, timestamp отправки, другая служебная информация, которая не нужна в orders. Отдельная таблица outbox дает больше гибкости.➖
outbox-таблицу можно чистить (сразу или спустя время), а добавленные в orders статус (дата и др.) отправки останутся там✏️ Паттерн Transactional Outbox решает так называемую dual-write problem — как записать данные в базу и одновременно отправить событие наружу так, чтобы они не разъехались. С ним мы можем гарантировать, что сообщение будет отправлено после успешного выполнения транзакции в базе данных и не потеряется. Cхема в комментах.
_______
Сталкивались ли вы с Transactional Outbox на практике? Делали по классике или через статусы отправки в основной таблице?