🌍 Привет мир! 👋🏻
🌍 Привет мир! 👋🏻
Бэкэндеры читаем, фронты не расстраиваемся и тоже читаем, а вдруг пригодится.
И сегодня весьма важная тема из жизни небольших (как правило небольших), но гордых микросервисов. Поговорим про Transactional Outbox & Inbox patterns, эти штучки не позволят вам потерять сообщения между сервисами, а как следствие сделают ваш сон — спокойным🙂!
😮💨 Мотивация
Простенький пример, есть два сервиса:
- Сервис заказов
- Сервис оплаты
Пользователь оформляет заказ, от нас нужно два действия:
- Сохранить заказ в БД.
- Отправить событие во внешний сервис оплаты.
Возможная проблема:
🚨 Заказ сохранен, но событие по какой-то причине не отправилось
🚨 Или наоборот, событие ушло, но сервис упал и заказ в базу не сохранился
И в этот момент начинается школа жизни, пофикси илиумри (не спи)
Вы могли подумать, да че там, я вот синхронно все гоняю и зарплату получаю 💵НО надёжность таких решений стремится к нулю, при падении одного сервиса теряются все события.
А еще можно так гонять данные:
Distributed Transactions — медленные и тяжелые подходы, легко приводят к блокировкам и зависаниям всей системы (old school привет).
Отправка сообщений напрямую в брокеры (Kafka, RabbitMQ, NATS) без промежуточного хранилища — в случае падения сервиса до отправки сообщения, возникает рассинхронизация данных.
🔴 Outbox
Идея заключается в том, что вы отправляете сообщения другим сервисам не напрямую, а через специальную таблицу Outbox.
⚙️ Как работает:
✏️ Выполняется бизнес-операция, сразу после этого необходимо добавить исходящее событие в таблицу Outbox внутри текущей транзакции.
✏️ Отдельный модуль (worker, job, CDC) регулярно читает таблицу Outbox и отправляет эти события в нужные очереди, сервисы или брокеры (Kafka, RabbitMQ и т.д.).
✏️ После успешной отправки сообщение помечается как отправленное, или как завершенное, но с какой-то ошибкой.
Если транзакция по каким-то причинам откатилась, сообщение в outbox тоже не сохранится, и другие сервисы не получат некорректное событие. В итоге добиваемся атомарности и консистентности данных.
🔴 Inbox
Противоположный подход. Здесь принимаются события от других сервисов, но не обрабатываются сразу, а сначала кладутся в специальную таблицу inbox, и только затем в отдельном модуле обрабатываются.
👀 Как выглядит на практике:
Допустим, есть сервис заказов, который отправляет событие о новом заказе.
Outbox (сервис заказов):
Inbox (сервис оплаты):
Описан базовый (но вполне достаточный для существования) сценарий, при соблюдении которого будут выполнены такие важные задачи как соблюдение атомарности и консистентности данных.
🔖 Советы:
✏️ Необходимо регулярно удалять или архивировать старые события из inbox и outbox, чтобы не падала производительность и не происходил чрезмерный рост таблиц.
✏️ Если работаете с брокером сообщений, не забывайте использовать (DLE) про это я уже писал здесь. Это позволит сохранить события и проанализировать ошибки позже, не теряя информацию.
💬 Делитесь своим мнением в комментариях👇! Если вам понравилась статья, не забудьте поставить лайк! 👍
#PATTERNS #ARCHITECTURE
Бэкэндеры читаем, фронты не расстраиваемся и тоже читаем, а вдруг пригодится.
И сегодня весьма важная тема из жизни небольших (как правило небольших), но гордых микросервисов. Поговорим про Transactional Outbox & Inbox patterns, эти штучки не позволят вам потерять сообщения между сервисами, а как следствие сделают ваш сон — спокойным🙂!
😮💨 Мотивация
Простенький пример, есть два сервиса:
- Сервис заказов
- Сервис оплаты
Пользователь оформляет заказ, от нас нужно два действия:
- Сохранить заказ в БД.
- Отправить событие во внешний сервис оплаты.
Возможная проблема:
🚨 Заказ сохранен, но событие по какой-то причине не отправилось
🚨 Или наоборот, событие ушло, но сервис упал и заказ в базу не сохранился
И в этот момент начинается школа жизни, пофикси или
Вы могли подумать, да че там, я вот синхронно все гоняю и зарплату получаю 💵НО надёжность таких решений стремится к нулю, при падении одного сервиса теряются все события.
А еще можно так гонять данные:
Distributed Transactions — медленные и тяжелые подходы, легко приводят к блокировкам и зависаниям всей системы (old school привет).
Отправка сообщений напрямую в брокеры (Kafka, RabbitMQ, NATS) без промежуточного хранилища — в случае падения сервиса до отправки сообщения, возникает рассинхронизация данных.
🔴 Outbox
Идея заключается в том, что вы отправляете сообщения другим сервисам не напрямую, а через специальную таблицу Outbox.
⚙️ Как работает:
✏️ Выполняется бизнес-операция, сразу после этого необходимо добавить исходящее событие в таблицу Outbox внутри текущей транзакции.
✏️ Отдельный модуль (worker, job, CDC) регулярно читает таблицу Outbox и отправляет эти события в нужные очереди, сервисы или брокеры (Kafka, RabbitMQ и т.д.).
✏️ После успешной отправки сообщение помечается как отправленное, или как завершенное, но с какой-то ошибкой.
Если транзакция по каким-то причинам откатилась, сообщение в outbox тоже не сохранится, и другие сервисы не получат некорректное событие. В итоге добиваемся атомарности и консистентности данных.
🔴 Inbox
Противоположный подход. Здесь принимаются события от других сервисов, но не обрабатываются сразу, а сначала кладутся в специальную таблицу inbox, и только затем в отдельном модуле обрабатываются.
👀 Как выглядит на практике:
Допустим, есть сервис заказов, который отправляет событие о новом заказе.
Outbox (сервис заказов):
// Cохранение заказа и события
await transaction(async (tx) => {
await tx.save(order);
await tx.save(outboxTable, {
eventType: 'ORDER_CREATED',
payload: order
});
});
// Воркер отправки событий
async function processOutbox() {
const events = await outboxTable.getUnprocessed();
for (const event of events) {
await eventBus.publish(event.eventType, event.payload);
await outboxTable.markProcessed(event.id);
}
}
Inbox (сервис оплаты):
// Получение и сохранение события
app.post('/events', async (req, res) => {
await inboxTable.insert({
eventType: req.body.type,
payload: req.body.payload
});
res.status(200).send('Accepted');
});
// Обработка Inbox
async function processInbox() {
const events = await inboxTable.getUnprocessed();
for (const event of events) {
try {
await handlePayment(event.payload);
await inboxTable.markProcessed(event.id);
} catch (err) {
console.error(err);
}
}
}Описан базовый (но вполне достаточный для существования) сценарий, при соблюдении которого будут выполнены такие важные задачи как соблюдение атомарности и консистентности данных.
🔖 Советы:
✏️ Необходимо регулярно удалять или архивировать старые события из inbox и outbox, чтобы не падала производительность и не происходил чрезмерный рост таблиц.
✏️ Если работаете с брокером сообщений, не забывайте использовать (DLE) про это я уже писал здесь. Это позволит сохранить события и проанализировать ошибки позже, не теряя информацию.
💬 Делитесь своим мнением в комментариях👇! Если вам понравилась статья, не забудьте поставить лайк! 👍
#PATTERNS #ARCHITECTURE

Хотите больше таких постов?
Подпишитесь на канал и читайте продолжение в Telegram.