⏱️ Lectura: 16 min
Cuando transferís dinero entre dos bancos distintos, dos bases de datos separadas tienen que confirmar la operación al mismo tiempo o ninguna debe mover un centavo. El protocolo que resuelve ese problema desde 1978 se llama two-phase commit, y hoy sigue corriendo dentro de PostgreSQL, MySQL y arquitecturas como Google Spanner cada vez que una transacción cruza más de un nodo.
📑 En este artículo
- TL;DR
- Qué es el two-phase commit y por qué importa
- Cómo funciona: la fase de preparación y la fase de commit
- Ejemplos prácticos: implementando two-phase commit
- Cómo empezar: habilitar transacciones preparadas paso a paso
- Casos de uso reales
- Errores comunes y buenas prácticas
- Comparativa con alternativas
- Profundizando: por qué los sistemas modernos evitan el 2PC puro
- Preguntas frecuentes
- ¿Qué diferencia hay entre two-phase commit y un commit normal?
- ¿Qué pasa si el coordinador se cae después de la fase de preparación?
- ¿Two-phase commit garantiza disponibilidad durante una partición de red?
- ¿Puedo usar two-phase commit entre bases de datos de proveedores distintos?
- ¿Cómo elimino una transacción preparada que quedó huérfana?
- ¿Two-phase commit sigue siendo relevante en 2026?
- Referencias
Esta guía explica cómo funciona el two-phase commit paso a paso, con ejemplos ejecutables en PostgreSQL, sus fallas conocidas y cuándo conviene reemplazarlo por alternativas más modernas como Sagas o consenso distribuido tipo Raft.
TL;DR
- Vas a entender las dos fases (prepare y commit) que usa two-phase commit para coordinar varios nodos.
- Vas a ejecutar una transacción distribuida real con PREPARE TRANSACTION en PostgreSQL.
- Vas a poder verificar transacciones preparadas pendientes con la vista pg_prepared_xacts.
- Vas a identificar el problema del bloqueo que aparece si el coordinador se cae entre fases.
- Vas a comparar two-phase commit contra 3PC, Sagas y consenso tipo Raft para elegir bien.
- Vas a conocer la sintaxis XA de MySQL como alternativa a la de PostgreSQL.
- Vas a saber cómo limpiar transacciones preparadas huérfanas antes de que bloqueen producción.
Qué es el two-phase commit y por qué importa
El two-phase commit (2PC) es un protocolo de coordinación que garantiza que una transacción que toca varios nodos termine igual en todos: o se confirma en cada uno, o se aborta en cada uno. No hay término medio donde un nodo confirme y otro no.
Lo diseñó Jim Gray en 1978 como parte de su trabajo sobre sistemas operativos de bases de datos, y desde entonces se volvió el mecanismo de referencia para dar atomicidad a transacciones distribuidas. La idea central es simple: separar la decisión de aplicar el cambio en dos pasos, uno donde cada nodo promete que puede cumplir, y otro donde el coordinador ejecuta esa promesa (o la cancela) en todos a la vez.
En términos de las propiedades ACID, two-phase commit extiende la atomicidad más allá de un solo motor de base de datos. Una transacción normal ya garantiza atomicidad dentro de un nodo: si el proceso se cae a mitad de camino, el motor revierte todo al reiniciar. Two-phase commit lleva esa misma garantía a un grupo de nodos que, individualmente, no saben nada del resto: cada uno solo entiende prepare, commit y abort, y confía en que el coordinador decida por todos a la vez.
Importa hoy porque cualquier sistema con sharding, con microservicios que comparten una transacción, o con bases de datos heterogéneas conectadas por un middleware XA, necesita algo parecido al two-phase commit para no terminar con datos inconsistentes entre nodos.
Cómo funciona: la fase de preparación y la fase de commit
Two-phase commit divide la transacción en dos rondas de comunicación entre un coordinador y N participantes, los nodos que van a escribir datos.
Fase 1: preparación (voting phase)
El coordinador envía un mensaje prepare a cada participante. Cada participante ejecuta la operación localmente pero sin confirmarla todavía: bloquea las filas o tablas involucradas y escribe el estado preparado en su propio log de transacciones, de forma durable en disco. Después responde YES (puedo confirmar) o NO (no puedo, por ejemplo si viola una restricción o hay un conflicto de bloqueo).
Fase 2: confirmación (commit phase)
Si todos los participantes votaron YES, el coordinador envía commit a todos y cada uno aplica el cambio de forma definitiva y libera los bloqueos. Si al menos uno votó NO, o no respondió dentro del tiempo esperado, el coordinador envía abort a todos, y cada participante revierte lo que había preparado.
Por qué el estado preparado debe quedar en disco
La garantía de two-phase commit depende de que, una vez que un participante contesta YES, esa promesa sobreviva a un reinicio. Por eso cada participante escribe su voto y los cambios pendientes en su propio write-ahead log antes de responder al coordinador. Si el proceso se cae después de votar YES y se reinicia, al arrancar vuelve a leer ese log, encuentra la transacción en estado preparada y queda esperando la orden final del coordinador en lugar de perder el trabajo.
sequenceDiagram
participant Co as Coordinador
participant A as Nodo A
participant B as Nodo B
Co->>A: prepare tx_482
Co->>B: prepare tx_482
A-->>Co: voto YES
B-->>Co: voto YES
Co->>A: commit tx_482
Co->>B: commit tx_482
Note over Co,B: la transaccion queda confirmada en los dos nodos
Ejemplos prácticos: implementando two-phase commit
Ejemplo 1: coordinador mínimo en Python
Antes de tocar SQL real, sirve ver la lógica pelada. Este coordinador de juguete simula la votación y la decisión final sin persistencia ni red:
class Participante:
def __init__(self, nombre):
self.nombre = nombre
def prepare(self):
return "YES"
def commit(self):
print(f"{self.nombre}: transaccion confirmada")
def abort(self):
print(f"{self.nombre}: transaccion abortada")
def coordinar(participantes):
votos = [p.prepare() for p in participantes]
if all(voto == "YES" for voto in votos):
for p in participantes:
p.commit()
return "COMMIT"
for p in participantes:
p.abort()
return "ABORT"
nodos = [Participante("cuenta_origen"), Participante("cuenta_destino")]
resultado = coordinar(nodos)
print(f"Resultado global: {resultado}")
La función coordinar pide el voto a cada participante y solo confirma si todos respondieron YES. Al correrlo, la salida esperada es cuenta_origen: transaccion confirmada, cuenta_destino: transaccion confirmada y Resultado global: COMMIT. Si cambiás el prepare de un nodo para devolver NO, el resultado global pasa a ABORT y ningún nodo confirma nada.
Ejemplo 2: two-phase commit real con PostgreSQL
PostgreSQL implementa two-phase commit de forma nativa con PREPARE TRANSACTION. Simulemos una transferencia entre dos nodos (dos bases distintas, banco_origen y banco_destino) que un coordinador externo tiene que confirmar de forma atómica:
-- Conexion al nodo A (banco_origen)
BEGIN;
UPDATE cuentas SET saldo = saldo - 500 WHERE id = 'cuenta_juan';
PREPARE TRANSACTION 'transferencia_482';
-- Conexion al nodo B (banco_destino)
BEGIN;
UPDATE cuentas SET saldo = saldo + 500 WHERE id = 'cuenta_maria';
PREPARE TRANSACTION 'transferencia_482';
-- El coordinador verifica que ambos nodos prepararon el mismo id
SELECT gid, prepared, owner FROM pg_prepared_xacts;
-- Si los dos aparecen listados, confirma en cada nodo por separado
COMMIT PREPARED 'transferencia_482'; -- ejecutar en nodo A
COMMIT PREPARED 'transferencia_482'; -- ejecutar en nodo B
Cada PREPARE TRANSACTION deja el cambio bloqueado y durable, pero invisible para otras transacciones hasta el commit. La vista pg_prepared_xacts es la forma de confirmar, desde afuera, que ambos nodos efectivamente prepararon la misma transacción antes de decidir el commit final. Si alguno no aparece en esa vista, el coordinador debe emitir ROLLBACK PREPARED en el que sí preparó, para no dejar el sistema a medias.
Cómo empezar: habilitar transacciones preparadas paso a paso
Por defecto, PostgreSQL trae esta función apagada. Para probarla en un entorno local o de pruebas:
- Abrí
postgresql.confy configurámax_prepared_transactions = 10(el valor por defecto es 0, que deshabilitaPREPARE TRANSACTIONpor completo). - Reiniciá el servidor por completo. Este parámetro no admite un simple reload, necesita un restart del proceso.
- Confirmá el valor activo con
SHOW max_prepared_transactions;. - Corré
BEGIN;, tu operación, yPREPARE TRANSACTION 'id_unico';en cada conexión que participa. - Verificá el estado global con
SELECT * FROM pg_prepared_xacts;antes de decidir. - Cerrá con
COMMIT PREPARED 'id_unico';en cada nodo, oROLLBACK PREPARED 'id_unico';si alguno falló.
flowchart TD
A["BEGIN"] --> B["PREPARE TRANSACTION"]
B --> C{"Todos los nodos votaron YES?"}
C -->|"si"| D["COMMIT PREPARED"]
C -->|"no"| E["ROLLBACK PREPARED"]
D --> F["Transaccion confirmada en todos los nodos"]
E --> G["Transaccion abortada en todos los nodos"]
💡 Tip: usá un identificador (gid) único y determinístico por transacción de negocio, por ejemplo el id del pedido, para poder ubicarla en pg_prepared_xacts si el coordinador se cae y necesitás recuperarla a mano.
Casos de uso reales
Two-phase commit aparece en varios contextos concretos, no solo en papers académicos:
- Middleware XA: el estándar X/Open XA, usado por JTA en Java EE, coordina transacciones entre bases de datos y colas de mensajes distintas usando el mismo esquema de prepare y commit.
- MySQL también lo soporta: con la sintaxis XA START, XA END, XA PREPARE y XA COMMIT, equivalente en espíritu a la de PostgreSQL.
- Bases de datos con sharding: cuando una transacción de negocio toca dos shards distintos, por ejemplo mover inventario entre dos particiones, 2PC es la forma clásica de mantenerlas sincronizadas.
- Migraciones entre motores: mover datos de una base a otra sin ventana de inconsistencia usa variantes de este protocolo.
- Colas de mensajes junto a una base de datos: sistemas que necesitan escribir en una tabla y publicar un mensaje de forma atómica evaluaron 2PC entre el broker y la base, aunque hoy la mayoría prefiere el patrón outbox para evitar ese acoplamiento.
- Google Spanner: combina two-phase commit entre grupos que replican con Paxos, para dar atomicidad a transacciones que cruzan varios shards dentro del mismo sistema.
Cuándo NO conviene usar two-phase commit: si tu transacción cruza servicios que no controlás por completo, por ejemplo la API de un proveedor externo, no podés confiar en que ese servicio implemente correctamente la fase de preparación, y terminás con un sistema que se comporta como 2PC pero sin sus garantías reales. En esos casos, un patrón basado en eventos con reintentos idempotentes suele ser más seguro que forzar un protocolo de consenso síncrono.
Errores comunes y buenas prácticas
El error más citado de two-phase commit es el problema del bloqueo: si el coordinador se cae después de que todos los participantes votaron YES, pero antes de enviar la orden de commit, cada participante queda con los bloqueos tomados y sin saber si debe confirmar o abortar. No puede decidir solo, porque otro nodo pudo haber votado NO y el coordinador simplemente no le avisó a tiempo.
flowchart TD
A["Coordinador envia PREPARE"] --> B["Nodo A vota YES y bloquea filas"]
A --> C["Nodo B vota YES y bloquea filas"]
B --> D["Coordinador se cae antes de enviar COMMIT"]
C --> D
D --> E["Nodo A y Nodo B quedan bloqueados esperando la orden final"]
subgraph "Zona de riesgo"
D
E
end
⚠️ Ojo: en PostgreSQL, una transacción preparada que quedó huérfana no solo bloquea filas: también impide que autovacuum limpie las tuplas muertas de esas tablas, porque el sistema tiene que preservarlas por si la transacción todavía se confirma. Una transacción olvidada puede inflar una tabla en producción durante días.
Buenas prácticas para evitarlo en producción:
- Monitoreá
pg_prepared_xactsy alertá si una fila tiene más de unos minutos de antigüedad sin resolverse. - Si usás middleware JTA/XA, revisá su configuración de decisiones heurísticas: por defecto, algunos permiten que un participante decida solo tras un timeout, lo que puede romper la atomicidad si el coordinador estaba vivo pero lento.
- Usá timeouts cortos en la fase de preparación: cuanto más tiempo un nodo mantiene locks tomados, más impacto tiene en el resto del tráfico.
- Guardá el log del coordinador en almacenamiento durable y separado de los participantes, para poder recuperar y terminar transacciones colgadas tras un reinicio.
Comparativa con alternativas
| Opción | Cuándo usarla | Ventaja | Limitación |
|---|---|---|---|
| Two-Phase Commit (2PC) | Transacciones cortas entre pocos nodos que necesitan atomicidad estricta | Consistencia fuerte: todo o nada | Bloquea recursos y depende de que el coordinador esté disponible |
| Three-Phase Commit (3PC) | Cuando se necesita reducir el bloqueo indefinido ante la caída del coordinador | Agrega una fase intermedia que evita que todos queden colgados para siempre | Un round trip más de latencia y sigue sin resolver bien las particiones de red |
| Saga (transacciones compensatorias) | Flujos de negocio largos entre microservicios | No mantiene locks abiertos entre pasos, tolera fallos parciales | Hay que escribir una operación de compensación por cada paso |
| Consenso tipo Raft o Paxos | Replicar el mismo dato entre varios nodos de un mismo clúster | Sigue funcionando mientras haya mayoría de nodos vivos | Resuelve replicación dentro de un clúster, no coordinación entre clústeres distintos |
Profundizando: por qué los sistemas modernos evitan el 2PC puro
Two-phase commit exige, como mínimo, dos rondas completas de ida y vuelta antes de poder confirmar cualquier cosa. En una base de datos local eso es barato; entre regiones geográficas distintas, cada ronda puede costar varios milisegundos, y la transacción entera queda esperando a la respuesta más lenta de todos los participantes.
Google Spanner resuelve parte del problema separando dos capas: dentro de cada grupo de réplicas usa consenso Paxos para tolerar la caída de nodos individuales, y entre grupos distintos usa two-phase commit solo para coordinar la transacción final, minimizando cuántos actores dependen del coordinador central en cada operación.
CockroachDB va un paso más allá con lo que llama parallel commits: en vez de esperar una segunda ronda completa para confirmar, el coordinador manda las escrituras y la intención de commit en paralelo, y solo si todo salió bien marca la transacción como confirmada sin ese segundo viaje de red completo. La idea general, reducir cuántas rondas de red separan preparado de confirmado, es la dirección en la que se mueven casi todas las bases de datos distribuidas modernas.
Las Sagas resuelven el problema desde otro ángulo: en vez de mantener locks abiertos mientras se espera una decisión global, cada paso se confirma de inmediato y, si algo falla más adelante, se ejecuta una operación de compensación, por ejemplo cancelar una reserva en vez de mantener bloqueado el inventario. A cambio, la aplicación pierde la garantía de aislamiento estricto entre pasos: otros procesos pueden ver estados intermedios.
💭 Clave: two-phase commit optimiza consistencia sobre disponibilidad; Sagas hacen exactamente lo contrario. Elegir entre ambos es, en el fondo, elegir en qué lado del teorema CAP querés quedar para ese flujo puntual.
Para medir el costo real de estas dos rondas en tu propio entorno, activá \timing en psql y compará cuánto tarda un PREPARE TRANSACTION seguido de COMMIT PREPARED contra un COMMIT directo de una transacción de un solo nodo: la diferencia te da el costo concreto de la ronda extra en tu propia red.
📖 Resumen en Telegram: Ver resumen
Tu próximo paso: levantá dos bases PostgreSQL locales con Docker, activá max_prepared_transactions en ambas y reproducí la transferencia del Ejemplo 2 a mano para ver pg_prepared_xacts en tiempo real.
Preguntas frecuentes
¿Qué diferencia hay entre two-phase commit y un commit normal?
Un commit normal confirma una transacción dentro de un solo nodo. Two-phase commit coordina que varios nodos independientes confirmen la misma transacción de negocio a la vez, usando una fase de votación antes de aplicar el cambio en ninguno.
¿Qué pasa si el coordinador se cae después de la fase de preparación?
Los participantes que ya votaron YES quedan bloqueados, con los recursos tomados, hasta que el coordinador se recupere y les indique si deben confirmar o abortar. Es el problema del bloqueo descrito arriba, y es la principal crítica que recibe el protocolo.
¿Two-phase commit garantiza disponibilidad durante una partición de red?
No. 2PC prioriza consistencia sobre disponibilidad: si el coordinador no puede comunicarse con un participante, la transacción completa queda pendiente en vez de avanzar de forma parcial.
¿Puedo usar two-phase commit entre bases de datos de proveedores distintos?
Sí, para eso existe el estándar XA: tanto PostgreSQL como MySQL, Oracle y SQL Server implementan la interfaz XA, y un transaction manager JTA puede coordinar el 2PC entre todos ellos.
¿Cómo elimino una transacción preparada que quedó huérfana?
Identificala con SELECT * FROM pg_prepared_xacts; y ejecutá ROLLBACK PREPARED 'gid'; (o COMMIT PREPARED 'gid'; si sabés que el resto de los nodos sí confirmaron) usando el mismo usuario que la creó.
¿Two-phase commit sigue siendo relevante en 2026?
Sí, como pieza interna dentro de bases de datos distribuidas como Spanner y CockroachDB, y en integraciones XA empresariales, aunque para flujos de negocio entre microservicios la mayoría de los equipos hoy prefiere Sagas por su tolerancia a fallos parciales.
Referencias
- Wikipedia: Two-phase commit protocol: historia y descripción formal del algoritmo.
- PostgreSQL Docs: PREPARE TRANSACTION: sintaxis oficial y el parámetro max_prepared_transactions.
- PostgreSQL Docs: pg_prepared_xacts: vista para inspeccionar transacciones preparadas pendientes.
- MySQL Docs: XA Transactions: sintaxis XA START, XA PREPARE y XA COMMIT en MySQL.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
Imagen destacada: Foto de Kier in Sight Archives en Unsplash
0 Comentarios