Transacciones: ACID y Concurrencia
Hasta ahora escribiste sentencias SQL sueltas. Pero en un sistema real, varias de esas sentencias tienen que comportarse como una sola unidad — y varios usuarios las ejecutan al mismo tiempo, sobre las mismas filas. Esta guía es sobre esas dos cosas: qué garantiza una transacción, y qué pasa cuando dos ocurren a la vez.
👁 — vistas · Ing. Roy Carrasco, Facultad de Ingeniería de Sistemas · UAB
Scrollea para empezar — el diagrama de la transferencia bancaria se arma solo, capa por capa, mientras leés.
Una operación que no puede quedar a medias
Ana le transfiere $300 a Beto. Esa transferencia en realidad son dos cambios en la base de datos: restarle a Ana y sumarle a Beto. Si el sistema se cae justo entre esos dos cambios, ¿qué pasa?
Una transacción agrupa varias sentencias SQL para que se comporten como una sola unidad. Armemos el diagrama con este mismo caso →
Abrir la transacción
Todo empieza con BEGIN (o START TRANSACTION): a partir de acá, ninguna sentencia queda confirmada en la base hasta que llegue un COMMIT explícito.
Ambos cambios, agrupados
Dentro de la transacción van las dos sentencias: restarle $300 a Ana, sumarle $300 a Beto. Hasta acá, ninguno de los dos cambios es visible para nadie más — ni siquiera se sabe todavía si van a quedar.
Todo o nada
Acá se bifurcan dos finales posibles. Si todo sale bien, COMMIT confirma los dos cambios juntos y de forma permanente (Durabilidad: sobreviven aunque el servidor se apague al segundo siguiente). Si algo falla a mitad de camino, ROLLBACK deshace absolutamente todo, incluida la resta que ya se había aplicado a Ana.
Eso es Atomicidad: o se aplican los dos cambios, o ninguno.
Una restricción que nunca se rompe
¿Y si alguien intenta restarle $2000 a Ana, que solo tiene $400? Una restricción CHECK (saldo >= 0) rechaza esa sentencia de inmediato, antes de que se aplique. Eso es Consistencia: ninguna transacción puede dejar la base en un estado que rompa sus propias reglas.
Detalle real de Postgres: apenas una sentencia falla dentro de una transacción, esa transacción entera queda "abortada" — cualquier otra sentencia que intentes en ella da error, hasta que hagas ROLLBACK.
Aislamiento: hace falta una segunda transacción
Ya viste Atomicidad, Consistencia y Durabilidad con una sola transacción. Pero Aislamiento — qué ve una transacción de los cambios de otra mientras ambas corren al mismo tiempo — recién se entiende con dos transacciones a la vez.
Sigamos scrolleando →
Practicá: ¿qué propiedad ACID es cada caso?
Cuatro situaciones, cuatro garantías distintas. Clasifica cada una.
Clasifica cada situación según la propiedad ACID que ilustra.
Dos cajeros, la misma cuenta
La cuenta de Ana tiene $400. Dos cajeros automáticos distintos intentan retirar $300 cada uno, casi al mismo tiempo. Si los dos creen que hay $400 disponibles, algo va a salir mal.
T1 y T2 arrancan
Cada cajero abre su propia transacción — Postgres no sabe (todavía) que están por tocar la misma fila.
Los dos ven $400
T1 y T2 leen el saldo de Ana por separado. Los dos ven $400, porque ninguno de los dos hizo ningún cambio todavía.
Un cambio que todavía nadie más ve
T1 calcula 400 − 300 = 100 y hace el UPDATE, pero no hizo COMMIT. Si T2 volviera a consultar ahora mismo, seguiría viendo $400 — eso sería una lectura sucia (dirty read) si Postgres se lo dejara ver. No se lo deja: ninguna transacción ve cambios no confirmados de otra, en ningún nivel de aislamiento de Postgres.
Ahora sí es real
T1 confirma. Recién en este momento el saldo de Ana es realmente $100 para cualquiera que lo consulte de nuevo.
Decidir con un dato viejo
T2 sigue con la transacción abierta desde el Paso 03, y calculó su resta con el $400 que leyó entonces — no con el $100 real de ahora. Si T2 aplica su UPDATE tal cual, Beto termina recibiendo $300 que en realidad ya no estaban: un lost update.
Esto puede pasar en Read Committed (el nivel por defecto de Postgres) cuando la aplicación lee y escribe en dos pasos separados.
SELECT ... FOR UPDATE
Para evitar justo esto, T1 puede pedir el candado de la fila al leerla: SELECT saldo FROM cuentas WHERE id=1 FOR UPDATE. Nadie más puede modificar (ni bloquear para modificar) esa fila hasta que T1 termine.
Bloqueada, no rota
Ahora T2 intenta tocar la misma fila y queda esperando: no falla, no avanza con datos viejos, simplemente espera. Cuando T1 hace COMMIT y libera el candado, T2 recién ahí lee el saldo real ($100), se da cuenta de que no alcanza para retirar $300, y hace ROLLBACK. La condición de carrera ya no puede pasar.
Deadlock
Ahora imaginá que T1 transfiere de la cuenta A a la B (bloquea A, después pide B) mientras T2 transfiere de B a A (bloquea B, después pide A) al mismo tiempo. Cada una espera un candado que tiene la otra: un deadlock.
Postgres detecta el ciclo (por defecto, revisa cada 1 segundo) y aborta una de las dos transacciones con un error, dejando que la otra siga.
Anomalías de lectura
Tres formas concretas en que una transacción puede ver datos "raros" por culpa de otra que corre al mismo tiempo.
| Anomalía | Qué pasa |
|---|---|
| Lectura sucia (dirty read) | Ver datos de otra transacción que todavía no hizo COMMIT — y que podría hacer ROLLBACK después. |
| Lectura no repetible (non-repeatable read) | Leer la misma fila dos veces en la misma transacción y obtener valores distintos, porque otra transacción confirmó un cambio en el medio. |
| Lectura fantasma (phantom read) | Repetir una consulta con condición (WHERE) y que aparezcan o desaparezcan filas enteras, por un INSERT/DELETE de otra transacción en el medio. |
Aunque el estándar SQL lo permite en el nivel más bajo (Read Uncommitted), PostgreSQL ni siquiera implementa ese nivel de verdad: si lo pedís, internamente te da Read Committed. Nunca vas a ver, en Postgres, datos de una transacción que no hizo commit todavía.
Une cada anomalía con su definición.
Niveles de aislamiento
El estándar SQL define cuatro niveles, de menos a más estricto. PostgreSQL implementa tres realmente distintos — y uno de ellos se comporta más estricto de lo que pide el estándar.
| Nivel | Lectura sucia | No repetible | Fantasma |
|---|---|---|---|
| Read Uncommitted | Permite (estándar) / previene (Postgres) | Permite | Permite |
| Read Committed (default) | Previene | Permite | Permite |
| Repeatable Read | Previene | Previene | Permite (estándar) / previene (Postgres) |
| Serializable | Previene | Previene | Previene |
En PostgreSQL, Repeatable Read se implementa tomando una única foto (snapshot) de toda la base al empezar la transacción — no solo de las filas ya leídas. Por eso, a diferencia del estándar SQL, en Postgres ese nivel también previene la lectura fantasma. Es una de esas veces en que el motor real es más generoso que la especificación mínima.
Elegí una anomalía y un nivel de aislamiento, y mirá si PostgreSQL real la previene o no. Es exploratorio: no suma a tu progreso.
—
¿Cuál de estos niveles de aislamiento de PostgreSQL previene la lectura fantasma, a diferencia de lo que exige el estándar SQL para ese mismo nombre de nivel?
Una transacción en nivel SERIALIZABLE falla con un error de serialización (código 40001) aunque su SQL sea perfectamente válido. ¿Qué corresponde hacer?
Bloqueos y deadlocks
Los niveles de aislamiento definen qué ve una transacción. Los bloqueos (locks) son la herramienta para decidir, de forma explícita, quién puede tocar una fila mientras otra la está usando.
| Herramienta | Qué hace |
|---|---|
SELECT ... FOR UPDATE | Lee una fila y además adquiere un candado sobre ella: ninguna otra transacción puede modificarla (ni bloquearla para modificar) hasta que la primera termine. |
UPDATE / DELETE | En Postgres, siempre adquieren un candado de fila automáticamente — por eso dos UPDATE sobre la misma fila nunca corren a ciegas al mismo tiempo. |
| Deadlock | Dos transacciones se bloquean mutuamente, cada una esperando un candado que tiene la otra. Postgres lo detecta y aborta una con error. |
T1 ejecuta SELECT saldo FROM cuentas WHERE id=1 FOR UPDATE dentro de una transacción, sin hacer COMMIT todavía. Si T2 intenta un UPDATE sobre esa misma fila, ¿qué pasa?
T1 tiene bloqueada la fila A y espera la fila B; T2 tiene bloqueada la fila B y espera la fila A. ¿Qué hace PostgreSQL en este caso?
Actividades de repaso
Un repaso integrador de todo el módulo. Cada actividad se corrige al instante; tu progreso se guarda automáticamente en este navegador.
Un cajero automático descuenta $300 de una cuenta, pero el sistema se cae justo antes de acreditárselo a la cuenta destino, dentro de la misma transacción. Al reiniciar, ¿qué debería haber pasado con la cuenta origen?
¿Cuál de estas NO es una de las cuatro propiedades ACID?
Una tienda en línea vende la última unidad de un producto a dos clientes distintos, porque ambas transacciones leyeron el mismo stock antes de que ninguna hiciera commit. ¿Qué evita este problema de forma confiable?
Repasemos el vocabulario de esta guía.
1. En PostgreSQL, el nivel de aislamiento por defecto es .
2. La propiedad ACID que garantiza que una transacción confirmada sobrevive a una caída del servidor es la .
3. Cuando dos transacciones se bloquean mutuamente esperando la fila que tiene la otra, eso se llama .
4. SELECT ...
adquiere un candado sobre la fila leída, evitando que otra transacción la modifique hasta el commit.
Cheat Sheet
| Término | Idea clave |
|---|---|
BEGIN / COMMIT / ROLLBACK | Abrir, confirmar o deshacer una transacción. |
| Atomicidad | Todo o nada: no hay resultados a medias. |
| Consistencia | Ninguna transacción rompe las reglas/restricciones de la base. |
| Aislamiento | Qué ve una transacción de otra que corre al mismo tiempo. |
| Durabilidad | Lo confirmado sobrevive a una caída del sistema. |
| Read Committed | Default de Postgres. Sin lecturas sucias; permite no repetibles y fantasmas. |
| Repeatable Read | Snapshot fijo de toda la transacción. En Postgres, previene también fantasmas. |
| Serializable | Sin ninguna anomalía; puede exigir reintentar por error de serialización. |
| Lectura sucia | Ver datos no confirmados de otra transacción. Imposible en Postgres. |
| Lectura no repetible | La misma fila cambia de valor entre dos lecturas. |
| Lectura fantasma | Aparecen o desaparecen filas entre dos consultas iguales. |
FOR UPDATE | Bloquea la fila leída para que nadie más la modifique hasta el commit. |
| Deadlock | Dos transacciones esperándose mutuamente. Postgres aborta una. |
¿Sabías que...?
🔤 ACID no siempre existió como sigla
El término lo acuñaron Andreas Reuter y Theo Härder en 1983, para nombrar formalmente algo que los sistemas de bases de datos ya intentaban garantizar desde antes, sin un acrónimo que lo resumiera.
📸 El Serializable de Postgres es más nuevo de lo que parece
PostgreSQL implementa Serializable con una técnica llamada Serializable Snapshot Isolation (SSI), publicada en un paper académico de 2008 — bastante más reciente que el resto del motor de transacciones.
⏱️ Detectar un deadlock no es instantáneo
Postgres no revisa deadlocks todo el tiempo: por defecto, espera 1 segundo (deadlock_timeout) antes de buscar el ciclo. Es una decisión a propósito: revisar todo el tiempo sería carísimo, y los deadlocks reales son poco frecuentes.