BD II · Transacciones
🔁 Guía de Aula Invertida

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.

Paso 01 · El problema

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 →

Paso 02 · BEGIN

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.

Paso 03 · Las dos operaciones

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.

Paso 04 · Atomicidad y Durabilidad

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.

Paso 05 · Consistencia

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.

Paso 06 · ¿Y el cuarto?

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 →

Diagrama en vivoPaso 1 / 6
Sin diagrama todavía — vamos a transferir $300 de la cuenta de Ana a la de Beto.

Practicá: ¿qué propiedad ACID es cada caso?

Cuatro situaciones, cuatro garantías distintas. Clasifica cada una.

Clasificación

Clasifica cada situación según la propiedad ACID que ilustra.

Si falla la mitad de una transferencia, se deshacen TODOS los cambios, no solo algunos.
Una restricción impide que el saldo de una cuenta quede en negativo después de cualquier operación.
Mientras una transacción está a mitad de camino, ninguna otra puede ver sus cambios todavía sin confirmar.
Una vez que el sistema confirma una compra, la información sobrevive incluso si el servidor se reinicia enseguida.

Paso 01 · El problema

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.

Paso 02 · BEGIN en paralelo

T1 y T2 arrancan

Cada cajero abre su propia transacción — Postgres no sabe (todavía) que están por tocar la misma fila.

Paso 03 · Ambos leen lo mismo

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.

Paso 04 · T1 actualiza (sin confirmar)

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.

Paso 05 · COMMIT de T1

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.

Paso 06 · El problema de T2

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.

Paso 07 · La prevención: candados

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.

Paso 08 · T2 espera

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.

Paso 09 · Cuando los candados chocan entre sí

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.

Diagrama en vivoPaso 1 / 9
Cuenta de Ana: $400. Dos cajeros van a retirar $300 cada uno, casi al mismo tiempo.

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íaQué 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.
PostgreSQL nunca permite la lectura sucia

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.

Emparejamiento

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.

NivelLectura suciaNo repetibleFantasma
Read UncommittedPermite (estándar) / previene (Postgres)PermitePermite
Read Committed (default)PrevienePermitePermite
Repeatable ReadPrevienePrevienePermite (estándar) / previene (Postgres)
SerializablePrevienePrevienePreviene
Postgres es más estricto que el papel

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.

probalo vos mismo

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.

Selección múltiple

¿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?

Selección múltiple

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.

HerramientaQué hace
SELECT ... FOR UPDATELee 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 / DELETEEn Postgres, siempre adquieren un candado de fila automáticamente — por eso dos UPDATE sobre la misma fila nunca corren a ciegas al mismo tiempo.
DeadlockDos transacciones se bloquean mutuamente, cada una esperando un candado que tiene la otra. Postgres lo detecta y aborta una con error.
Selección múltiple

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?

Selección múltiple

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.

0 / 0
actividades correctas en toda la guía
Selección múltiple

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?

Selección múltiple

¿Cuál de estas NO es una de las cuatro propiedades ACID?

Selección múltiple

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?

Completar

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érminoIdea clave
BEGIN / COMMIT / ROLLBACKAbrir, confirmar o deshacer una transacción.
AtomicidadTodo o nada: no hay resultados a medias.
ConsistenciaNinguna transacción rompe las reglas/restricciones de la base.
AislamientoQué ve una transacción de otra que corre al mismo tiempo.
DurabilidadLo confirmado sobrevive a una caída del sistema.
Read CommittedDefault de Postgres. Sin lecturas sucias; permite no repetibles y fantasmas.
Repeatable ReadSnapshot fijo de toda la transacción. En Postgres, previene también fantasmas.
SerializableSin ninguna anomalía; puede exigir reintentar por error de serialización.
Lectura suciaVer datos no confirmados de otra transacción. Imposible en Postgres.
Lectura no repetibleLa misma fila cambia de valor entre dos lecturas.
Lectura fantasmaAparecen o desaparecen filas entre dos consultas iguales.
FOR UPDATEBloquea la fila leída para que nadie más la modifique hasta el commit.
DeadlockDos 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.