Nivel 2 · 25 min
Patrones de Caché
Los patrones de caché definen cómo se sincronizan el caché y la fuente de verdad (base de datos). Elegir el patrón equivocado lleva a datos inconsistentes, race conditions o pérdida de datos en fallos. Los principales: Cache-Aside, Write-Through, Write-Behind y Read-Through.
Cache-Aside (Lazy Loading)
Cache-Aside es el patrón más común. El cliente es responsable de gestionar el caché: primero busca en caché, si hay miss lee de la DB y popula el caché, y escribe directo a la DB (invalidando o actualizando el caché). Ventajas: solo se cachea lo que se usa. Desventajas: cache miss inicial es lento, el caché puede quedar stale si la DB se actualiza sin invalidar. Thundering herd: múltiples requests simultáneos con miss pueden atacar la DB a la vez.
Write-Through y Write-Behind
Write-Through: cada escritura en la DB también escribe en el caché de forma síncrona — el caché siempre está actualizado. Desventaja: latencia de escritura mayor (dos escrituras), y se cachean datos que pueden no volver a leerse. Write-Behind (Write-Back): las escrituras van primero al caché y se propagan a la DB de forma asíncrona. Menor latencia de escritura, pero riesgo de pérdida de datos si el caché falla antes de propagar.
Read-Through y Cache Stampede
Read-Through: en un miss, el caché mismo consulta la DB y se auto-popula — transparente para el cliente. El cliente siempre lee del caché, nunca de la DB directamente. Cache Stampede (Thundering Herd): cuando una clave expira bajo alta carga, muchos requests simultáneos tienen miss y atacan la DB al mismo tiempo. Soluciones: mutex lock (solo un thread repopula), probabilistic early expiry (refrescar antes de que expire), o background refresh.
Code example
// Cache-Aside con proteccion de stampede
async function getUserWithLock(userId) {
const cached = await redis.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
const lockKey = `lock:user:${userId}`;
const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (!acquired) {
await sleep(50); // esperar que otro thread popule
return getUserWithLock(userId);
}
const user = await db.findUser(userId);
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
await redis.del(lockKey);
return user;
}