Stack: PostgreSQL · Proyecto: TicketFlow Estado: Publicada — la lección más importante del módulo de datos Prerrequisito: Lección 09 — Índices
Objetivos
- Explicar ACID con ejemplos de TicketFlow, no con definiciones de manual.
- Elegir nivel de aislamiento sabiendo qué anomalia evita cada uno.
- Usar
select_for_updatey transacciones atómicas en Django para reservas concurrentes. - Escribir la transacción de reserva definitiva (la que sobrevive al pico de venta).
1. ACID en una transacción real
La transacción de reserva agrupa: INSERT reservation + INSERT items + (luego) UPDATE payment. Sin transacción:
- Atomicidad: falla el INSERT del ítem → queda la reserva huérfana. Con transacción: rollback total o nada.
- Consistencia: las constraints (I1) se comprueban al commit — la transacción nunca deja la BD en estado imposible.
- Aislamiento: dos reservas concurrentes del mismo asiento no se ven a medias (el corazón de esta lección).
- Durabilidad: el COMMIT confirmado sobrevive al reinicio (WAL de PostgreSQL).
En Django: from django.db import transaction + with transaction.atomic(): — el bloque entero o nada. Las excepciones hacen rollback; el return, commit.
2. Niveles de aislamiento: qué anomalia elimina cada uno
| Nivel | Fenómenos que SÍ permite | Evita |
|---|---|---|
| READ COMMITTED (default PG) | lecturas no repetibles, fantasma | lecturas sucias |
| REPEATABLE READ | fantasma (en PG: snapshot) | no repetibles |
| SERIALIZABLE | — | todo (con reintentos) |
La lectura no repetible en TicketFlow: lees el saldo de la tarjeta regalo (100), otro proceso gasta 80, tú gastas 70 con el saldo viejo → -50. No es problema del nivel por sí solo: es check-then-act sin bloqueo (Lección 03). Los niveles controlan lecturas; el concurrent-write se controla con bloqueos.
Decisión práctica: quedas en READ COMMITTED + bloqueos explícitos (SELECT... FOR UPDATE) en el flujo crítico. SERIALIZABLE solo si el dominio lo exige y asumes reintentos por serialization_failure.
3. Bloqueos: el SELECT FOR UPDATE
with transaction.atomic():
seat = Seat.objects.select_for_update().get(pk=seat_id) # fila bloqueada
if seat.esta_libre():
item = ReservationItem.objects.create(...) # nadie más toca esta fila aquíFOR UPDATE bloquea la fila elegida hasta el commit/rollback: el segundo request espera en su SELECT y, al despertar, ve el estado ya confirmado. Variantes que debes conocer:
nowait=True: no espera, lanza error (para responder 409 al instante).skip_locked=True: salta filas bloqueadas (patrón de workers de cola: cada worker toma reservas distintas — Lección 29).- Deadlock: dos transacciones que bloquean asientos en orden distinto — la regla de orden global de la 03 reaparece: bloquea siempre ordenado (
.order_by("id")en el select_for_update del queryset).
4. La transacción de reserva definitiva
from django.db import transaction, IntegrityError
@transaction.atomic
def reservar(event_id, seat_ids, user):
# 1. Bloquea asientos en orden determinista (anti-deadlock)
seats = list(
Seat.objects.select_for_update()
.filter(pk__in=seat_ids, event_id=event_id)
.order_by("id")
)
if len(seats) != len(seat_ids):
raise Conflict("asiento inexistente")
# 2. Reglas de negocio con las filas ya bloqueadas
if any(s.esta_reservado() for s in seats):
raise Conflict("asiento ocupado")
evento_debe_estar_abierto(seats[0].event) # I4
# 3. Escrituras atómicas
r = Reservation.objects.create(
user=user, event=seats[0].event,
status=ReservationState.PENDING_PAYMENT,
expires_at=now() + timedelta(minutes=10), # I2
)
ReservationItem.objects.bulk_create([
ReservationItem(reservation=r, seat=s, price_at_purchase=s.precio()) for s in seats
])
return rPor qué funciona sin raza: el bloqueo convierte check-then-act en check-then-act con exclusión mutua. La constraint UNIQUE de la 00b queda como red de seguridad (defense in depth): si un camino se salta el bloqueo, la BD rechaza con IntegrityError → 409.
Regla de oro de alcance: dentro de atomic() no hay HTTP externo, ni sleep, ni llamadas lentas (pasarela). La transacción sostiene bloqueos: corta y libera. Lo externo va después (outbox/cola: Lección 30/32).
Autoevaluación
- ¿Qué garantía de ACID falla si el proceso muere entre el INSERT de reserva y el de ítems sin transacción? ¿Y con transacción?
- ¿Por qué "subir a SERIALIZABLE" no arregla el saldo de la tarjeta regalo por sí solo? ¿Qué es la respuesta correcta?
- ¿Qué hace el segundo request exactamente cuando el primero tiene
FOR UPDATEsobre la fila? ¿Y connowait? ¿Yskip_locked? - ¿Por qué el
select_for_update().order_by("id")evita deadlocks en reservas de varios asientos? - ¿Qué operaciones JAMÁS van dentro de
transaction.atomic()y por qué?
Continúa con los ejercicios. Las soluciones solo tras intentarlo.