Las aplicaciones monolíticas tradicionales dependían en gran medida de las propiedades ACID para mantener la integridad de los datos. Sin embargo, a medida que las aplicaciones se volvían más complejas, las deficiencias de este modelo monolítico se hacían cada vez más evidentes.
Aunque la arquitectura de microservicios abordó eficazmente muchas de estas limitaciones, también introdujo un reto importante en la gestión de las transacciones y la garantía de la coherencia de los datos en múltiples bases de datos y servicios independientes.
El patrón Saga ofrece una solución estructurada para abordar este reto. Proporciona un enfoque sistemático para gestionar transacciones a través de múltiples microservicios.
Esto resuelve las complejidades de las transacciones distribuidas y se alinea a la perfección con los principios de la arquitectura de microservicios, caracterizada por el acoplamiento flexible y la capacidad de despliegue independiente de los servicios.
¿Qué es el patrón Saga?
El patrón Saga es un patrón de diseño utilizado para gestionar transacciones y garantizar la coherencia de los datos en múltiples servicios de un sistema distribuido, especialmente en una arquitectura de microservicios.
A diferencia de las aplicaciones monolíticas tradicionales, en las que una única transacción de base de datos puede gestionar la coherencia, los microservicios suelen utilizar diferentes bases de datos, lo que dificulta el mantenimiento de la integridad de los datos en todo el sistema mediante transacciones ACID estándar.
El patrón Saga aborda este problema dividiendo una transacción en transacciones locales más pequeñas gestionadas por diferentes servicios.
Hay 3 componentes principales en el patrón Saga que necesitas conocer para entender cómo funciona: transacciones locales, transacciones de compensación y comunicación.
- Transacciones locales: Cada paso de un proceso de negocio se ejecuta como una transacción local en su servicio respectivo.
- Transacciones de compensación: Si una de las transacciones locales falla, se activan transacciones de compensación en los servicios en los que los pasos anteriores se ejecutaron con éxito. Estas transacciones compensatorias son esencialmente operaciones de deshacer, que garantizan que el sistema vuelva a un estado coherente.
- Comunicación: Los servicios se comunican entre sí a través de mensajes o eventos. Esto puede ser síncrono o, más comúnmente, asíncrono utilizando colas de mensajes o buses de eventos.
El controlador de ejecución de Saga activa estos eventos en caso de que se produzca un fallo para garantizar la estabilidad del sistema.
Cómo implementar el patrón Saga con Node.js
Hay 2 enfoques para implementar el patrón Saga: Saga Basado en Coreografía y Saga Basado en Orquestación.
- Saga basada en orquestación: Un único orquestador (arranger) gestiona todas las transacciones y dirige los servicios para ejecutar transacciones locales.
- Saga basada en coreografía: Todos los servicios que forman parte de la transacción distribuida publican un nuevo evento tras completar su transacción local.
Para este ejemplo, utilizaré el enfoque Choreography-Based
Saga y un escenario real de reserva de habitaciones de hotel con 3 microservicios:
- Servicio de Reservas
- Servicio de Pagos
- Servicio de Notificaciones.
Servicio de reserva
- Inicia el proceso reservando una habitación: Esta es la primera transacción local. Una vez realizada con éxito, envía un mensaje al Servicio de Pago para procesar el pago.
- Servicio de Pago: Recibe el mensaje y procesa el pago. Si el pago se realiza correctamente, consigna su transacción local e informa al Servicio de Reservas y al Servicio de Notificaciones.
- Servicio de notificación: Al recibir la confirmación de que el pago se ha realizado correctamente, envía un correo electrónico de confirmación de la reserva al usuario.
- Gestión de fallos: Si el servicio de pago se encuentra con un problema (por ejemplo, rechazo del pago), devuelve un mensaje de error al servicio de reserva. El Servicio de Reservas ejecuta entonces una transacción compensatoria para cancelar la reserva de la habitación, garantizando que el sistema vuelva a su estado original coherente.
Requisitos previos
- Proyecto Node.js con las dependencias necesarias (express, amqplib, nodemailer, mongoose, dotenv) instaladas.
- Servidor RabbitMQ ejecutándose local o remotamente.
- Servidor o servicio de correo electrónico para el Servicio de Notificación (por ejemplo, Nodemailer con SMTP o un servicio API de correo electrónico).
Recuerda que si quieres aprender Nodejs te dejo el CURSO o el siguiente video
Paso 1: Crear un punto final de API para iniciar reservas
// booking-service.js
const express = require('express');
const amqp = require('amqplib');
const app = express();
app.use(express.json());
// Connect to 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');
}
// Booking endpoint to reserve a room
app.post('/book', async (req, res) => {
// Save booking to the database and attempt to reserve a roomconst
booking = { /* ... */ };
// ... booking logic
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' });
}
});
// Start the server and connect to RabbitMQ
const PORT = 3000;
app.listen(PORT, async () => {
console.log(Booking Service listening on port ${PORT} );
await connectRabbitMQ();
});
Este servicio de reserva gestiona solicitudes HTTP POST para crear una nueva reserva. Intenta reservar una habitación y, si tiene éxito, envía un mensaje al Servicio de Pago a través de una cola RabbitMQ llamada 'payment_queue'
.
Paso 2: Crear un punto final de API para recibir reservas y procesar pagos
// 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());
// Insert logic to process payment
const paymentSuccess = true; // Replace with actual payment success condition
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();
El servicio de pago espera mensajes en la "cola_de_pago"
. Procesa el pago y, en función del resultado, envía un mensaje a "notification_queue"
si el pago se ha realizado correctamente o a "compensation_queue"
si el pago no se ha realizado correctamente.
Paso 3: Crear un punto final de API para escuchar los pagos correctos y enviar un correo electrónico
// 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());
// Insert logic to send email notification to the user
console.log(Sending booking confirmation for bookingId: ${booking.id} );
// Setup nodemailer transport here
// Send email...
channel.ack(msg);
});
}
connectRabbitMQ();
El Servicio de Notificación está a la escucha de los mensajes en la 'notification_queue'
. Cuando recibe un mensaje, envía un correo electrónico de confirmación al cliente.
Paso 4: Crear un servicio de compensación para gestionar las faltas
// 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());
// Insert logic to cancel the booking
console.log(Compensating transaction: cancelling bookingId: ${booking.id} );
// Update booking status in database to 'CANCELLED' or similar...
channel.ack(msg);
});
}
connectRabbitMQ();
El Servicio de Compensación está a la escucha de mensajes en la "cola_de_compensación"
. Cuando recibe un mensaje, indicando un fallo en el pago, realiza una transacción de compensación para cancelar la reserva y revertir el sistema a un estado consistente.
Ya está. Has implementado con éxito 3 microservicios con el patrón Saga.
Cada servicio realiza sus tareas y se comunica con otros servicios a través de eventos.
¿Qué ocurre si utilizo Saga basada en orquestación?
Si planea utilizar Saga basada en orquestación en lugar de Saga basada en coreografía, deberá utilizar un coordinador central para indicar a los servicios participantes qué transacciones locales deben ejecutar.
Las diferencias clave en un enfoque basado en la orquestación serían:
Un servicio Orquestador independiente gestionaría el proceso global.
Cada servicio se comunicaría con el orquestador tras completar su transacción local.
El orquestador decidiría el siguiente paso y enviaría órdenes al servicio adecuado, incluidas las transacciones compensatorias necesarias debido a un fallo.
La elección entre coreografía y orquestación suele depender de la complejidad del proceso empresarial, el grado de acoplamiento que se esté dispuesto a aceptar entre servicios y la necesidad de un control central sobre la transacción empresarial.
La coreografía está más descentralizada y requiere menos configuración, mientras que la orquestación puede proporcionar más control y es más fácil de gestionar y supervisar para sagas complejas.
Conclusión
El patrón Saga aborda eficazmente las complejidades de las transacciones distribuidas en una arquitectura de microservicios, garantizando la coherencia de los datos sin comprometer la independencia del servicio.
A medida que los microservicios siguen dominando el panorama del software, dominar el patrón Saga es esencial para crear aplicaciones robustas y resistentes.