🧪 Taller de Laboratorio
vistas

Transacciones: Atomicidad y Concurrencia en Código Real

Otra vez PostgreSQL de verdad, compilado a WebAssembly, corriendo enteramente en tu navegador. Vas a abrir, confirmar y deshacer transacciones reales con BEGIN/COMMIT/ROLLBACK, provocar en vivo el error real de Postgres cuando una transacción queda "abortada", y reproducir con tus propias manos el problema del lost update — antes de resolverlo con FOR UPDATE.

RC
Ing. Roy Carrasco
Facultad de Ingeniería de Sistemas · UAB

Bienvenido

Este taller es la práctica de laboratorio de transacciones (BEGIN/COMMIT/ROLLBACK), atomicidad y concurrencia en PostgreSQL. No vuelve a explicar la teoría desde cero —asume que ya viste la guía— y en cambio te da un PostgreSQL real para ejecutar cada paso con tus propias manos.

📎 Prerrequisito

Si todavía no viste la teoría, repasa primero Transacciones: ACID y Concurrencia antes de continuar. Este taller usa una tabla cuentas (con Ana y Beto, el mismo ejemplo de la guía) y, en el Reto final, una tabla productos para reproducir el escenario de la última unidad vendida dos veces.

⚠️ Todo vive en la memoria de esta pestaña — y en una única sesión compartida

Esta página corre su propia base de datos, independiente de cualquier otro taller que hayas abierto antes, y se pierde si recargás, cerrás la pestaña o navegás a otra guía. Hay un detalle extra que en este taller importa más que en los demás: todos los bloques de código comparten la misma sesión de Postgres (la misma conexión). Si un bloque abre una transacción con BEGIN y no la cierra con COMMIT o ROLLBACK, esa transacción sigue abierta y afecta a todos los bloques siguientes — vas a comprobarlo a propósito en la sección 2. Si en algún momento algo se comporta raro (por ejemplo, un error de "current transaction is aborted"), escribe ROLLBACK; en la Zona de pruebas libres para destrabar la sesión.

🔍 Qué SÍ y qué NO se puede probar con una sola conexión

PGlite (el Postgres que corre esta página) es una sola conexión, así que dos transacciones nunca están activas de verdad al mismo tiempo acá. Todo lo que es real de una sola transacción (BEGIN/COMMIT/ROLLBACK, la restricción CHECK abortando una transacción, FOR UPDATE como sintaxis) lo vas a ejecutar tal cual contra Postgres real. Lo que necesita dos conexiones reales bloqueándose entre sí (que T2 quede esperando el candado de T1, o un deadlock) no se puede reproducir acá — para eso ya viste el diagrama animado de la guía teórica. En la sección 3 vas a simular el intercalado escribiendo, en orden, la misma secuencia de sentencias que producirían el problema si dos cajeros la ejecutaran en paralelo: el enunciado te va a avisar cada vez que estés simulando algo en vez de ejecutando concurrencia real.

✍️ Lo escribís vos

Cada bloque parte vacío. El enunciado te dice qué tenés que lograr — el código SQL lo escribís de cero.

🔓 Una sesión, de punta a punta

Esa misma sesión compartida es lo que te permite comprobar, con Postgres real, qué pasa si una transacción queda abierta o abortada — algo que en una app normal casi nunca se ve tan de cerca.

🧪 Simulaciones, avisadas

Cada vez que un ejercicio simule concurrencia en vez de ejecutarla de verdad, el enunciado te lo va a decir explícitamente.

La base de datos está vacía. Baja a "Prepara tu base de datos" para empezar.

Zona de pruebas libres

Este bloque queda disponible durante toda la guía. Usalo en cualquier momento para probar una idea propia, revisar el estado de una tabla con SELECT * FROM tabla;, o escribir un ROLLBACK; suelto si necesitás destrabar la sesión — sin desordenar los bloques guiados.

🧪 Escribe cualquier sentencia SQL

Prepara tu base de datos

Este bloque no es un ejercicio — ya viene resuelto. Crea una tabla, cuentas, con dos filas: Ana y Beto. Ejecútalo tal cual, una sola vez, antes de seguir.

Qué hace este bloque

Crea cuentas con columnas id, titular y saldo, con una restricción CHECK (saldo >= 0) — ninguna cuenta puede quedar en negativo, sin importar qué UPDATE se intente. Ana arranca con Bs 400 y Beto con Bs 100, el mismo punto de partida que usa la guía teórica.

🏗️ Ejecuta esta preparación tal cual
Confirma los datos

El resultado final debería mostrarte 2 filas: Ana con 400.00 y Beto con 100.00. Si ves otra cosa, usa "🔄 Reiniciar base de datos" y vuelve a ejecutar este bloque.

1. BEGIN y COMMIT

Agrupar dos cambios para que se confirmen juntos, de forma permanente.

Enunciado — 1.1

Escribe una transacción completa que transfiera Bs 300 de Ana a Beto: abre con BEGIN, resta 300 al saldo de Ana, suma 300 al saldo de Beto, y confirma con COMMIT. Termina con un SELECT * FROM cuentas ORDER BY id; para comprobarlo.

🗄️ Ejercicio 1.1 — Transferencia con BEGIN...COMMIT
Los dos cambios, confirmados juntos

El SELECT final debería mostrar a Ana con Bs 100 y Beto con Bs 400. Nada de esto fue visible para ningún otro bloque mientras la transacción seguía abierta — recién quedó aplicado con el COMMIT.

1.2 — Antes de ejecutar, predice: ¿qué mostraría el SELECT si en vez de correr todo este bloque de una, hubieras dejado la transacción abierta (sin COMMIT) y hubieras corrido el SELECT en la Zona de pruebas libres? Escribe tu predicción como comentario en el editor, y después comprobalo de verdad: abre una transacción nueva, resta Bs 50 a Beto sin confirmar todavía, y ejecuta este mismo bloque.

🗄️ Ejercicio 1.2 — Abre una transacción y no la cierres todavía
Ahora ve a la Zona de pruebas libres

Escribe ahí SELECT * FROM cuentas;. Como estás en la misma sesión, vas a ver el saldo de Beto ya rebajado a Bs 350 — no porque otra transacción lo vea (Postgres nunca deja ver cambios sin confirmar de otra transacción), sino porque técnicamente seguís dentro de la misma transacción que abriste en 1.2. Cuando termines de mirar, volvé acá y ejecutá el siguiente bloque para confirmarla.

🗄️ Ejercicio 1.3 — Confirma la transacción que dejaste abierta

2. ROLLBACK y transacciones abortadas

Deshacer a propósito, y lo que pasa cuando Postgres deshace un error por vos.

Enunciado — 2.1

Abre una transacción, resta Bs 30 al saldo de Ana, pero en vez de confirmar, deshazla con ROLLBACK.

🗄️ Ejercicio 2.1 — Arrepentirse con ROLLBACK
Como si nunca hubiera pasado

El saldo de Ana sigue en Bs 100, sin ningún rastro de la resta. ROLLBACK deshace absolutamente todo lo hecho desde el BEGIN — no solo lo último.

Ahora un caso distinto: un ROLLBACK que Postgres te obliga a hacer.

Enunciado — 2.2

Ana tiene Bs 100. Abre una transacción e intenta restarle Bs 500 — más de lo que tiene. La restricción CHECK (saldo >= 0) debería rechazar el UPDATE. No escribas ROLLBACK todavía, dejá la transacción tal como quede después del error.

🗄️ Ejercicio 2.2 — Provoca la violación del CHECK
El error esperado

Debería aparecer un error que menciona violates check constraint. Hasta acá, ningún cambio se aplicó — pero la transacción sigue abierta, porque nunca llegaste a un COMMIT ni a un ROLLBACK.

Enunciado — 2.3

Sin haber cerrado esa transacción, intenta algo completamente inocente: SELECT * FROM cuentas; — una consulta que ni siquiera toca la fila que falló.

🗄️ Ejercicio 2.3 — Intenta seguir usando la transacción abortada
"current transaction is aborted"

Este SELECT también falla, con un mensaje del estilo current transaction is aborted, commands ignored until end of transaction block — exactamente el detalle real de Postgres que menciona la guía teórica: apenas una sentencia falla dentro de una transacción, toda la transacción queda abortada, y cualquier otra sentencia que intentes en ella (aunque sea válida) va a fallar de la misma forma hasta que hagas ROLLBACK.

⚠️ No sigas sin ejecutar este bloque

Mientras esta transacción siga abierta y abortada, cualquier sentencia que intentes en el resto del taller (incluida la Zona de pruebas libres) va a fallar con el mismo error. Ejecuta ROLLBACK; para destrabar la sesión.

🗄️ Ejercicio 2.4 — Destraba la sesión con ROLLBACK
Ana sigue con Bs 100

El intento de resta de Bs 500 nunca llegó a aplicarse — Atomicidad y Consistencia trabajando juntas: la restricción impidió un estado inválido, y el ROLLBACK dejó la base exactamente como estaba antes del intento.

3. Simulando un lost update

Esto es una simulación, no concurrencia real: vas a escribir, en un solo bloque y en orden, la misma secuencia de sentencias que producirían el problema si dos cajeros la ejecutaran al mismo tiempo contra la cuenta de Ana.

El escenario

Ana tiene Bs 100. Dos cajeros distintos, casi al mismo tiempo, leen ese saldo (ven Bs 100 los dos) y cada uno decide retirarle Bs 80. Cada cajero calcula su propia resta en base al valor que leyó (100 − 80 = 20) y la aplica con un UPDATE que escribe ese número ya calculado — así es como muchas aplicaciones reales arman este tipo de operación, sin bloquear nada.

Enunciado — 3.1

Escribe, en este orden, dentro de un mismo bloque: una transacción que simula al "Cajero 1" (calculó 100 − 80 = 20 y confirma), seguida de otra transacción que simula al "Cajero 2" (calculó el mismo 100 − 80 = 20, porque leyó el saldo antes de que el Cajero 1 confirmara, y también confirma). Termina con un SELECT del saldo de Ana.

🗄️ Ejercicio 3.1 — Dos retiros simulados, el mismo dato viejo
Bs 20, como si solo hubiera pasado un retiro

El saldo final es Bs 20 — el mismo resultado que si Ana hubiera retirado Bs 80 una sola vez. En realidad se confirmaron dos retiros de Bs 80 (Bs 160 en total), pero el segundo UPDATE pisó por completo el resultado del primero sin enterarse de que ya había cambiado. El retiro del Cajero 1 quedó perdido — de ahí el nombre lost update. Ninguna restricción CHECK lo detecta, porque Bs 20 sigue siendo un número válido (≥ 0): el problema no es un valor imposible, es un valor incorrecto.

4. FOR UPDATE en la práctica

La sintaxis que, con dos conexiones reales, evita justo el problema anterior.

Por qué esto no se puede demostrar del todo acá

Con una sola conexión no hay ningún "Cajero 2" real esperando un candado — así que este ejercicio practica la sintaxis y el patrón correcto, no el bloqueo en sí. En un escenario real con dos conexiones, FOR UPDATE obliga a la segunda transacción a esperar a que la primera confirme, y recién ahí le deja leer el saldo ya actualizado — nunca el valor viejo que causó el lost update de la sección anterior. Eso es exactamente lo que anima el diagrama de la guía teórica.

Enunciado — 4.1

Primero, reiniciá el saldo de Ana a Bs 100 (para partir de un número limpio). Después, dentro de una transacción, usa SELECT saldo FROM cuentas WHERE titular = 'Ana' FOR UPDATE; para leer y bloquear la fila, calcula el nuevo saldo restando Bs 80 a partir de ese resultado, y confirma.

🗄️ Ejercicio 4.1 — Leer con candado, calcular, confirmar
El patrón, no solo el resultado

El resultado (Bs 20) es el mismo que ya viste — lo que cambia es cómo se llegó ahí: el valor restado salió de una lectura hecha dentro de la misma transacción que hizo el bloqueo, no de un número calculado antes y guardado aparte. Esa diferencia es exactamente lo que, con dos conexiones reales, cierra la puerta al lost update de la sección 3.

Reto final: la última unidad, vendida dos veces

El mismo problema de la sección 3, ahora con inventario — y resuelto con el patrón de la sección 4.

Prepara la tabla productos

Este bloque ya viene resuelto. Crea productos con un CHECK (stock >= 0) y una sola fila: un póster con stock = 1.

🏗️ Ejecuta esta preparación tal cual
Requisito 1 — Reproduce el problema (simulado)

Igual que en la sección 3: simula a dos clientes que leyeron stock = 1 casi al mismo tiempo y ambos confirman su compra calculando stock = 0. Un solo bloque, dos transacciones secuenciales (Cliente A y Cliente B), terminando con un SELECT del stock.

🏆 Requisito 1 — Dos compras confirmadas, una sola unidad
Dos ventas confirmadas, un solo poster

El stock queda en 0, pero se confirmaron dos compras: el sistema le cobró a dos clientes distintos la misma unidad. Esto es exactamente el escenario que ya viste en la pregunta de la guía teórica sobre la tienda en línea.

Requisito 2 — Resuélvelo con el patrón seguro

Reinicia el stock a 1. Después, escribe dos intentos de compra, uno después del otro, usando siempre SELECT ... FOR UPDATE antes de decidir: el primer intento debe descontar el stock (queda en 0). El segundo intento debe leer el stock ya en 0 y, como no queda unidad disponible, terminar en ROLLBACK en vez de vender.

🏆 Requisito 2 — Dos intentos, uno vende y el otro se frena solo
Bonus — deja que el CHECK sea la última red de seguridad

Sin usar ROLLBACK esta vez: con el stock otra vez en 0, intenta directamente UPDATE productos SET stock = stock - 1 WHERE nombre = 'Póster edición limitada';. Debería fallar con violates check constraint — aunque el patrón con FOR UPDATE ya evita llegar a este punto, la restricción sigue ahí por si algún código en algún lugar se olvida de chequear antes de escribir.

🏆 Bonus — La red de seguridad del CHECK

Repaso

Preguntas cortas sobre lo que acabas de observar en vivo. Tu progreso se guarda automáticamente en este navegador.

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

En el ejercicio 1.1, si el sistema se hubiera caído justo después de restarle a Ana y antes de sumarle a Beto (sin COMMIT), ¿qué garantiza que ninguno de los dos cambios queda aplicado?

Verdadero o falso

En este taller, si un bloque deja una transacción abierta (BEGIN sin COMMIT ni ROLLBACK), el siguiente bloque que ejecutes corre dentro de esa misma transacción, porque todos comparten la misma sesión de Postgres.

Selección múltiple

En la sección 2, el UPDATE del ejercicio 2.2 falló por la restricción CHECK. El SELECT del ejercicio 2.3, que ni siquiera tocaba esa fila, también dio error. ¿Por qué?

Selección múltiple

En la simulación de la sección 3, el Cajero 1 y el Cajero 2 calcularon el mismo saldo final (Bs 20) para la cuenta de Ana. ¿Qué reveló exactamente ese resultado?

Selección múltiple

Con dos conexiones reales (no en esta demo de una sola sesión), ¿por qué agregar FOR UPDATE al SELECT inicial evita el lost update de la sección 3?

Clasificación

Clasifica cada situación observada en este taller según el concepto que ilustra.

El UPDATE violó el CHECK y todas las sentencias siguientes fallaron hasta el ROLLBACK.
Dos transacciones leyeron el mismo saldo, y la segunda sobrescribió el resultado de la primera.
Un ROLLBACK deshizo por completo un cambio a mitad de camino, sin dejar ningún rastro.
Bloquear la fila leída para que nadie más la modifique hasta el commit.

Completar

Completa según lo que ejecutaste en este taller.

1. Para abrir una transacción se usa .

2. Para confirmar sus cambios de forma permanente se usa .

3. Para deshacer todo lo hecho desde el BEGIN se usa .

4. Para leer una fila y además bloquearla contra escritura de otras transacciones se agrega al SELECT.

Emparejamiento

Une cada término con lo que hace.

Cheat Sheet del taller

FormaPara qué sirve
BEGIN;Abre una transacción. Nada queda confirmado hasta el COMMIT.
COMMIT;Confirma todos los cambios de la transacción, de forma permanente.
ROLLBACK;Deshace todos los cambios hechos desde el BEGIN.
CHECK (columna >= valor)Restricción que rechaza un cambio inválido y, si falla en plena transacción, la aborta entera.
current transaction is abortedAviso de Postgres: una sentencia anterior falló en esta transacción — hace falta ROLLBACK.
SELECT ... FOR UPDATELee una fila y la bloquea contra escritura hasta que la transacción termine.
Lost updateUn UPDATE con datos viejos sobrescribe, sin saberlo, un cambio ya confirmado.
Sesión compartida (de este taller)Todos los bloques usan la misma conexión: una transacción sin cerrar afecta a los siguientes.

¿Sabías que...?

🔓 Una sola sesión, a propósito

A diferencia de una app real (que usa un pool de varias conexiones), este taller corre todo en una única sesión de Postgres — por eso pudiste comprobar en vivo qué pasa cuando una transacción queda abierta o abortada, algo que en producción casi nunca se ve tan de cerca.

🏦 El lost update pasa todos los días

El patrón "leer un valor en el código de la app, calcular el nuevo valor ahí, y recién después escribirlo" es extremadamente común — y es exactamente el que produce un lost update si nadie bloquea la fila entre la lectura y la escritura.

🧪 Con dos pestañas reales, sí se ve el bloqueo

Si esta misma base de datos viviera en un servidor Postgres real (no en la memoria de una pestaña), abrir dos conexiones distintas y repetir el ejercicio de la sección 4 sí mostraría a la segunda conexión quedando literalmente esperando el candado de la primera — la limitación de hoy es solo de esta demo en memoria, no de PostgreSQL.