Stack: PostgreSQL + Redis · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de datos Prerrequisito: Lección 11 — Migraciones
Objetivos
- Elegir entre Postgres y un almacén especializado con criterio (qué sacrificas con cada uno).
- Usar Redis como caché, contador atómico, lock distribuido y leaderboard — con TTLs razonables.
- Usar JSONB de PostgreSQL cuando el "NoSQL" que necesitas cabe en tu BD actual.
- Reconocer los anti-patrones: Mongo "porque es moderno", Redis como base de datos primaria.
1. Qué sacrificas con cada almacén
| Almacén | Gana | Sacrifica | En TicketFlow |
|---|---|---|---|
| PostgreSQL | ACID, constraints, SQL, EXPLAIN | escritura simple a escala extrema | la fuente de verdad (siempre) |
| Redis | latencia µs, atomics, TTL, estructuras | durabilidad (por defecto), sin SQL, RAM | caché, contadores, locks, colas (broker) |
| MongoDB | documentos flexibles, sharding fácil | joins/ACID multi-doc (mejoró, pero no es su punto) | catálogos de solo-lectura variados (aquí: no) |
| Elasticsearch | búsqueda de texto, agregaciones | consistencia, coste de sync | búsqueda de eventos (más adelante, si hay demanda) |
La regla senior: Postgres es la base de datos primaria; lo demás es especialización justificada por una métrica (latencia de disponibilidad, throughput de contador, búsqueda de texto). "Necesito NoSQL" no es una métrica.
2. Redis: los cinco usos que sí valen
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
# 1. Caché con TTL (disponibilidad, listado público)
r.setex(f"avail:{event_id}", 10, json.dumps(asientos_libres)) # TTL 10s: dato "fresco-ish"
# 2. Contador atómico (rate limit por IP, tickets por usuario)
r.incr(f"rate:{ip}:{minute}", ex=60)
# 3. Lock distribuido (evitar doble canje de tarjeta regalo en 2 pods)
lock = r.lock(f"gift:{card_code}", timeout=5, blocking_timeout=2)
with lock: # SET NX + expiración: el lock muere si el pod muere
descontar_del_ledger(...)
# 4. Leaderboard (top ventas) — sorted sets
r.zincrby("sales:ranking", importe, event_id)
r.zrevrange("sales:ranking", 0, 9, withscores=True)
# 5. Cola simple (broker de Celery: Lección 29)
r.lpush("jobs:emails", job_id)Lo que hay que saber: Redis es single-threaded (comandos atómicos: por eso INCR y SET NX resuelven concurrencia sin locks de tu parte), en memoria (la durabilidad depende de configuración RDB/AOF: no es tu fuente de verdad), y sin SQL (patrones de acceso por clave: si necesitas "todos los X con condición", no es Redis).
3. JSONB: el NoSQL que ya tienes
Campos semi-estructurados con índice, dentro de Postgres:
ALTER TABLE events_event ADD COLUMN metadata JSONB DEFAULT '{}';
CREATE INDEX idx_event_metadata ON events_event USING GIN (metadata);
SELECT * FROM events_event
WHERE metadata @> '{"venue_type": "stadium"}'; -- índice GIN lo sirveCuándo: atributos que varían por evento (configuración de aforo por zona, preferencias del organizador) sin schema que evolucione por migración. Qué pierdes: constraints por campo, NOT NULLs, tipos duros. Regla: JSONB para decoraciones; columnas para identidad y dinero.
4. Anti-patrones que verás (y cómo responder)
- "Mongo porque el schema cambia rápido" → el schema cambia por migraciones versionadas justamente para controlar ese cambio (Lección 11). Lo que cambia cada día no es el schema: es la disciplina.
- "Redis como BD primaria" → la cola pierde mensajes si Redis cae (persistencia parcial), no hay queries ad hoc, todo cabe en RAM. Redis es cofactor, no protagonista.
- "Polyglot everywhere" → cada almacén nuevo es: otro deploy, otro backup, otro failure mode, otro set de drivers. El coste se paga en operación (Módulo 9), no en el MVP.
- Cache stampede → 1000 peticiones golpean la BD al expirar la caché caliente. Antídoto: TTL con jitter, o lock de regeneración (solo un worker recalcula).
Autoevaluación
- ¿Qué tres preguntas haces antes de añadir un almacén nuevo al stack?
- ¿Por qué INCR de Redis resuelve el rate limit sin race condition y un
SELECT + UPDATEde Postgres (sin FOR UPDATE) no? - ¿Qué pasaría en producción si el lock de tarjeta regalo no tuviera TTL y el pod muera con el lock tomado?
- ¿Cuándo eliges JSONB sobre columnas reales, y qué constraint pierdes en el camino?
- El rate de aciertos de tu caché de disponibilidad es del 40% con TTL 10s. ¿Qué miras: TTL, clave, o que la consulta no debía cachearse?
Continúa con los ejercicios. Las soluciones solo tras intentarlo.