Реализация шаблона Saga в микросервисах с помощью Node.js

Chistyakov V
NOP::Nuances of Programming
6 min readMay 28, 2024

Целостность данных в монолитных приложениях во многом определяется свойствами атомарности, непротиворечивости, изолированности и долговечности (Atomicity, Consistency, Isolation, Durability — ACID). При этом по мере усложнения приложений недостатки монолитной модели становятся все более очевидными. Эффективно устранить многие из них позволяет микросервисная архитектура. Однако она также создает и серьезные проблемы для управления транзакциями и согласованности данных между независимыми базами данных и сервисами.

Структурированное решение этой проблемы предоставляет шаблон Saga. Он предлагает системный подход к управлению транзакциями между несколькими микросервисами. При этом устраняются проблемы распределенных транзакций в соответствии с принципами архитектуры микросервисов, характеризующейся слабой связностью и возможностью независимого развертывания сервисов.

Что такое шаблон Saga?

Saga — это шаблон проектирования, используемый для управления транзакциями и обеспечивающий согласованность данных между отдельными сервисами в распределенной системе, особенно в архитектуре микросервисов. В отличие от традиционных монолитных приложений, где транзакции выполняются в единой базе данных, не нарушая их согласованность, микросервисы часто обращаются к разным базам данных, что усложняет поддержание целостности данных во всей системе с использованием стандартных транзакций ACID. Шаблон Saga решает эту проблему, разбивая транзакцию на более мелкие локальные составляющие, обрабатываемые различными сервисами.

Шаблон включает три основных компонента: локальные транзакции, компенсационные транзакции и коммуникации. Они позволяют понять особенности работы Saga.

  • Локальные транзакции. Каждый шаг бизнес-процесса выполняется как локальная транзакция в соответствующем сервисе.
  • Компенсационные транзакции. Если одна из локальных транзакций завершается неудачно, компенсационные транзакции запускаются в тех службах, где предыдущие шаги были успешно выполнены. Компенсирующие транзакции — это по сути операции отмены, гарантирующие возврат системы в согласованное состояние.
  • Коммуникации. Сервисы взаимодействуют между собой посредством сообщений или событий. Это может происходить синхронно, но чаще асинхронно с использованием очередей сообщений или шин событий. В случае сбоя/отказа для обеспечения стабильности системы контроллер выполнения (Saga Execution Controller) запускает эти события.

Как реализовать шаблон Saga с помощью Node.js

Существует два подхода к реализации шаблона «Saga»: «Saga на основе хореографии» и «Saga на основе оркестрации».

  • Saga на основе оркестрации: один оркестратор (аранжировщик) управляет всеми транзакциями и сервисами для выполнения локальных транзакций.
  • Saga на основе хореографии: все сервисы, являющиеся частью распределенной транзакции, публикуют новое событие после завершения своей локальной транзакции.

В этом примере использован подход «Saga на основе хореографии» и реальный сценарий бронирования номеров в отеле с тремя микросервисами: бронирования, оплаты и уведомлений (Booking Service, Payment Service и Notification Service).

  • Booking Service. Запускается процесс резервирования номера. Это первая локальная транзакция. После подтверждения оплаты отправляется сообщение для обработки платежа в Payment Service.
  • Payment Service. Прием сообщения и обработка платежа. Если платеж прошел успешно, совершается локальная транзакция с информированием сервисов бронирования и уведомлений (Booking Service и Notification Service).
  • Notification Service. После получения подтверждения об успешной оплате, сервис отправляет пользователю электронное письмо с подтверждением бронирования.
  • Обработка ошибок. Если Payment Service обнаружит проблемы (например, отказ в проведении платежа), он возвращает в Booking Service сообщение об ошибке. После этого Booking Service выполняет компенсирующую транзакцию для отмены бронирования номера, обеспечивающую возврат системы в исходное согласованное состояние.

Необходимые условия

  • Проект Node.js с установленными зависимостями (express, amqplib, nodemailer, mongoose и dotenv).
  • Сервер RabbitMQ, работающий локально или удаленно.
  • Сервер электронной почты или сервис для Notification Service (например, Nodemailer с SMTP или сервисом API электронной почты).

Шаг 1. Создание конечной точки API для запуска процесса бронирования

// booking-service.js

const express = require('express');
const amqp = require('amqplib');
const app = express();
app.use(express.json());
//Подключение к RabbitMQ
const rabbitUrl = 'amqp://localhost';
let channel;
async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('payment_queue');
}
// Конечная точка для бронирования номера
app.post('/book', async (req, res) => {
//Сохранение результатов бронирование в базе данных и попытка резервирования номера.

booking = { /* ... */ };
// ... логика бронирования
if (bookingReservedSuccessfully) {
await publishToQueue('payment_queue', booking);
return res.status(200).json({ message: 'Booking initiated', booking });
} else {
return res.status(500).json({ message: 'Booking failed' });
}
});
//Запуск сервера и подключение к RabbitMQ
const PORT = 3000;
app.listen(PORT, async () => {
console.log(Booking Service listening on port ${PORT} );
await connectRabbitMQ();
});

В процессе нового бронирования Booking Service обрабатывает запросы HTTP POST. Сервис пытается зарезервировать номер и в случае успеха отправляет сообщение в Payment Service через очередь сообщений (Payment_queue) RabbitMQ.

Шаг 2. Создание конечной точки API для прослушивания бронирований и обработки платежей

// payment-service.js
const amqp = require('amqplib');
const rabbitUrl = 'amqp://localhost';
let channel;

async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('notification_queue');
await channel.assertQueue('compensation_queue');
channel.consume('payment_queue', async (msg) => {
const booking = JSON.parse(msg.content.toString());
//Вставка логоки оформления платежа
const paymentSuccess = true; //Замена актуальным условием успешного платежа
if (paymentSuccess) {
await channel.sendToQueue('notification_queue', Buffer.from(JSON.stringify(booking)));
} else {
await channel.sendToQueue('compensation_queue', Buffer.from(JSON.stringify(booking)));
}
channel.ack(msg);
});
}
connectRabbitMQ();

Payment Service прослушивает сообщения в очереди Payment_queue и обрабатывает платеж. Затем в зависимости от результата либо отправляет сообщение в notification_queue (если платеж прошел), либо в compensation_queue (если платеж не выполнен).

Шаг 3. Создание конечной точки API для отслеживания успешных платежей и отправки электронной почты.

// notification-service.js
const amqp = require('amqplib');
const nodemailer = require('nodemailer');

const rabbitUrl = 'amqp://localhost';
let channel;
async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('notification_queue');
channel.consume('notification_queue', async (msg) => {
const booking = JSON.parse(msg.content.toString());
//Вставка логики для отправки уведомления пользователю по электронной почте.
console.log(Sending booking confirmation for bookingId: ${booking.id} );
//Настройка транспорта nodemailer
//Отправка электронного соообщения ...
channel.ack(msg);
});
}
connectRabbitMQ();

Notification Service прослушивает сообщения из очереди notification_queue. Получив сообщение он отправляет клиенту подтверждение по электронной почте.

Шаг 4. Создание сервиса компенсации для обработки ошибок

// compensation-service.js
const amqp = require('amqplib');

const rabbitUrl = 'amqp://localhost';
let channel;
async function connectRabbitMQ() {
const connection = await amqp.connect(rabbitUrl);
channel = await connection.createChannel();
await channel.assertQueue('compensation_queue');
channel.consume('compensation_queue', async (msg) => {
const booking = JSON.parse(msg.content.toString());
//Вставка логики для отмены бронирования
console.log(Compensating transaction: cancelling bookingId: ${booking.id} );
//Обновляем статус бронирования в базе данных на «CANCELLED» или подобное...
channel.ack(msg);
});
}
connectRabbitMQ();

Сервис компенсации прослушивает сообщения в compensation_queue. Получив сообщение, указывающее на сбой платежа, он выполняет компенсирующую транзакцию для отмены бронирования и возврата системы в согласованное состояние.

Вы успешно реализовали 3 микросервиса с помощью шаблона Saga. Каждый из них выполняет свои задачи и взаимодействует с другими сервисами через события.

Шаблон Saga и метод оркестрации

При использовании Saga на основе оркестрации (вместо Saga + хореография) необходим центральный координатор, сообщающий участвующим сервисам, какие локальные транзакции нужно выполнять.

Главные отличия шаблона Saga c методом оркестрации:

  • Всем процессом управляет отдельный сервис Orchestrator.
  • Каждый сервис взаимодействует с Оркестратором после завершения локальной транзакции.
  • Оркестратор принимает решение о выполнении очередного шага и отправляет соответствующему сервису команды, включая любые компенсирующие транзакции, необходимые в случае отказа.

Выбор между хореографией и оркестрацией часто определяется сложностью бизнес-процесса, задаваемой степенью взаимосвязи между сервисами и необходимостью централизованного контроля над бизнес-транзакциями. Более централизованный метод хореографии требует меньше настроек, а оркестрация может обеспечить больше контроля, упрощает управление и мониторинг сложных шаблонов Saga.

Заключение

В архитектуре микросервисов шаблон Saga позволяет эффективно решать проблемы распределенных транзакций, обеспечивая согласованность данных между независимыми сервисами. Грамотное применение шаблона Saga необходимо для создания надежных и отказоустойчивых приложений с микросервисами, которые сегодня наиболее востребованы.

Читайте также:

Читайте нас в Telegram, VK и Дзен

--

--