Estrategias de Cache - Fundamentos de Optimizacion del Rendimiento

14 min de lectura | 2024.12.26

Que es la Cache

La cache es un mecanismo que almacena temporalmente copias de datos en un lugar de acceso rapido. Reduce los accesos a la fuente de datos original (base de datos, API, etc.) y acorta el tiempo de respuesta.

Efecto de la Cache: Si una consulta a la base de datos tarda 100ms, la obtencion desde la cache puede completarse en menos de 1ms.

Capas de Cache

Cache del Navegador
      ↓
Cache de CDN
      ↓
Cache de Aplicacion (Redis, etc.)
      ↓
Cache de Base de Datos
      ↓
Base de Datos

Patrones de Cache

Cache-Aside

La aplicacion gestiona directamente la cache y la base de datos.

async function getUser(userId) {
  // 1. Verificar cache
  const cached = await cache.get(`user:${userId}`);
  if (cached) {
    return JSON.parse(cached);
  }

  // 2. Cache miss: obtener de la BD
  const user = await db.users.findById(userId);

  // 3. Guardar en cache
  await cache.setex(`user:${userId}`, 3600, JSON.stringify(user));

  return user;
}

Ventajas: Simple, resistente a fallos Desventajas: Latencia en cache miss

Read-Through

La propia cache se encarga de obtener los datos.

// Configuracion de la libreria de cache
const cache = new Cache({
  loader: async (key) => {
    // Se llama automaticamente en cache miss
    const userId = key.replace('user:', '');
    return await db.users.findById(userId);
  }
});

// Uso (simple!)
const user = await cache.get(`user:${userId}`);

Write-Through

Al escribir, se actualizan simultaneamente la cache y la BD.

async function updateUser(userId, data) {
  // Actualizar BD
  const user = await db.users.update(userId, data);

  // Actualizar cache simultaneamente
  await cache.setex(`user:${userId}`, 3600, JSON.stringify(user));

  return user;
}

Ventajas: Alta consistencia de datos Desventajas: Aumento de latencia en escritura

Write-Behind

Se escribe inmediatamente en la cache y la reflexion en la BD se hace de forma asincrona.

async function updateUser(userId, data) {
  // Actualizar cache inmediatamente
  await cache.setex(`user:${userId}`, 3600, JSON.stringify(data));

  // Agregar escritura a la BD a la cola
  await writeQueue.add({ userId, data });

  return data;
}

// Worker en segundo plano
writeQueue.process(async (job) => {
  await db.users.update(job.userId, job.data);
});

Ventajas: Escritura rapida Desventajas: Riesgo de perdida de datos

Invalidacion de Cache

TTL (Time To Live)

Expira automaticamente despues de un tiempo determinado.

// Expira despues de 60 segundos
await cache.setex('key', 60, 'value');

Invalidacion Basada en Eventos

Se elimina explicitamente la cache al actualizar datos.

async function updateUser(userId, data) {
  await db.users.update(userId, data);

  // Invalidar caches relacionadas
  await cache.del(`user:${userId}`);
  await cache.del(`user:${userId}:profile`);
  await cache.del(`users:list`);
}

Invalidacion Basada en Patrones

// Eliminar todas las caches relacionadas con el usuario
const keys = await cache.keys('user:123:*');
await cache.del(...keys);

Problemas de Cache y Soluciones

Cache Stampede (Avalancha)

Problema donde muchas solicitudes tienen cache miss simultaneamente.

// Solucion: Usar bloqueo
async function getWithLock(key, loader) {
  const cached = await cache.get(key);
  if (cached) return JSON.parse(cached);

  // Obtener bloqueo
  const lockKey = `lock:${key}`;
  const locked = await cache.set(lockKey, '1', 'NX', 'EX', 10);

  if (!locked) {
    // Otro proceso esta cargando → esperar y reintentar
    await sleep(100);
    return getWithLock(key, loader);
  }

  try {
    const data = await loader();
    await cache.setex(key, 3600, JSON.stringify(data));
    return data;
  } finally {
    await cache.del(lockKey);
  }
}

Recalculo Probabilistico Temprano

Actualiza la cache probabilisticamente antes de que expire el TTL.

async function getWithProbabilisticRefresh(key, loader, ttl) {
  const data = await cache.get(key);
  const remainingTtl = await cache.ttl(key);

  // Si queda poco TTL, recalcular probabilisticamente
  if (data && remainingTtl < ttl * 0.1) {
    if (Math.random() < 0.1) {
      // 10% de probabilidad de actualizacion en segundo plano
      loader().then(newData => {
        cache.setex(key, ttl, JSON.stringify(newData));
      });
    }
  }

  if (data) return JSON.parse(data);

  const newData = await loader();
  await cache.setex(key, ttl, JSON.stringify(newData));
  return newData;
}

Diseno de Claves de Cache

// Buen diseno de clave
const key = `user:${userId}:profile:v2`;

// Componentes:
// - Prefijo: Tipo de entidad
// - Identificador: ID unico
// - Subrecurso: Datos especificos
// - Version: Compatibilidad al cambiar esquema

Guia de Diseno de TTL

Tipo de DatosTTLRazon
Contenido estatico1 dia - 1 semanaCasi nunca cambia
Perfil de usuario1 - 24 horasBaja frecuencia de cambio
Informacion de configuracion5 - 30 minutosSe actualiza moderadamente
Datos en tiempo real1 - 5 minutosCambia frecuentemente
Sesion30 min - 24 horasBalance entre seguridad y UX

Resumen

La cache es una tecnica fundamental para la optimizacion del rendimiento. Entendiendo patrones como Cache-Aside y Write-Through, y disenando estrategias apropiadas de TTL e invalidacion, puede construir sistemas rapidos y escalables. Considere el balance entre la complejidad y los beneficios de la cache al implementarla.

← Volver a la lista