Comments 15
Хорошая статья, очень неплохо описаны подводные камни, спасибо !
Не совсем понятно как именно гарантируется согласованность - за счет чего? Со всеми подводными камнями - система может пребывать в неконсистентном состоянии неограниченное время, как и в случае без outbox. Фактически с outbox-ом мы просто можем делать много попыток доставки, через любые интервалы, надеясь что рано или поздно сообщение дойдет (если повезет). Что нам мешает делать то же самое из главного обработчика? Результат можно вернуть сразу, а фоновым потоком делать попытки доставки, записывая ошибки в лог.
Результат можно вернуть сразу, а фоновым потоком делать попытки доставки, записывая ошибки в лог
А что делать, когда сервис перезапустится?
Это уже ньюансы, можно при желании реализовать graceful shutdown где ждать завершения всех потоков. Да, при аварийном завершении все будет плохо. Но зато у нас нет дополнительных таблиц и сервисов, которые тоже могут упасть, все относительно..
Вы заблуждаетесь, кроме разве что того, что всё может упасть
Вы берете какой-то простой кейс (одна таблица, одно уведомление, и всех устраивает что запись в таблице есть, а уведомление может произойти через час или день. Это eventually transactional какой-то кейс учебный.
Берем реальный боевой пример: обновление таблиц в двух разных базах (кассандра и мсскл), уведомление внутреннего и внешнего сервисов, отправка письма пользователю. Все должно быть не eventually, а реальной транзакцией. Расскажите как вы это реализуете с outbox таблицей.
Меня всегда интересовало почему нельзя сделать коммит после успешной отправки сообщения в очередь. Если сообщение не отправится транзакция не закоммитится и будет ещё одна попытка сделать всё с начала. Так транзакция и сообщение будут отправлены только вместе.
А если сообщение отправится, а транзакция упадёт?
СУБД - это источник правды. События должны отправляться только после того, как новое состояние этой "правды" надёжно зафиксировано.
Тогда произойдет повтор. Конечно сообщение отправится повторно, но разве не это же самое происходит в outbox table когда сообщение отправляется, а запись об этом не происходит?
Ну, просто нет смысла кричать "я выиграл!", пока деньги на руки не получил. Когда получил, можно и пару раз похвастаться: доказательства-то есть.
Раньше, кстати, таким же вопросом задавался. Проще же. Но нет, не проще: последствия гораздо хуже.
PS: "Тогда произойдет повтор". Вы в этом на 100% уверены?
судя по схеме, я вижу, что в рамках транзакции предполагается писать в очередь. Является ли это антипаттерном в таком случае? Ведь есть масса проблем в таком подходе, самое банальное - это возможность повесить транзакцию на залипшей отправке в условную кафку. Можно ли обойтись без этой транзакции?
Я понимаю это так, что я беру данные из аутбокса без транзакции, начинаю их постепенно отправлять в очередь, запоминая что отправил. Допустим в середине этого этапа спотыкаюсь, и потом начинаю их удалять/помечать в аутбоксе. Споткнулся при удалении - получаю дубли как максимум, споткнулся на выборке из аутбокса - даже не начал их обрабатывать, споткнулся на очереди - удаляю что есть. Судя по рассуждениям транзакция лишняя, я прав?
А в это время другой поток вашего сервиса вычитывает те же записи и тоже шлет их в параллель... Поэтому сначала делается лок записей в БД и только если он успешен запускается рассылка
Ну или обеспечить строго однопоточную обработку что с ростом общей нагрузки на вход событий не всегда реально поддержать по вычитке с приемлемой скоростью
Вместо блокировок строк и опросов таблицы можно использовать CDC (Change Data Capture), но он тоже не лишен недостатков:
Нет подтверждения о доставке. В примере есть поле со статусом, так вот если использовать CDC, то придется слепо полагаться на его надежность, ну и мониторить его конечно.
Нет удаления записей, но можно партиционировать по датам и дропать "старые" партиции
Еще один дополнительный инфраструктурный компонент, который добавляет задержки и также может отказать.
Но из плюсов - он, например, с Postgres мимикрирует под реплику (создает слот логической репликации) и "слушает" изменения указанных в конфиге таблиц. Может в сообщении присылать состояние таблицы до выполнения операции и после, что иногда бывает удобно для отображения какой-нибудь истории изменений сущности (это уже больше не про Outbox конечно).
Два замечания:
Сценарий гарантирует что сообщение отправится в брокер, но не гарантирует что будет только одна отправка. То есть стоит добавить уникальный идентификатор сообщения.
Будьте аккуратны с запросами вроде SELECT ..WHERE ... ORDER BY .. LIMIT. PostgreSQL сначала будет искать, подходящие под условие WHERE, потом их упорядочивать и только потом выполнит LIMIT. Если под WHERE попадет большое количество записей то данный запрос может работать очень медленно и требовать больших ресурсов.
Паттерн Transactional Outbox: от теории до продакшена