Desafíos de las Transacciones Distribuidas
En microservicios, una operación de negocio puede abarcar múltiples servicios. Dado que cada servicio tiene su propia base de datos, las transacciones ACID tradicionales no se pueden utilizar.
flowchart LR
Order["1. Servicio de Pedidos<br/>Crear pedido"]
Inventory["2. Servicio de Inventario<br/>Reducir stock"]
Payment["3. Servicio de Pagos<br/>Procesar pago"]
Shipping["4. Servicio de Envíos<br/>Programar envío"]
Order --> Inventory --> Payment --> Shipping
Question["❓ ¿Qué pasa si algo falla?<br/>¿Hacer rollback de todo?"]
Commit de Dos Fases (2PC)
Un protocolo donde todos los participantes confirman que están listos antes de hacer commit simultáneamente.
sequenceDiagram
participant C as Coordinador
participant A as Participante A
participant B as Participante B
C->>A: 1. Prepare
C->>B: 1. Prepare
A-->>C: 2. Ready
B-->>C: 2. Ready
C->>A: 3. Commit
C->>B: 3. Commit
A-->>C: 4. Done
B-->>C: 4. Done
Problemas del 2PC
| Problema | Descripción |
|---|---|
| Bloqueo | Los participantes mantienen bloqueos mientras esperan la respuesta del coordinador |
| Punto único de fallo | Si el coordinador cae, todo el sistema se detiene |
| Bajo rendimiento | Requiere comunicación síncrona |
| Limitaciones en entornos distribuidos | Difícil manejar particiones de red |
El 2PC no se recomienda en microservicios modernos
Patrón Saga
Divide transacciones de larga duración en una serie de transacciones locales. En caso de fallo, se ejecutan transacciones compensatorias para hacer rollback.
flowchart LR
subgraph Normal["Flujo Normal"]
T1["T1<br/>Crear pedido"] --> T2["T2<br/>Reservar stock"] --> T3["T3<br/>Pago"] --> T4["T4<br/>Programar envío"]
end
flowchart LR
subgraph Failure["En caso de fallo (fallo en T3)"]
T1b["T1"] --> T2b["T2"] --> T3b["T3<br/>❌ Fallo"] --> C2["C2<br/>Devolver stock"] --> C1["C1<br/>Cancelar pedido"]
end
Método de Orquestación
Un orquestador central controla la Saga.
class OrderSaga {
async execute(orderData) {
const sagaId = uuid();
let currentStep = 0;
try {
// Step 1: Crear pedido
const order = await orderService.create(orderData);
currentStep = 1;
// Step 2: Reservar stock
await inventoryService.reserve(order.items);
currentStep = 2;
// Step 3: Pago
await paymentService.process(order.total);
currentStep = 3;
// Step 4: Programar envío
await shippingService.schedule(order);
currentStep = 4;
return { success: true, orderId: order.id };
} catch (error) {
// Ejecutar transacciones compensatorias
await this.compensate(currentStep, order);
throw error;
}
}
async compensate(step, order) {
if (step >= 3) await paymentService.refund(order.id);
if (step >= 2) await inventoryService.release(order.items);
if (step >= 1) await orderService.cancel(order.id);
}
}
Método de Coreografía
Cada servicio publica y suscribe eventos para coordinarse.
flowchart TB
subgraph Success["Flujo Normal"]
OS1["Servicio de Pedidos"] --> E1["OrderCreated"]
E1 --> IS1["Servicio de Inventario"] --> E2["InventoryReserved"]
E2 --> PS1["Servicio de Pagos"] --> E3["PaymentProcessed"]
E3 --> SS1["Servicio de Envíos"] --> E4["ShippingScheduled"]
end
flowchart TB
subgraph Failure["En caso de fallo"]
PS2["Servicio de Pagos"] --> EF1["PaymentFailed"]
EF1 --> IS2["Servicio de Inventario"] --> EF2["InventoryReleased<br/>(compensación)"]
EF2 --> OS2["Servicio de Pedidos"] --> EF3["OrderCancelled<br/>(compensación)"]
end
Comparación
| Aspecto | Orquestación | Coreografía |
|---|---|---|
| Visibilidad | Flujo claro | Flujo distribuido |
| Acoplamiento | Depende del orquestador | Servicios débilmente acoplados |
| Complejidad | Lógica simple | Diseño de eventos complejo |
| Depuración | Fácil | Difícil |
Transacciones Compensatorias
En lugar de hacer rollback en caso de fallo, se ejecutan operaciones inversas.
// Operación original
async function reserveInventory(items) {
for (const item of items) {
await db.inventory.update({
where: { productId: item.productId },
data: { quantity: { decrement: item.quantity } }
});
}
}
// Transacción compensatoria
async function releaseInventory(items) {
for (const item of items) {
await db.inventory.update({
where: { productId: item.productId },
data: { quantity: { increment: item.quantity } }
});
}
}
Consideraciones sobre Compensación
No todas las operaciones son compensables:
| Operación | Método de compensación |
|---|---|
| Envío de email | Enviar “email de cancelación” |
| Inicio de trabajo físico | Requiere intervención manual |
| Llamada a API externa | Requiere API de compensación del sistema externo |
Patrón Outbox
Garantiza la actualización de base de datos y la publicación de eventos de manera confiable.
// 1. Ejecutar ambos dentro de una transacción
async function createOrder(orderData) {
await db.transaction(async (tx) => {
// Guardar datos de negocio
const order = await tx.orders.create(orderData);
// Guardar evento en la tabla outbox
await tx.outbox.create({
eventType: 'OrderCreated',
payload: JSON.stringify(order),
status: 'pending'
});
});
}
// 2. Proceso separado hace polling del outbox
async function publishOutboxEvents() {
const events = await db.outbox.findMany({
where: { status: 'pending' }
});
for (const event of events) {
await messageQueue.publish(event.eventType, event.payload);
await db.outbox.update({
where: { id: event.id },
data: { status: 'published' }
});
}
}
Consistencia Eventual
Se acepta que la consistencia no sea inmediata, sino que eventualmente se alcanzará.
| Modelo de consistencia | Descripción |
|---|---|
| Consistencia fuerte | Inmediatamente después de escribir, todos ven los mismos datos |
| Consistencia eventual | Después de escribir, eventualmente todos verán los mismos datos (se tolera inconsistencia temporal) |
Manejo de Consistencia Eventual
// Ejemplo de manejo en la UI
async function placeOrder(orderData) {
const result = await api.createOrder(orderData);
// Mostrar como "procesando" en lugar de confirmado inmediatamente
return {
orderId: result.orderId,
status: 'processing',
message: 'Hemos recibido su pedido. Le notificaremos por email cuando esté confirmado.'
};
}
Resumen
Las transacciones distribuidas son un desafío difícil en la arquitectura de microservicios. El 2PC no es adecuado para sistemas distribuidos modernos; se recomiendan el patrón Saga y las transacciones compensatorias. Es importante aceptar la consistencia eventual y seleccionar el modelo de consistencia apropiado para los requisitos del negocio.
← Volver a la lista