Una consulta que tarda 200ms contra tu base de datos sigue tardando 200ms la millonésima vez que alguien la ejecuta, y cada una de esas ejecuciones compite por las mismas conexiones, el mismo I/O de disco y la misma CPU. Redis guarda la respuesta en memoria, en un servidor que no hace nada más, así que la lectura número un millón cuesta un round trip de red en lugar de un plan de consulta.

Qué hace Redis realmente como cache

Redis es un almacén clave-valor en memoria. Los valores viven en RAM, así que las lecturas y escrituras se miden en tiempos por debajo del milisegundo en lugar de las decenas o cientos de milisegundos que cuesta una consulta relacional en cuanto entran en juego joins, índices bajo presión o un pool de conexiones ocupado. Se coloca junto a tu base de datos, no en su lugar: tu aplicación pregunta primero a Redis y va a la base de datos solo cuando Redis no tiene la respuesta.

El patrón para esto se llama cache-aside (también “lazy loading”), y es el primero al que recurre la mayoría de las aplicaciones:

  1. La aplicación pide una clave a Redis.
  2. Cache hit — Redis la tiene, la devuelve. La base de datos ni se entera de la petición.
  3. Cache miss — Redis no la tiene. Consulta la base de datos, escribe el resultado en Redis y luego lo devuelve.
1
2
3
4
5
Request → ¿Redis? ──hit──→ devuelve el valor
            miss
          Base de datos → escribe en Redis → devuelve el valor

La base de datos sigue siendo la fuente de verdad. Redis guarda una copia rápida y desechable de las partes que lees a menudo. Si Redis se reinicia vacío, no pierdes nada: se rellena solo en la siguiente ronda de cache misses.

Cómo ejecutar Redis en Docker

La forma más rápida de tener una instancia de Redis es la imagen oficial. Si nunca has usado Docker, Qué es Docker y para qué sirve cubre lo básico antes de seguir.

1
docker run --name redis-cache -d -p 6379:6379 redis:8

Esto arranca Redis 8 en segundo plano, publicando el puerto 6379 en tu máquina. Conéctate con redis-cli, instalado localmente o ejecutado desde un segundo contenedor en la misma red:

1
docker run -it --rm --network container:redis-cache redis:8 redis-cli
1
2
3
4
5
6
127.0.0.1:6379> SET user:42:name "Elena"
OK
127.0.0.1:6379> GET user:42:name
"Elena"
127.0.0.1:6379> TTL user:42:name
(integer) -1

Que TTL devuelva -1 significa que la clave no tiene expiración: se queda hasta que la borres o Redis la elimine por presión de memoria. Para una cache casi siempre quieres una expiración, para que los datos obsoletos caduquen solos.

1
2
3
4
127.0.0.1:6379> SET user:42:name "Elena" EX 300
OK
127.0.0.1:6379> TTL user:42:name
(integer) 297

EX 300 hace que la clave expire en 300 segundos. PX hace lo mismo en milisegundos, y EXPIRE user:42:name 300 pone un TTL a una clave que ya existe.

Para cualquier cosa más allá de una comprobación manual rápida, ejecuta Redis con Compose junto al resto de tu stack — mira Qué Es Docker Compose y Cómo Se Usa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
services:
  redis:
    image: redis:8
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --save 60 1 --maxmemory 256mb --maxmemory-policy allkeys-lru

  api:
    build: .
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379

volumes:
  redis-data:

api llega a Redis por el hostname redis porque Compose pone ambos servicios en la misma red definida por el usuario, el mecanismo que cubre Qué Es una Red Docker y Cómo Se Usa. El volumen redis-data importa menos para una cache pura que para una base de datos principal (mira Qué es un volumen Docker y cómo se usa): perder la cache solo significa que las siguientes lecturas se convierten en cache misses. Aun así consérvalo si tener una cache ya caliente tras un reinicio importa para tu presupuesto de latencia.

Cómo escribir la lógica cache-aside en tu aplicación

Redis te da los comandos; tu aplicación todavía tiene que llamarlos en el orden correcto. Envolver una lectura de base de datos en cache-aside se ve así:

1
2
3
4
5
6
7
8
9
def get_user(user_id):
    key = f"user:{user_id}"
    cached = redis.get(key)
    if cached is not None:
        return json.loads(cached)

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.set(key, json.dumps(user), ex=300)
    return user

Dos detalles hacen la mayor parte del trabajo aquí:

  • El TTL (ex=300) no es opcional. Sin él, una fila que cambia en la base de datos sigue sirviendo su valor antiguo desde Redis para siempre. Ajusta el TTL a cuánto tiempo es aceptable un dato desactualizado para esa clave concreta, no a un número global para toda la aplicación.
  • json.dumps/json.loads. Redis guarda cadenas, más un puñado de tipos más ricos como hashes y sorted sets — nunca los objetos de tu lenguaje. Cualquier cosa estructurada hay que serializarla al entrar y parsearla al salir.

Cuando la fila subyacente cambia, borra la clave en lugar de esperar al TTL:

1
2
3
def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.delete(f"user:{user_id}")

La siguiente lectura tras una actualización es un cache miss garantizado, que rellena Redis con la fila fresca. Cache-aside nunca intenta mantener la cache sincronizada en tiempo real. Hace que las entradas obsoletas desaparezcan rápido, por TTL o por borrado explícito al escribir.

Eviction: qué pasa cuando Redis se queda sin memoria

Redis guarda todo en RAM, así que la memoria es finita de una forma en que el disco normalmente no lo es. Pon un tope fijo con maxmemory, y dile a Redis qué hacer al llegar a ese tope con maxmemory-policy:

PolíticaComportamientoÚsala para
noevictionRechaza escrituras nuevas al llenarse, devuelve un errorUn almacén de datos que no puede perder datos silenciosamente
allkeys-lruElimina la clave usada menos recientemente, cualquier claveUna cache pura — la opción por defecto sensata
volatile-lruElimina la clave usada menos recientemente entre las que tienen TTLMezclar claves de cache y claves permanentes en una instancia
volatile-ttlElimina primero la clave con menos TTL restanteRate limiters, tokens de corta vida

Para una instancia de cache dedicada, allkeys-lru casi siempre es la correcta: cada clave en ella es desechable por definición, así que deja que Redis descarte lo que menos hayas tocado últimamente. noeviction es la política equivocada para una cache, porque convierte “Redis está lleno” en errores de aplicación en lugar de descartar en silencio las entradas frías.

1
2
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory-policy allkeys-lru

Ajusta ambos en el comando de arranque, como en el archivo de Compose de arriba, no solo en tiempo de ejecución. Reiniciar el contenedor resetea un CONFIG SET puesto solo en runtime a los valores por defecto de Redis (maxmemory 0, es decir sin límite, tope solo por el host).

Cuándo la cache de Redis es la solución equivocada

El caching esconde un camino lento; no lo repara. Cuatro situaciones en las que recurrir a Redis primero empeora las cosas:

  • Los datos cambian en cada lectura de todas formas. Un ticker bursátil en vivo o un contador incrementado en cada petición no gana nada con una cache invalidada tan rápido como se llena. Has añadido un salto de red sin ganar hit rate.
  • Necesitas consistencia fuerte. Cache-aside es eventually consistent por diseño: entre una escritura y el siguiente cache miss hay una ventana en la que todavía puede servirse un valor obsoleto. Para un saldo de cuenta o un stock de inventario donde esa ventana causa daño real, no la pongas en cache, o ponla con un TTL de segundos y acepta el compromiso explícitamente.
  • El cuello de botella real está en otro sitio. Si tu base de datos es lenta por un índice que falta o una consulta N+1, arregla eso primero. Cachear una consulta mal escrita solo mueve la misma respuesta equivocada a memoria más rápido.
  • Un thundering herd al expirar. Cuando una clave caliente expira, todas las peticiones concurrentes pueden fallar la cache a la vez y machacar la base de datos simultáneamente. Usa SET key value NX EX 30 como bloqueo de corta duración, en manos de la petición que rellena la cache, para que las demás esperen a esa en lugar de amontonarse sobre la base de datos.

Por dónde empezar con la cache de Redis

Pon un contenedor redis:8 delante de tu lectura más lenta y más repetida. Envuélvela en cache-aside con un TTL que puedas justificar, no un número redondo elegido al azar, y ajusta maxmemory-policy allkeys-lru para que una cache llena degrade en lugar de dar errores. Después mide el hit rate. El write-through caching, la invalidación por pub/sub y Redis Streams cuestan complejidad, y ninguno merece la pena hasta que un simple cache-aside te haya mostrado exactamente dónde se queda corto.

Artículos relacionados