Introducción
Cuando una aplicación backend atiende a muchos usuarios simultáneamente, la base de datos se convierte en el campo de batalla central para la consistencia. Dos usuarios reservando el último asiento, una confirmación de pago compitiendo con un reembolso, un informe leyendo datos mientras hay una actualización en curso — cada uno de estos escenarios toca la maquinaria de transacciones y concurrencia de PostgreSQL. Hacerlo mal provoca corrupción silenciosa de datos, pedidos duplicados o saldos imposibles.
Esta lección se centra en lo que un desarrollador backend debe saber para razonar sobre el comportamiento concurrente: cómo PostgreSQL envuelve el trabajo en transacciones, cómo los niveles de aislamiento cambian la visibilidad entre transacciones concurrentes, cómo MVCC permite que lectores y escritores no se bloqueen entre sí, y dónde intervienen realmente los bloqueos a nivel de fila. El objetivo no es la profundidad de la administración de bases de datos, sino el modelo mental práctico necesario cuando un informe de bug en producción dice «dos peticiones hicieron lo mismo» o «el dashboard mostraba números obsoletos».
ACID y límites de transacción
Una transacción en PostgreSQL es una unidad de trabajo que se confirma completamente o se revierte completamente. Las cuatro garantías — Atomicidad, Consistencia, Aislamiento, Durabilidad — describen lo que la base de datos promete si envuelves tus sentencias en BEGIN y COMMIT.
En un backend, la forma típica se ve así:
Si el segundo UPDATE falla, el primero también se revierte. Nunca hay un estado en el que el dinero se haya debitado pero no acreditado. La atomicidad aquí no es algo opcional — es lo que mantiene los balances correctos.
Un punto práctico para desarrolladores backend: PostgreSQL trata cada sentencia individual como su propia transacción implícita a menos que hagas BEGIN explícitamente. Así que si emites dos sentencias UPDATE separadas sin un bloque de transacción, cada una se confirma de forma independiente. Esta es la causa raíz de muchos bugs «aplicados parcialmente» en código de aplicación que habla con la base de datos a través de un connection pooler o un ORM con la configuración por defecto de autocommit.
Niveles de aislamiento en PostgreSQL
SQL define cuatro niveles de aislamiento: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ y SERIALIZABLE. PostgreSQL implementa tres de ellos como comportamientos realmente distintos — READ UNCOMMITTED se comporta igual que READ COMMITTED debido a cómo funciona MVCC internamente.
El nivel que elijas decide lo que una transacción puede ver de las transacciones concurrentes. READ COMMITTED es el valor por defecto y suele ser lo que quieres: cada sentencia ve los datos confirmados antes de que esa sentencia comenzara. Si otra transacción confirma una actualización entre tus dos SELECT, el segundo SELECT verá el nuevo valor — lo cual puede sorprender a quienes esperan una vista estable.
REPEATABLE READ fija la instantánea al momento en que la transacción comienza, no a cada sentencia. SERIALIZABLE va un paso más allá: detecta violaciones de serialización y aborta una de las transacciones en conflicto con un error de fallo de serialización que la aplicación debe estar preparada para reintentar.

MVCC y bloqueos a nivel de fila
PostgreSQL utiliza Control de Concurrencia Multiversión (Multi-Version Concurrency Control). Cuando haces UPDATE de una fila, la base de datos no sobrescribe la versión antigua en su lugar. Escribe una nueva versión de fila y marca la antigua como muerta. Los lectores ven la versión que estaba confirmada en el momento en que se tomó su instantánea. Por eso una consulta de informe de larga duración no bloquea las escrituras entrantes, y viceversa — operan sobre versiones de fila diferentes.
Pero MVCC no elimina todo el bloqueo. Cuando dos transacciones intentan hacer UPDATE de la misma fila, PostgreSQL necesita un ganador, y el perdedor espera un bloqueo a nivel de fila hasta que la primera transacción confirme o se revierta. SELECT FOR UPDATE es la forma explícita de reclamar ese bloqueo antes de hacer lógica de negocio que dependa del valor actual.
El patrón de SELECT FOR UPDATE seguido de un UPDATE condicional es la solución estándar para la condición de carrera clásica donde dos peticiones leen cada una el valor, ambas deciden que es seguro, y ambas lo decrementan. Sin el bloqueo explícito, ambas transacciones leen el mismo conteo de stock original y vendes de más.
Errores comunes y conclusiones prácticas
Tres modos de fallo aparecen constantemente en código backend. Primero, las transacciones de larga duración mantienen instantáneas y evitan la limpieza de tuplas, por lo que la tabla se hincha y el rendimiento se degrada con el tiempo — cierra las transacciones rápidamente, no abras una al inicio de una petición HTTP y la dejes abierta a través de llamadas downstream. Segundo, los interbloqueos ocurren cuando dos transacciones adquieren bloqueos en órdenes opuestos; mantén el orden consistente en todo el código, o confía en que PostgreSQL detectará y abortará una de ellas para reintentar. Tercero, ignorar los fallos de serialización bajo SERIALIZABLE lleva a corrupción de datos — la base de datos te está diciendo que las operaciones no eran realmente seguras para ejecutarse en paralelo, y tu lógica de reintento es parte del contrato, no opcional.
La mentalidad práctica: trata el nivel de aislamiento y la elección de bloqueo como parte del diseño de tu aplicación, no como un detalle de ajuste de la base de datos. El READ COMMITTED por defecto con SELECT FOR UPDATE explícito alrededor de las secciones críticas resuelve la mayoría de bugs de concurrencia de forma limpia. Recurre a SERIALIZABLE cuando la corrección importa más que el rendimiento y puedes manejar los fallos.
Resumen
Las transacciones agrupan múltiples sentencias en una unidad atómica. Los niveles de aislamiento controlan lo que las transacciones concurrentes pueden ver del trabajo no confirmado de las demás. MVCC le da a PostgreSQL lecturas y escrituras no bloqueantes manteniendo múltiples versiones de fila, mientras que los bloqueos a nivel de fila resuelven conflictos cuando dos transacciones apuntan a la misma fila. Para los desarrolladores backend, las herramientas concretas son la disciplina de BEGIN/COMMIT, el patrón SELECT FOR UPDATE para actualizaciones propensas a condiciones de carrera, y un orden de bloqueo consistente para evitar interbloqueos. Elige el nivel de aislamiento de forma deliberada — el valor por defecto funciona para la mayoría de casos, pero REPEATABLE READ y SERIALIZABLE existen para situaciones donde el comportamiento por defecto no es lo suficientemente estricto.
Punto de control de la lección
Repasa lo aprendido y recibe comentarios sobre tu trabajo.