Capítulo 1: Introducción a Transacciones Distribuidas
Capítulo 1: Introducción a Transacciones Distribuidas
“En sistemas distribuidos, la consistencia requiere coordinación”
Antes de Comenzar: Conceptos Fundamentales
Antes de adentrarnos en las transacciones distribuidas, es importante entender algunos conceptos básicos que usaremos a lo largo de este tutorial.
¿Qué es una Transacción?
Una transacción es un conjunto de operaciones que se ejecutan como una unidad indivisible. Imagina que vas al banco a transferir dinero de tu cuenta de ahorro a tu cuenta corriente. Esta operación involucra dos pasos: restar dinero de una cuenta y sumarlo a otra. Si solo se ejecutara el primer paso (restar), perderías dinero. Por eso, ambas operaciones deben completarse juntas o ninguna debe ejecutarse. Eso es una transacción.
¿Qué significa “Distribuido”?
Un sistema distribuido es aquel donde los componentes (servidores, bases de datos, servicios) están separados físicamente y se comunican a través de una red. En lugar de tener todo en una sola computadora, tienes múltiples computadoras trabajando juntas. Esto trae beneficios (escalabilidad, disponibilidad) pero también desafíos (la red puede fallar, los mensajes pueden perderse).
¿Qué son los Microservicios?
Los microservicios son una forma de diseñar aplicaciones donde cada funcionalidad se implementa como un servicio independiente con su propia base de datos. En lugar de una aplicación monolítica gigante, tienes muchas aplicaciones pequeñas que colaboran entre sí.
El Problema
En un monolito, una transacción puede abarcar múltiples operaciones:
// Monolito: Una transacción ACID
async function createOrder(data: OrderData): Promise<void> {
await db.transaction(async (tx) => {
const order = await tx.insert(orders, data);
await tx.update(inventory, { quantity: sql`quantity - ${data.quantity}` });
await tx.insert(payments, { orderId: order.id, amount: data.total });
// Si algo falla, todo se revierte automáticamente
});
}
El código anterior muestra una transacción ACID. ACID es un acrónimo que describe las propiedades que garantiza una transacción de base de datos:
- Atomicidad (Atomicity): Todas las operaciones se completan o ninguna lo hace. No hay estados intermedios.
- Consistencia (Consistency): La base de datos pasa de un estado válido a otro estado válido.
- Aislamiento (Isolation): Las transacciones concurrentes no interfieren entre sí.
- Durabilidad (Durability): Una vez confirmada, la transacción persiste incluso si el sistema falla.
En microservicios, cada servicio tiene su propia base de datos:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Order │ │ Inventory │ │ Payment │
│ Service │ │ Service │ │ Service │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ OrderDB │ │ InventoryDB │ │ PaymentDB │
└─────────────┘ └─────────────┘ └─────────────┘
No hay transacción global que abarque todas las bases de datos.
Este es el problema central: cuando cada servicio tiene su propia base de datos (principio conocido como “database per service”), no podemos usar las transacciones ACID tradicionales que cruzan múltiples bases de datos.
Teorema CAP
El Teorema CAP (también conocido como Teorema de Brewer) es un principio fundamental en sistemas distribuidos que establece una limitación inevitable.
En un sistema distribuido, solo puedes garantizar 2 de 3:
| Propiedad | Descripción |
|---|---|
| Consistency | Todos los nodos ven los mismos datos |
| Availability | El sistema responde siempre |
| Partition Tolerance | El sistema funciona con fallos de red |
La mayoría de sistemas eligen AP (disponibilidad + tolerancia) con consistencia eventual.
¿Por qué elegir AP? En el mundo real, las particiones de red (fallos de comunicación entre servidores) van a ocurrir. No es una cuestión de “si” sino de “cuándo”. Por lo tanto, la tolerancia a particiones (P) es obligatoria. Esto nos deja elegir entre C (consistencia fuerte) o A (disponibilidad). La mayoría de aplicaciones web prefieren seguir funcionando (aunque con datos potencialmente desactualizados) a dejar de responder completamente.
Consistencia Eventual
La consistencia eventual es un modelo donde el sistema garantiza que, si no hay nuevas actualizaciones, eventualmente todos los nodos tendrán los mismos datos. Es como cuando publicas algo en redes sociales: puede que tus amigos no lo vean inmediatamente, pero eventualmente todos lo verán.
En el contexto de transacciones distribuidas, significa que los datos pueden estar temporalmente inconsistentes entre servicios, pero el sistema se encargará de sincronizarlos.
sequenceDiagram
participant C as Cliente
participant O as Order Service
participant I as Inventory Service
C->>O: Crear pedido
O->>O: Guardar pedido (pendiente)
O-->>C: Pedido creado
O->>I: Reservar stock (async)
I->>I: Actualizar inventario
I->>O: Stock reservado
O->>O: Actualizar pedido (confirmado)
El pedido está temporalmente inconsistente entre la creación y la confirmación.
Patrones para Consistencia
1. Two-Phase Commit (2PC)
El Two-Phase Commit (Compromiso en Dos Fases) es un protocolo clásico para coordinar transacciones distribuidas. Funciona como un “votación” entre todos los participantes.
Coordinador central que prepara y confirma todas las partes:
Fase 1: Prepare
Coordinador → Todos: "¿Pueden commit?"
Todos → Coordinador: "Sí/No"
Fase 2: Commit
Si todos dijeron sí:
Coordinador → Todos: "Commit"
Si alguien dijo no:
Coordinador → Todos: "Abort"
Problemas:
- Punto único de fallo (coordinador)
- Bloqueo de recursos durante la transacción
- No escala bien
2. Saga Pattern
El Saga Pattern (Patrón Saga) es una alternativa a 2PC diseñada específicamente para microservicios. Una Saga es una secuencia de transacciones locales donde cada transacción actualiza un servicio y publica un evento o mensaje para disparar la siguiente transacción.
La diferencia clave con 2PC es que cada paso se confirma inmediatamente (no hay bloqueo esperando a otros) y si algo falla, se ejecutan transacciones compensatorias para deshacer los cambios anteriores.
Secuencia de transacciones locales con compensaciones:
T1 → T2 → T3 → ... → Tn
↓ ↓ ↓ ↓
C1 ← C2 ← C3 ← ... ← Cn (si falla)
Cada transacción tiene una compensación que la revierte. Una compensación no es simplemente un “undo” técnico; es una operación de negocio que revierte semánticamente el efecto de la operación original. Por ejemplo, si la operación original fue “cobrar $100 al cliente”, la compensación sería “reembolsar $100 al cliente”.
3. Outbox Pattern
El Outbox Pattern (Patrón de Bandeja de Salida) resuelve un problema específico: ¿cómo garantizamos que cuando guardamos datos también publicamos el evento correspondiente? Si guardamos los datos pero falla la publicación del evento, tenemos inconsistencia.
La solución es guardar el evento en una tabla “outbox” dentro de la misma transacción que los datos. Un proceso separado lee esta tabla y publica los eventos.
Garantiza que eventos y datos se persisten juntos:
await db.transaction(async (tx) => {
// 1. Guardar datos
await tx.insert(orders, orderData);
// 2. Guardar evento en outbox (misma transacción)
await tx.insert(outbox, {
eventType: 'OrderCreated',
payload: orderData
});
});
// Proceso separado publica eventos del outbox
Por Qué Sagas
| Aspecto | 2PC | Saga |
|---|---|---|
| Bloqueo | Sí | No |
| Escalabilidad | Baja | Alta |
| Complejidad | Media | Alta |
| Recuperación | Automática | Manual (compensación) |
| Disponibilidad | Baja | Alta |
Las Sagas son el patrón preferido para microservicios porque:
- No bloquean recursos
- Escalan horizontalmente
- Toleran fallos de red
- Son eventualmente consistentes
Nuestro Sistema: OrderFlow
Implementaremos un flujo de pedido con múltiples servicios:
graph LR
O[Order Service] --> I[Inventory Service]
O --> P[Payment Service]
O --> S[Shipping Service]
O --> N[Notification Service]
La saga coordinará:
- Crear pedido (Order Service)
- Reservar stock (Inventory Service)
- Procesar pago (Payment Service)
- Programar envío (Shipping Service)
- Notificar cliente (Notification Service)
Si cualquier paso falla, los anteriores se compensan.
Resumen
- Las transacciones distribuidas son difíciles
- El teorema CAP limita nuestras opciones
- 2PC no escala bien en microservicios
- Las Sagas ofrecen consistencia eventual sin bloqueos
- Cada operación necesita una compensación
Glosario
Transacción
Definición: Conjunto de operaciones que se ejecutan como una unidad indivisible. O todas las operaciones se completan exitosamente, o ninguna tiene efecto.
Por qué es importante: Sin transacciones, los datos podrían quedar en estados inconsistentes. Por ejemplo, dinero podría “desaparecer” si se resta de una cuenta pero no se suma a otra.
Ejemplo práctico: Transferir $100 de cuenta A a cuenta B requiere dos operaciones: restar de A y sumar a B. La transacción garantiza que ambas ocurran o ninguna.
Transacción Distribuida
Definición: Una transacción que involucra múltiples bases de datos o servicios ubicados en diferentes servidores o sistemas.
Por qué es importante: En arquitecturas de microservicios, cada servicio tiene su propia base de datos. Coordinar operaciones entre ellos requiere mecanismos especiales porque no podemos usar las transacciones tradicionales de una sola base de datos.
Ejemplo práctico: Crear un pedido que involucre el servicio de órdenes (guardar pedido), servicio de inventario (reservar stock) y servicio de pagos (cobrar al cliente).
ACID
Definición: Acrónimo que describe las cuatro propiedades de las transacciones de base de datos: Atomicidad, Consistencia, Aislamiento y Durabilidad.
Por qué es importante: Estas propiedades garantizan que las transacciones sean confiables y predecibles, evitando corrupción de datos y estados inconsistentes.
Ejemplo práctico: Si una transacción intenta insertar un registro pero viola una restricción de clave única, la base de datos revierte automáticamente toda la transacción (atomicidad) y los datos permanecen consistentes.
Microservicios
Definición: Estilo arquitectónico donde una aplicación se construye como un conjunto de servicios pequeños, independientes y desplegables por separado, cada uno con su propia base de datos.
Por qué es importante: Permite escalar, desarrollar y desplegar partes de la aplicación de forma independiente. Sin embargo, introduce complejidad en la gestión de transacciones que cruzan múltiples servicios.
Ejemplo práctico: Una tienda online podría tener servicios separados para: catálogo de productos, carrito de compras, pagos, envíos y notificaciones.
Teorema CAP
Definición: Principio que establece que un sistema distribuido solo puede garantizar dos de tres propiedades: Consistencia, Disponibilidad y Tolerancia a Particiones.
Por qué es importante: Nos obliga a tomar decisiones de diseño conscientes. No podemos tener todo; debemos elegir qué sacrificar según las necesidades del negocio.
Ejemplo práctico: Un sistema de chat puede preferir disponibilidad (los mensajes siempre se pueden enviar) sobre consistencia estricta (algunos usuarios pueden ver los mensajes en orden ligeramente diferente).
Consistencia Eventual
Definición: Modelo de consistencia que garantiza que, si no se hacen nuevas actualizaciones, eventualmente todas las réplicas de los datos convergerán al mismo valor.
Por qué es importante: Permite construir sistemas más disponibles y escalables, a cambio de aceptar que los datos pueden estar temporalmente desincronizados.
Ejemplo práctico: Cuando cambias tu foto de perfil en una red social, algunos amigos pueden ver la nueva foto mientras otros todavía ven la anterior. Eventualmente, todos verán la misma.
Saga
Definición: Patrón de diseño para gestionar transacciones distribuidas mediante una secuencia de transacciones locales, donde cada transacción actualiza un servicio y dispara la siguiente.
Por qué es importante: Proporciona una forma de mantener consistencia de datos entre múltiples servicios sin bloquear recursos, haciéndolo ideal para microservicios.
Ejemplo práctico: Procesar un pedido como una saga: (1) crear pedido, (2) reservar inventario, (3) cobrar pago, (4) programar envío. Si el pago falla, se ejecutan compensaciones para liberar el inventario y cancelar el pedido.
Compensación
Definición: Operación de negocio que revierte semánticamente el efecto de una transacción anterior cuando una saga falla.
Por qué es importante: Permite deshacer cambios que ya fueron confirmados en transacciones anteriores, manteniendo la consistencia del sistema.
Ejemplo práctico: Si “reservar inventario” ya se ejecutó pero “cobrar pago” falla, la compensación de “reservar inventario” sería “liberar inventario” para devolver los productos al stock disponible.
Two-Phase Commit (2PC)
Definición: Protocolo de coordinación donde un coordinador central pregunta a todos los participantes si pueden confirmar (fase 1) y luego les indica que confirmen o aborten (fase 2).
Por qué es importante: Fue el primer enfoque para transacciones distribuidas, pero tiene problemas de escalabilidad y disponibilidad que lo hacen inadecuado para microservicios modernos.
Ejemplo práctico: El coordinador pregunta a la base de datos de órdenes y la de inventario: “¿pueden confirmar?”. Si ambas dicen sí, envía “confirmen”. Si alguna dice no, envía “aborten” a todas.
Outbox Pattern
Definición: Patrón donde los eventos se guardan en una tabla de la misma base de datos junto con los datos de negocio, garantizando que ambos se persistan en la misma transacción.
Por qué es importante: Resuelve el problema de garantizar que un cambio de datos y su evento asociado se publiquen de forma atómica, evitando inconsistencias.
Ejemplo práctico: Al crear un pedido, en la misma transacción se inserta el pedido en la tabla orders y el evento OrderCreated en la tabla outbox. Un proceso separado lee la tabla outbox y publica los eventos.