Redis: estrategias de caché que todo backend debería conocer

Diagrama de capas de memoria y caché

Redis es el caché in-memory dominante en backends modernos. Pero “poner Redis delante” no es estrategia — es ingrediente. Los equipos que usan Redis eficientemente han internalizado patrones específicos de interacción con la base de datos, diseño de TTL, e invalidación. Este artículo repasa los más importantes.

Los cuatro patrones principales

Cache-aside (lazy loading)

El patrón más común. La aplicación consulta primero al caché; si falla, consulta la base de datos y guarda el resultado en caché.

def get_user(user_id):
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 3600, json.dumps(user))  # TTL 1h
    return user

Pros: simple, resiliente si Redis cae (la app sigue funcionando con penalty). Contras: primer acceso siempre miss, riesgo de datos obsoletos.

Read-through

La aplicación consulta siempre al caché, y el caché se ocupa de rellenarse desde la base de datos cuando falta. Requiere biblioteca/proxy que medie.

Pros: lógica más simple en la aplicación. Contras: acoplamiento entre caché y base de datos, menos flexible.

Write-through

Al escribir, la aplicación actualiza simultáneamente caché y base de datos. Caché siempre refleja estado.

Pros: caché nunca está desactualizado. Contras: latencia de escritura aumenta (dos sistemas), si caché cae puede perderse si no hay write-back.

Write-behind (write-back)

La aplicación escribe solo al caché; un proceso asíncrono persiste a la base de datos después.

Pros: latencia de escritura mínima. Contras: riesgo de pérdida de datos si el caché cae antes del flush. Solo apropiado donde la durabilidad no es crítica.

Diseño de TTL

Decisión subestimada. TTL demasiado corto → muchos misses innecesarios. TTL demasiado largo → datos obsoletos. Reglas prácticas:

  • Datos que cambian predeciblemente: TTL = tiempo entre cambios. Ej. tipo de cambio cada 5 min → TTL 5-10 min.
  • Datos muy estables: TTL largo (horas o días). Perfil de usuario que cambia una vez cada varios meses.
  • Datos críticos para precisión: TTL corto + invalidación explícita en los escritores.
  • Precios, inventarios, datos que si desactualizados causan problemas: invalidación explícita, no solo TTL.

Un TTL bien elegido es uno de los mayores impactos en tasa de hit.

Thundering herd

Un problema clásico: muchas peticiones concurrentes llegan al mismo tiempo al miss del caché. Todas llaman a la base de datos simultáneamente, saturándola.

Soluciones:

  • Lock en caché: el primer miss toma un lock, los demás esperan y leen del caché una vez rellenado. Patrón “request coalescing”.
  • TTL con jitter: en lugar de TTL fijo, TTL = base + aleatorio entre 0 y N%. Evita que miles de claves expiren simultáneamente.
  • Early refresh: antes de que el TTL expire, si la clave está por caducar, una sola petición refresca en background mientras el resto sirve del valor aún válido.

Librerías como ReadWriteRedisCache o cache-stampede implementan estos patrones.

Invalidación: lo difícil

“There are only two hard things in Computer Science: cache invalidation and naming things.” Phil Karlton lo dijo con razón.

Tres estrategias de invalidación:

TTL only

Deja que expire. Simple, puede ser suficiente para datos no críticos. No funciona cuando “ahora” importa.

Invalidación explícita en escritores

Cada código que escribe en la base de datos también invalida las entradas de caché relacionadas. Requiere disciplina y documentación.

def update_user(user_id, data):
    db.update(...)
    redis.delete(f"user:{user_id}")
    redis.delete(f"user_list:active")  # invalidar queries relacionadas

Event-based invalidation

Un log de cambios en la base de datos (CDC) dispara eventos que invalidan el caché. Más desacoplado pero añade complejidad operativa.

Patrones específicos de Redis

Aprovechar features nativos:

  • SCAN en lugar de KEYS en producción. KEYS bloquea el servidor completo en bases grandes.
  • Pipelines para batch de operaciones. Un pipeline con 100 comandos reduce 100 RTTs a 1.
  • Scripts Lua para operaciones atómicas complejas (ej. “incrementa este contador y devuelve el valor si < umbral”).
  • Estructuras adecuadas: Sorted Set para leaderboards, HyperLogLog para cardinalidad aproximada, Stream para logs eventuales, GEO para geolocalización.

Cuándo no usar caché

Señal de que el caché no es la respuesta:

  • Tasa de hit baja (<50%). El caché solo añade latencia y complejidad.
  • Datos que cambian tan rápido que TTL sería <1s. El overhead del caché supera el valor.
  • Datos únicos por petición (búsquedas complejas con parámetros muy variables). No hay oportunidad de reuso.

Ver nuestra cobertura relacionada sobre cuándo seguir con RabbitMQ para colas — patrones similares de decisión basada en caso de uso.

Conclusión

Redis es herramienta poderosa, pero no mágica. Los equipos que le sacan partido han invertido en diseño consciente de patrón (cache-aside vs write-through), TTL adecuado al dominio, invalidación bien orquestada, y defensa contra thundering herd. Sin esos fundamentos, “meter Redis” frecuentemente empeora sistemas en vez de mejorarlos.

Síguenos en jacar.es para más sobre arquitectura backend, rendimiento y bases de datos.

Entradas relacionadas