Transacciones Distribuidas - Patrones de diseño para mantener la consistencia

15 min de lectura | 2024.12.25

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

ProblemaDescripción
BloqueoLos participantes mantienen bloqueos mientras esperan la respuesta del coordinador
Punto único de falloSi el coordinador cae, todo el sistema se detiene
Bajo rendimientoRequiere comunicación síncrona
Limitaciones en entornos distribuidosDifí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

AspectoOrquestaciónCoreografía
VisibilidadFlujo claroFlujo distribuido
AcoplamientoDepende del orquestadorServicios débilmente acoplados
ComplejidadLógica simpleDiseño de eventos complejo
DepuraciónFácilDifí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ónMétodo de compensación
Envío de emailEnviar “email de cancelación”
Inicio de trabajo físicoRequiere intervención manual
Llamada a API externaRequiere 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 consistenciaDescripción
Consistencia fuerteInmediatamente después de escribir, todos ven los mismos datos
Consistencia eventualDespué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