Inicio / Artículos

Fundamentos del caché en Redis: patrones, trampas y conceptos para entrevistas

El cacheo con Redis en Node.js: caché por separado, TTL, protección contra saturación y políticas de eliminación, además de las preguntas de entrevista sobre diseño de sistemas relacionadas con ellos.

Editor: REACTAPP.TOP11 min de lectura2516 palabras

Fuentes: Redis Isn’t Just a Cache: The Concepts Every Backend Developer Should Know

¿Por qué necesitamos un caché si ya tenemos una base de datos?

Imagínese un punto de extremo en su aplicación que se ve así:

GET /products/123

Un cliente accede a esa ruta, y su servidor consulta MongoDB:

API → MongoDB → Product

La base de datos devuelve el registro del producto, y todo funciona bien.

Ahora imagine que 10,000 personas solicitan ese mismo producto más o menos al mismo tiempo.

Terminaría repitiendo exactamente el mismo proceso miles de veces:

Request
   ↓
Database
   ↓
Same Product

Su base de datos sigue realizando el mismo trabajo una y otra vez para las mismas consultas.

¿No sería mejor si la respuesta pudiera guardarse en algún lugar?

Esencialmente, para eso sirve el cachéing.

¿Qué es el cachéing?

Un caché es una capa de almacenamiento más rápida que mantiene los datos solicitados con frecuencia por un breve período de tiempo, para que su aplicación no se vea obligada a acceder al almacenamiento de datos principal, que es más lento, en cada ocasión.

En lugar del flujo habitual:

Request → Database → Response

introducimos un atajo:

Request
   ↓
Cache
   ↓
Hit? ── Yes → Response
   │
   No
   ↓
Database
   ↓
Cache
   ↓
Response

El objetivo no es eliminar por completo la base de datos.

Sino reducir las operaciones en la base de datos que no es necesario realizar dos veces.

Acceso al caché exitoso vs. fallo en el caché

Estos dos conceptos surgen constantemente, por lo que vale la pena conocerlos bien.

Acceso al caché exitoso

Esto ocurre cuando los datos que busca ya se encuentran en el caché.

Request
  ↓
Redis
  ↓
Found ✅
  ↓
Response

Fallo en el caché

Esto ocurre cuando el caché no contiene nada para usted.

Request
  ↓
Redis
  ↓
Not Found ❌
  ↓
Database
  ↓
Store in Redis
  ↓
Response

Una métrica útil aquí es el índice de accesos exitosos al caché:

Cache Hit Ratio =
Cache Hits / Total Cache Requests

Una proporción más alta suele indicar que se está atendiendo más tráfico directamente desde la caché en lugar de desde la base de datos subyacente.

No obstante, un número mayor no significa automáticamente éxito. Si la caché está llena de entradas obsoletas o de poco valor, una alta tasa de aciertos no hace que sea útil.

¿Entonces, qué papel juega Redis?

Redis es un almacén de datos en memoria que se utiliza con frecuencia para la caché, aunque sus capacidades van mucho más allá de ese único caso de uso.

Ofrece una variedad de estructuras de datos, entre las que se incluyen:

  • Cadenas de texto
  • Hashes
  • Listas
  • Conjuntos
  • Conjuntos ordenados
  • Flujos
  • JSON
  • Además de otros tipos especializados

Para una caché sencilla, se puede pensar en ella de la siguiente manera:

Key                 Value
────────────────────────────
product:123         Product data
user:42             User data

Dado que Redis almacena datos frecuentemente utilizados en memoria, las lecturas desde la caché pueden ser extremadamente rápidas.

Redis en una aplicación Node.js

Para utilizar el cliente node-redis, debes instalarlo con:

npm install redis

Configurar una conexión básica solo requiere un par de líneas:

import { createClient } from "redis";
const redis = createClient();
await redis.connect();

A partir de ahí, almacenar y recuperar valores es sencillo:

await redis.set("user:42", "Hamza");
const user = await redis.get("user:42");

La documentación oficial de Redis indica actualmente a node-redis como el cliente recomendado para proyectos Node.js.

No obstante, conocer los comandos SET y GET solo te lleva hasta cierto punto.

La verdadera pregunta que merece respuesta es:

¿Cómo diseñas realmente una aplicación basada en Redis?

El patrón más importante: Cache-Aside

Si hay un patrón que todo desarrollador de backend debería aprender primero, es Cache-Aside.

En este patrón, la propia aplicación es responsable de gestionar el caché.

Request
   ↓
Check Redis
   ↓
 ┌───────────────┐
 │ Cache Hit?    │
 └───────────────┘
    ↓ Yes    ↓ No
 Response   Database
              ↓
           Redis
              ↓
           Response

Una versión simplificada en código se ve así:

const cached = await redis.get(key);
if (cached) return JSON.parse(cached);const product = await Product.findById(id);await redis.set(key, JSON.stringify(product), {
    EX: 300
});

La opción EX: 300 establece un TTL de 300 segundos en la entrada cachée.

Cache-Aside es, sin duda, el patrón de caché más utilizado con Redis.

TTL: El caché no debe durar para siempre

Imaginemos que hemos cachéado lo siguiente:

product:123

¿Qué debería suceder si el precio del producto se actualiza posteriormente?

Sin un plazo de vencimiento, los usuarios podrían seguir viendo el precio antiguo indefinidamente.

Esa es la razón del TTL, o Time To Live.

Product stored
     ↓
TTL = 300 seconds
     ↓
Expires
     ↓
Next request → Database → Fresh cache

El TTL impone un límite al tiempo durante el cual se puede servir datos obsoletos.

Aun así, resista la tentación de elegir un TTL excesivamente largo.

El valor correcto depende por completo de hasta qué punto su aplicación puede permitirse la obsolescencia de los datos.

Invalidación de caché: el famoso problema difícil

Considere este escenario:

Database:
price = ₹799
Redis:
price = ₹799

Luego se actualiza la base de datos:

Database:
price = ₹899

Pero Redis sigue conservando:

price = ₹799

En este punto, su aplicación está sirviendo datos inconsistentes según de dónde los lea.

Una solución común es limpiar la caché cada vez que cambien los datos subyacentes:

await Product.findByIdAndUpdate(id, update);
await redis.del(`product:${id}`);

Después de eso, la siguiente solicitud sigue este camino:

Redis → Miss
   ↓
Database → ₹899
   ↓
Redis → Store fresh value

En los sistemas basados en el modelo cache-aside, borrar simplemente la entrada de caché afectada cada vez que se realiza una escritura suele ser más fácil y seguro que intentar mantener el caché en sincronía permanente con la base de datos.

Tres problemas de caché que deberías conocer

Estos constituyen excelentes temas de conversación en entrevistas.

1. Cache Stampede

Imagina que una clave muy utilizada llega a su fecha de vencimiento:

product:123 → EXPIRED

Inmediatamente después, llegan 1,000 solicitudes al mismo tiempo.

Todas ellas fallan al intentar acceder al caché.

Todas ellas terminan recayendo en la base de datos.

1000 requests
      ↓
1000 DB queries 💥

El caché estaba diseñado para proteger a la base de datos del sobrecargo, pero el evento de vencimiento provocó un aumento brusco en la carga.

Las formas típicas de evitar esto incluyen:

  • El uso de bloqueos o solicitudes secuenciales, de modo que solo una solicitud rellene el caché mientras las demás esperan
  • TTLs escalonados para que las claves no caduquen todas al mismo tiempo
  • Actualización anticipada probabilística, que renueva una clave ligeramente antes de que realmente expire
  • Precalentamiento por adelantado de los datos solicitados con frecuencia
  • La propia documentación de Redis aborda explícitamente las estrategias de protección contra ataques masivos en claves populares.

    2. Penetración de caché

    ¿Qué sucede cuando los clientes siguen solicitando datos que simplemente no existen?

    GET /users/999999
    

    Redis devuelve “no encontrado”.

    La base de datos responde “no se encontró”.

    La siguiente solicitud idéntica repite exactamente el mismo ciclo.

    Un actor malintencionado o un cliente defectuoso podría explotar esto para someter a la base de datos con búsquedas inútiles.

    Una contramedida útil es el caché negativo: almacenar en caché el hecho de que algo está faltando:

    user:999999 → NOT_FOUND
    TTL → short
    

    Con esto en su lugar, las búsquedas repetidas del mismo registro inexistente pueden responderse directamente desde Redis en lugar de acceder a la base de datos cada vez.

    3. Avalancha de caché

    Ahora imagine un gran lote de claves en caché que vencen aproximadamente al mismo tiempo.

    Key A → expired
    Key B → expired
    Key C → expired
    Key D → expired
           ↓
    Massive DB traffic
    

    Esta situación se conoce como avalancha de caché.

    Una forma típica de reducir el riesgo es evitar asignar tiempos de vencimiento idénticos a grandes grupos de claves, por ejemplo, añadiendo un pequeño margen aleatorio a cada TTL.

    Expulsión de caché: ¿Qué sucede cuando Redis se queda sin memoria?

    La memoria de Redis no es infinita.

    Cuando el uso de memoria alcanza el límite configurado, Redis comienza a eliminar claves según la regla de eliminación que hayas establecido. Redis incluye varios modos de eliminación, lo que te permite eliminar las entradas menos utilizadas en todo el espacio de claves o solo entre aquellas con fecha de vencimiento establecida, eliminar las entradas menos frecuentemente usadas en cualquiera de esos dos ámbitos, eliminar según las claves que estén más cerca de vencer, o rechazar por completo nuevas escrituras una vez que la memoria está llena.

    LRU significa esencialmente:

    Deshacerse de aquello que no se ha tocado en mucho tiempo.

    LFU significa esencialmente:

    Deshacerse de aquello que no se accede con frecuencia.

    Elige el que mejor se adapte a tus patrones de acceso específicos.

    Redis no es solo un caché

    Este punto se plantea con frecuencia en las entrevistas.

    Dado que Redis ofrece varias estructuras de datos integradas, puede servir para propósitos mucho más allá del caché:

    Strings       → Caching
    Hashes        → Objects / sessions
    Lists         → Queues
    Sets          → Unique collections
    Sorted Sets   → Leaderboards
    Streams       → Event processing
    

    Por lo tanto, la afirmación de que:

    "Redis es una base de datos de caché"

    solo explica una parte de la historia.

    Una descripción más precisa es:

    Redis es un almacén de estructuras de datos en memoria, utilizado típicamente para el caché, pero también capaz de gestionar sesiones, colas, clasificaciones, limitación de velocidad, mensajería pub/sub y mucho más.

    La documentación oficial de Redis sobre tipos de datos explica estas estructuras junto con otros tipos especializados que soporta.

    ¿Y qué pasa con la persistencia en Redis?

    Dado que Redis está en memoria, ¿un reinicio borra todo?

    No necesariamente.

    Redis ofrece varias estrategias de persistencia:

    • RDB — instantáneas periódicas en un momento determinado del conjunto de datos
    • AOF — un registro que guarda cada operación de escritura
    • RDB + AOF combinados
    • Sin persistencia alguna

    Si Redis solo funciona como caché, perder su contenido al reiniciarse suele ser aceptable, ya que la aplicación puede volver a cargarlo desde la base de datos principal.

    Pero si Redis almacena datos que son realmente importantes por sí mismos, la configuración de persistencia se vuelve un problema mucho más grave.

    Una trampa común en las entrevistas

    “¿Por qué no almacenar todo simplemente en Redis?”

    Porque la memoria de Redis es un recurso limitado y relativamente costoso en comparación con el almacenamiento persistente convencional.

    Eso significa que no se debe usar la caché de forma indiscriminada.

    Los buenos candidatos para el caché comparten estas características:

    • Son solicitados con frecuencia
    • Son costosos de obtener o calcular
    • Se puede tolerar cierto grado de obsolescencia
    • Vale la pena el costo en memoria por mantenerlos a mano

    Trate a Redis como una capa de mejora del rendimiento que funciona junto a su sistema, y no como un sustituto total de su base de datos principal.

    Caché local vs Caché distribuido

    Imagínese una aplicación Node.js que se ejecuta en tres servidores separados:

    Server A → Cache A
    Server B → Cache B
    Server C → Cache C
    

    Cada uno de estos servidores acaba manteniendo su propio caché independiente.

    Un usuario podría acceder al Servidor A y obtener un resultado del caché, y luego acceder al Servidor B y no encontrar el mismo dato en el caché.

    La introducción de una instancia compartida de Redis cambia la situación:

                 ┌→ Server A ─┐
    Client ──────┼→ Server B ─┼→ Redis
                 └→ Server C ─┘
    

    Ahora cada instancia de la aplicación accede al mismo caché compartido.

    Esta ventaja del estado compartido es una razón importante por la cual Redis destaca especialmente cuando una aplicación se escala horizontalmente.

    La imagen de la entrevista avanzada

    Supongamos que un entrevistador plantea este desafío:

    "Diseñe una estrategia de caché para una API de producto con alto tráfico."

    Una respuesta débil se queda en:

    "Usaré Redis."

    Una respuesta más sólida explica el razonamiento paso a paso:

    Request
                         ↓
                      Redis
                    ↙       ↘
                 Hit         Miss
                  ↓            ↓
               Response      Database
                                ↓
                             Redis
                                ↓
                             Response
    

    Luego aborda cada una de estas decisiones de diseño:

    1. Patrón de caché

    Elegir el patrón cache-aside.

    2. TTL

    Establecerlo según el grado de obsolescencia que puedan tolerar los datos del producto.

    3. Invalidación

    Borrar la clave correspondiente cada vez que se actualicen los datos subyacentes.

    4. Protección contra sobrecarga

    Aplicar técnicas de bloqueo, consolidación de solicitudes o actualización temprana.

    5. Expulsión

    Elegir una política LRU o LFU adecuada para la carga de trabajo.

    6. Diseño de claves

    product:{id}
    user:{id}
    

    7. Monitoreo

    Prestar atención a:

    • Relación de aciertos
    • Relación de fallos
    • Latencia
    • Uso de memoria
    • Cuentas de expulsión
    • Claves frecuentes
    • Errores de Redis

    Ese tipo de razonamiento estructurado proporciona una respuesta mucho más convincente sobre el diseño del sistema que simplemente afirmar que "Redis es rápido."

    Revisión rápida de entrevistas

    ¿Por qué molestarse en usar caché?

    Reduce la latencia y evita que el almacén de datos principal tenga que manejar una y otra vez las mismas lecturas costosas.

    ¿Por qué elegir específicamente Redis?

    Ofrece acceso rápido en memoria, soporte para vencimiento, estructuras de datos versátiles y funcionalidades diseñadas para sistemas distribuidos.

    ¿Qué significa cache-aside?

    La aplicación busca primero en la caché; si no está allí, consulta la base de datos y luego escribe ese resultado de vuelta en la caché.

    ¿Qué significa TTL?

    Es la duración de vida que tiene una entrada en caché antes de vencerse automáticamente.

    ¿Qué significa invalidación de caché?

    Significa eliminar o actualizar las entradas en caché cada vez que cambian los datos originales.

    ¿Qué es una estampida de caché?

    Es una situación en la que un gran número de solicitudes no encuentran los datos en la caché al mismo tiempo y todas acceden a la base de datos simultáneamente.

    LRU versus LFU?

    LRU elimina lo que se ha utilizado con menos frecuencia recientemente. LFU elimina lo que se ha utilizado con menos frecuencia en general.

    ¿Merece cada consulta ser almacenada en caché?

    En absoluto. Base la decisión en la frecuencia con la que se accede a los datos, cuán costoso es obtenerlos, qué nivel de obsolescencia se considera aceptable y cuanta memoria puedes destinar.

    El modelo mental que vale la pena mantener

    Omite memorizar los comandos de Redis como punto de partida. En su lugar, concéntrate en la arquitectura subyacente:

                  ┌─────────────┐
    Request ─────→│    Redis    │
                  └──────┬──────┘
                         │
                  Cache Hit?
                   ↙         ↘
                 YES           NO
                  ↓             ↓
              Response       Database
                                ↓
                             Redis
                                ↓
                             Response
    

    Luego analiza cuatro preguntas:

    ¿Qué debe estar en el caché?

    ¿Cuánto tiempo debe permanecer allí?

    ¿Cuándo es necesario invalidarlo?

    ¿Qué sucede si el caché falla o expira bajo carga intensa?

    Abordar estas preguntas es lo que distingue el conocimiento básico sobre caché del pensamiento de nivel profesional.

    Conclusión

    El caché no es simplemente:

    "Coloca Redis delante de MongoDB."

    Una estrategia sólida de caché se basa fundamentalmente en compromisos.

    Estás intercambiando:

    Memoria por velocidad

    y, a veces:

    Frescura por rendimiento

    Redis proporciona los mecanismos, pero es la lógica de tu aplicación la que determina cómo se utilizan.

    Si te estás preparando para entrevistas, no te conformes con decir:

    "Redis es una base de datos en memoria."

    Estar preparado para explicar:

    Caché por separado, TTL, invalidación, ratio de aciertos, expulsión, efecto estampida, penetración, efecto avalancha, caché distribuido y consistencia.

    Una vez que entiendas cómo se relacionan estos conceptos, habrás ido más allá de simplemente conocer Redis.

    Ahora comprendes cómo los sistemas con alto tráfico protegen sus bases de datos manteniendo una alta velocidad.

    Lecturas relacionadas

  • Diseñando backend de chat en tiempo real: habitaciones, persistencia y escalabilidad — Aprenda cómo arquitecturar un backend de chat en tiempo real utilizando Socket.IO, PostgreSQL y Redis, abordando temas como las habitaciones, el orden de persistencia de los mensajes, la presencia de usuarios y la escalabilidad multi-servidor.
  • Construyendo sistemas de trabajos en fondo confiables con BullMQ y Redis — Aprenda cómo diseñar pipelines de trabajos en segundo plano resistentes en Node.js con BullMQ y Redis, cubriendo temas como las reintentos, la concurrencia, la idempotencia y el monitoreo.