Módulo 2 · Bases de datos

Lección 10 — Transacciones

ACID, niveles de aislamiento, bloqueos y concurrencia optimista/pesimista en la reserva de asientos.

Publicada
En esta lección
  1. Ejercicio 1 — La raza
  2. Ejercicio 2 — FOR UPDATE
  3. Ejercicio 3 — Deadlock
  4. Ejercicio 4 — Aislamiento
  5. Ejercicio 5 — La transacción definitiva
  6. Resumen del profesor

Ejercicio 1 — La raza

  1. Con constraint: 1 reserva creada + 1 IntegrityError (la BD rechaza al perdedor). Los datos sobreviven; la experiencia del perdedor depende de tu manejo (409, no 500).
  2. Sin constraint: 2 reservas del mismo asiento — el negocio vendió dos veces la misma silla. Este es EL experimento del curso: la integridad vive en la BD, no en la buena voluntad del código (00b/03/10, la tríada).

Ejercicio 2 — FOR UPDATE

  1. El thread 2 espera en su SELECT hasta el commit del 1; al despertar ve el asiento ocupado → su rama de negocio lanza conflicto. 1 reserva. 2. nowait lanza DatabaseError/OperationalError (psycopg: could not obtain lock) — se traduce a 409 Conflict con mensaje de dominio. 3. Con skip_locked cada thread toma su fila libre sin bloquearse: es el patrón workers (29).

Ejercicio 3 — Deadlock

  1. PostgreSQL detecta el círculo y mata UNA transacción: Deadlock detected (psycopg: DeadlockDetected), con rollback de la víctima. La otra continúa.
  2. Con orden global por id, el círculo no se forma: el segundo thread espera el primer lock en el mismo asiento y luego toma el otro ya libre.
  3. Reintento: captura django.db.utils.OperationalError (o el código 40P01) y reejecuta la función una vez tras 50-100ms. Con orden global no hará falta, pero la red de seguridad es gratis — y el patrón reaparece con SERIALIZABLE.

Ejercicio 4 — Aislamiento

  1. READ COMMITTED: A ve el precio nuevo en su segunda lectura dentro de la misma transacción (cada statement lee el snapshot más reciente): lecturas no repetibles, tal como el nivel promete.
  2. REPEATABLE READ: A sigue viendo el precio viejo (snapshot al inicio de la transacción).
  3. Regla: bloqueo puntual (FOR UPDATE) para filas concretas que vas a escribir; nivel SERIALIZABLE/REPEATABLE READ para invariantes multi-fila complejas que asumen reintentos. TicketFlow: READ COMMITTED + bloqueos — el 95% de los backends.

Ejercicio 5 — La transacción definitiva

  1. 5 threads / 5 asientos: 5 reservas 201; los bloqueos son por filas distintas → sin contención (si bloquearas la TABLA entera, los verías serializarse — esta es la diferencia entre lock de fila y de tabla).
  2. Mismo asiento: 1×201 y 1×409 con cuerpo de dominio (seat_already_reserved).
  3. Queries esperadas: ~4-6 (SELECT for update, checks, INSERT reserva, INSERT items). Tiempo total de las 5: del orden de decenas de ms (los bloqueos paralelos por fila no se pelean).

Resumen del profesor

  • atomic() para atomicidad; FOR UPDATE para exclusión; orden global para no abrazarse; constraint como red de seguridad; lo lento FUERA de la transacción.
  • READ COMMITTED + bloqueos explícitos es la receta del 95%; SERIALIZABLE se paga con reintentos.
  • Esta lección cierra el círculo que abrió la 00b: la invariante I1 ahora es demostrable bajo concurrencia real.

Después de la corrección: Lección 11 — Migraciones versionadas.