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:
- La aplicación pide una clave a Redis.
- Cache hit — Redis la tiene, la devuelve. La base de datos ni se entera de la petición.
- Cache miss — Redis no la tiene. Consulta la base de datos, escribe el resultado en Redis y luego lo devuelve.
| |
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.
| |
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:
| |
| |
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.
| |
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:
| |
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í:
| |
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:
| |
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ítica | Comportamiento | Úsala para |
|---|---|---|
noeviction | Rechaza escrituras nuevas al llenarse, devuelve un error | Un almacén de datos que no puede perder datos silenciosamente |
allkeys-lru | Elimina la clave usada menos recientemente, cualquier clave | Una cache pura — la opción por defecto sensata |
volatile-lru | Elimina la clave usada menos recientemente entre las que tienen TTL | Mezclar claves de cache y claves permanentes en una instancia |
volatile-ttl | Elimina primero la clave con menos TTL restante | Rate 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.
| |
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 30como 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.