Capítulo 2: El Problema del Two-Phase Commit
Capítulo 2: El Problema del Two-Phase Commit
“2PC garantiza consistencia a costa de disponibilidad”
¿Qué es Two-Phase Commit?
El Two-Phase Commit (2PC), o “Compromiso en Dos Fases”, es un protocolo diseñado para coordinar transacciones que involucran múltiples bases de datos o servicios. Piensa en él como un sistema de votación: antes de hacer cualquier cambio permanente, todos los participantes deben estar de acuerdo.
El protocolo se llama “dos fases” porque el proceso se divide en dos etapas claramente diferenciadas:
- Fase de Preparación (Prepare): El coordinador pregunta a todos si están listos.
- Fase de Compromiso (Commit): Si todos dijeron que sí, se confirma; si alguien dijo que no, se aborta.
Componentes del 2PC
- Coordinador: Es el “director de orquesta” que gestiona todo el proceso. Envía los mensajes de preparación y decide si confirmar o abortar.
- Participantes: Son los servicios o bases de datos que ejecutarán parte de la transacción. Cada uno vota si puede completar su parte.
Cómo Funciona 2PC
sequenceDiagram
participant C as Coordinador
participant P1 as Participante 1
participant P2 as Participante 2
Note over C,P2: Fase 1: Prepare
C->>P1: PREPARE
C->>P2: PREPARE
P1-->>C: VOTE_COMMIT
P2-->>C: VOTE_COMMIT
Note over C,P2: Fase 2: Commit
C->>P1: COMMIT
C->>P2: COMMIT
P1-->>C: ACK
P2-->>C: ACK
Implementación Básica
// Coordinador 2PC (simplificado)
class TwoPhaseCommitCoordinator {
private participants: Participant[] = [];
async execute(transaction: Transaction): Promise<boolean> {
// Fase 1: Prepare
const prepareResults = await Promise.all(
this.participants.map(p => p.prepare(transaction))
);
const allPrepared = prepareResults.every(r => r === 'PREPARED');
// Fase 2: Commit o Abort
if (allPrepared) {
await Promise.all(
this.participants.map(p => p.commit(transaction.id))
);
return true;
} else {
await Promise.all(
this.participants.map(p => p.abort(transaction.id))
);
return false;
}
}
}
interface Participant {
prepare(tx: Transaction): Promise<'PREPARED' | 'ABORT'>;
commit(txId: string): Promise<void>;
abort(txId: string): Promise<void>;
}
Problemas de 2PC
1. Bloqueo de Recursos
El bloqueo de recursos es una técnica donde un sistema “reserva” ciertos datos para evitar que otras operaciones los modifiquen mientras se procesa una transacción. Es como poner un candado en una puerta: nadie más puede pasar hasta que tú termines.
Durante la fase de prepare, los recursos quedan bloqueados:
class InventoryParticipant implements Participant {
async prepare(tx: Transaction): Promise<'PREPARED' | 'ABORT'> {
// Bloquear stock durante prepare
await this.db.execute(sql`
SELECT * FROM inventory
WHERE product_id = ${tx.productId}
FOR UPDATE -- BLOQUEO!
`);
// Verificar disponibilidad
if (await this.hasStock(tx.productId, tx.quantity)) {
// Recursos bloqueados hasta commit/abort
return 'PREPARED';
}
return 'ABORT';
}
}
Si el coordinador falla entre prepare y commit, los recursos quedan bloqueados indefinidamente.
2. Punto Único de Fallo
Un punto único de fallo (Single Point of Failure, SPOF) es cualquier componente cuyo fallo detiene todo el sistema. Es el “eslabón débil” de la cadena.
┌──────────────┐
│ Coordinador │ <- Si falla aquí, todo se bloquea
└──────────────┘
│
┌────┴────┐
↓ ↓
┌──┐ ┌──┐
│P1│ │P2│ ← Esperando decisión del coordinador
└──┘ └──┘
3. No Tolera Particiones de Red
Una partición de red ocurre cuando los servidores no pueden comunicarse entre sí debido a problemas de red, aunque cada servidor individualmente funcione bien. Es como si cortaras el cable telefónico: ambos teléfonos funcionan, pero no pueden hablar entre sí.
sequenceDiagram
participant C as Coordinador
participant P1 as Participante 1
participant P2 as Participante 2
C->>P1: PREPARE
C->>P2: PREPARE
P1-->>C: VOTE_COMMIT
Note over C,P2: Red particionada!
P2--xC: (mensaje perdido)
Note over C: Timeout esperando P2
C->>P1: ABORT
Note over P2: ¿Qué hago?
4. Latencia Aumentada
La latencia es el tiempo que tarda una operación en completarse. En 2PC, la latencia aumenta porque cada operación requiere múltiples viajes de ida y vuelta por la red.
RTT (Round-Trip Time) es el tiempo que tarda un mensaje en ir del emisor al receptor y volver. Cada fase de 2PC requiere al menos un RTT con cada participante.
Tiempo sin 2PC: T1 + T2 + T3
Tiempo con 2PC: 2 * (T1 + T2 + T3) + coordinación
Donde:
- Prepare: T1 + T2 + T3
- Commit: T1 + T2 + T3
- Coordinación: RTT adicional
Escenarios de Fallo
Fallo del Coordinador en Fase 1
// Participante no sabe si abortar o esperar
class UncertainParticipant {
private state: 'INITIAL' | 'PREPARED' | 'COMMITTED' | 'ABORTED' = 'INITIAL';
async prepare(tx: Transaction): Promise<'PREPARED' | 'ABORT'> {
this.state = 'PREPARED';
// Si el coordinador falla aquí...
// ¿Commit o abort? No sabemos
return 'PREPARED';
}
// Necesitamos un timeout, pero ¿cuánto esperar?
async recover(): Promise<void> {
if (this.state === 'PREPARED') {
// Opción 1: Abortar (podría ser incorrecto)
// Opción 2: Esperar indefinidamente (bloqueo)
// Opción 3: Consultar a otros participantes (complejo)
}
}
}
Fallo del Participante en Fase 2
sequenceDiagram
participant C as Coordinador
participant P1 as Participante 1
participant P2 as Participante 2
Note over C,P2: Fase 1 exitosa
C->>P1: COMMIT
C->>P2: COMMIT
P1-->>C: ACK
Note over P2: P2 falla antes de commit
P2--xC: (sin respuesta)
Note over C: ¿Reintento? ¿Hasta cuándo?
2PC vs Sagas
El aislamiento en esta tabla se refiere a qué tan “invisibles” son los cambios intermedios de una transacción para otras transacciones. Con aislamiento fuerte, nadie ve tus cambios hasta que confirmas todo. Con aislamiento débil, otras transacciones pueden ver estados intermedios.
| Escenario | 2PC | Saga |
|---|---|---|
| Fallo de coordinador | Bloqueo | Saga continúa/compensa |
| Partición de red | Bloqueo | Consistencia eventual |
| Latencia | 2x operaciones | 1x operaciones |
| Complejidad | Media | Alta (compensaciones) |
| Aislamiento | Fuerte | Débil |
Cuándo Usar 2PC
2PC todavía tiene casos de uso válidos:
- Bases de datos relacionadas: Transacciones entre schemas
- Sistemas legacy: Integración con sistemas que lo soportan
- Baja latencia requerida: Cuando no hay red intermedia
- Pocas operaciones: 2-3 participantes máximo
// Ejemplo: Transacción entre schemas en PostgreSQL
await db.execute(sql`
BEGIN;
-- Schema de órdenes
INSERT INTO orders.orders (id, customer_id) VALUES (${orderId}, ${customerId});
-- Schema de auditoría
INSERT INTO audit.order_events (order_id, event) VALUES (${orderId}, 'created');
COMMIT;
`);
Resumen
- 2PC garantiza consistencia fuerte pero sacrifica disponibilidad
- El bloqueo de recursos es problemático en alta concurrencia
- El coordinador es un punto único de fallo
- No tolera particiones de red adecuadamente
- Para microservicios, las Sagas son preferibles
Glosario
Coordinador (en 2PC)
Definición: Componente central que gestiona el protocolo Two-Phase Commit. Es responsable de enviar los mensajes de preparación, recolectar los votos y decidir si confirmar o abortar la transacción.
Por qué es importante: Sin un coordinador, los participantes no sabrían cuándo prepararse ni cuál fue la decisión final. Sin embargo, su rol centralizado lo convierte en un punto único de fallo.
Ejemplo práctico: En una transacción que involucra el servicio de pagos y el de inventario, el coordinador envía “PREPARE” a ambos, espera sus respuestas, y luego envía “COMMIT” o “ABORT” según los votos recibidos.
Participante (en 2PC)
Definición: Servicio o base de datos que ejecuta una parte de la transacción distribuida. Recibe instrucciones del coordinador, vota si puede completar su parte, y ejecuta la acción final.
Por qué es importante: Los participantes son quienes realmente ejecutan el trabajo. Su capacidad de prepararse (bloquear recursos) y esperar la decisión final es lo que hace posible la coordinación.
Ejemplo práctico: El servicio de inventario como participante: cuando recibe “PREPARE”, verifica que hay stock suficiente y lo bloquea. Cuando recibe “COMMIT”, confirma la reserva. Con “ABORT”, libera el bloqueo.
Bloqueo de Recursos
Definición: Mecanismo donde un sistema reserva exclusivamente ciertos datos o recursos para evitar que otras operaciones los modifiquen durante el procesamiento de una transacción.
Por qué es importante: Garantiza que los datos no cambien mientras se decide si confirmar o abortar. Sin embargo, si el coordinador falla, los recursos pueden quedar bloqueados indefinidamente.
Ejemplo práctico: Durante la fase PREPARE, el inventario ejecuta SELECT ... FOR UPDATE que bloquea los registros de productos. Ninguna otra transacción puede modificar esos productos hasta que se libere el bloqueo.
Punto Único de Fallo (SPOF)
Definición: Componente de un sistema cuyo fallo provoca que todo el sistema deje de funcionar. No hay redundancia ni alternativa si este componente falla.
Por qué es importante: Los SPOF son riesgos críticos en arquitecturas distribuidas. Identificarlos y eliminarlos (mediante redundancia o diseño alternativo) es esencial para sistemas resilientes.
Ejemplo práctico: En 2PC, si el coordinador falla después de enviar PREPARE pero antes de enviar COMMIT/ABORT, todos los participantes quedan bloqueados esperando una decisión que nunca llegará.
Partición de Red
Definición: Situación donde algunos nodos de un sistema distribuido no pueden comunicarse entre sí debido a fallos de red, aunque cada nodo individualmente funcione correctamente.
Por qué es importante: Las particiones de red son inevitables en sistemas distribuidos. Cómo el sistema maneja estas situaciones determina su disponibilidad y consistencia durante fallos.
Ejemplo práctico: El coordinador puede comunicarse con el servicio de pagos pero no con el de inventario. El coordinador no sabe si inventario está caído o solo incomunicado, y no puede decidir con seguridad.
Latencia
Definición: Tiempo que transcurre desde que se inicia una operación hasta que se completa. En sistemas distribuidos, incluye el tiempo de procesamiento más el tiempo de transmisión por la red.
Por qué es importante: Alta latencia afecta directamente la experiencia del usuario y el rendimiento del sistema. 2PC duplica la latencia porque requiere dos rondas de comunicación.
Ejemplo práctico: Si cada servicio tarda 50ms en procesar y la red añade 10ms, una operación normal tarda 60ms. Con 2PC, serían 120ms (dos fases) más el overhead de coordinación.
RTT (Round-Trip Time)
Definición: Tiempo que tarda un mensaje en viajar desde el emisor al receptor y la respuesta en volver al emisor. Incluye la latencia de ida y de vuelta.
Por qué es importante: Cada fase de 2PC requiere al menos un RTT con cada participante. Cuantos más participantes y mayor el RTT, más lenta es la transacción.
Ejemplo práctico: Si el RTT entre el coordinador y un participante es 20ms, y hay 3 participantes, solo la comunicación de la fase PREPARE consume 60ms (asumiendo comunicación secuencial).
Consistencia Fuerte
Definición: Modelo donde todas las lecturas siempre devuelven el resultado de la escritura más reciente. Todos los nodos ven los mismos datos en todo momento.
Por qué es importante: Simplifica el razonamiento sobre el sistema porque no hay estados intermedios visibles. Sin embargo, lograrla en sistemas distribuidos tiene costos en disponibilidad y latencia.
Ejemplo práctico: Si transfieres $100 de cuenta A a B, con consistencia fuerte, cualquier consulta posterior siempre mostrará ambas cuentas con sus saldos actualizados, nunca estados intermedios.
Aislamiento (de transacciones)
Definición: Propiedad que determina cómo y cuándo los cambios realizados por una transacción son visibles para otras transacciones concurrentes.
Por qué es importante: El aislamiento fuerte evita anomalías como lecturas sucias o escrituras perdidas, pero tiene costo en rendimiento. El aislamiento débil permite más concurrencia pero requiere que la aplicación maneje posibles inconsistencias.
Ejemplo práctico: Con aislamiento fuerte (2PC), nadie ve el pedido hasta que se confirma todo. Con aislamiento débil (Sagas), el pedido podría ser visible mientras el pago aún se procesa.
Timeout
Definición: Tiempo máximo que un sistema espera por una respuesta antes de asumir que la operación falló. Es un mecanismo de protección contra bloqueos indefinidos.
Por qué es importante: Sin timeouts, un sistema podría esperar eternamente por una respuesta que nunca llega. Sin embargo, elegir el timeout correcto es difícil: muy corto causa falsos positivos, muy largo retrasa la detección de fallos.
Ejemplo práctico: El coordinador espera máximo 5 segundos por el voto de cada participante. Si no responden en ese tiempo, asume fallo y envía ABORT a todos.
← Capítulo 1: Introducción | Capítulo 3: Orquestación vs Coreografía →