Por qué el caching avanzado es una necesidad para aplicaciones TypeScript
En los sistemas modernos de alta carga, el caching ha dejado de ser una opción — es un elemento obligatorio de la arquitectura. Para los desarrolladores de TypeScript que trabajan con Node.js, elegir la estrategia de caching correcta puede reducir el tiempo de respuesta de la API de 200 ms a 2 ms y disminuir la carga en la base de datos en un 80-90%. En este artículo, exploraremos tres enfoques de caching a nivel avanzado: Redis, Memcached y soluciones integradas en TypeScript.
1. Caching integrado en TypeScript: desde un simple Map hasta un caché LRU
Las soluciones integradas son ideales para aplicaciones monolíticas o microservicios donde no se requiere un caché distribuido. La opción más primitiva es usar Map, pero no es adecuada para producción debido a la falta de TTL y control de memoria.
Implementación de un caché LRU con TTL
Consideremos una implementación avanzada de un caché LRU (Least Recently Used) con limpieza automática:
interface CacheEntry<T> { value: T; expiresAt: number;}
class LRUCache<T> { private cache: Map<string, CacheEntry<T>>; private readonly maxSize: number; private readonly ttlMs: number;
constructor(maxSize: number = 100, ttlMs: number = 60000) { this.cache = new Map(); this.maxSize = maxSize; this.ttlMs = ttlMs; }
get(key: string): T | undefined { const entry = this.cache.get(key); if (!entry) return undefined;
if (Date.now() > entry.expiresAt) { this.cache.delete(key); return undefined; }
// Movemos la clave al final (la más reciente) this.cache.delete(key); this.cache.set(key, entry); return entry.value; }
set(key: string, value: T): void { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.maxSize) { // Eliminamos el elemento más antiguo (el primero) const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); }
this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs, }); }
clear(): void { this.cache.clear(); }}
// Usoconst userCache = new LRUCache<User>(500, 300000); // 500 elementos, 5 minutos de TTLPatrón avanzado: decorador de caching
Para mantener el código limpio, usamos decoradores de TypeScript:
function Cached(ttlMs: number = 60000) { const cache = new LRUCache<any>(100, ttlMs);
return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) { const key = `${propertyKey}_${JSON.stringify(args)}`; const cached = cache.get(key); if (cached !== undefined) return cached;
const result = await originalMethod.apply(this, args); cache.set(key, result); return result; };
return descriptor; };}
class UserService { @Cached(120000) // 2 minutos async getUser(id: string): Promise<User> { // Consulta costosa a la BD return db.findUser(id); }}2. Redis: caching distribuido con estructuras de datos avanzadas
Redis no es solo un almacén key-value. Su soporte para estructuras de datos complejas (Hash, List, Set, Sorted Set) abre posibilidades para implementar estrategias de caching avanzadas.
Caching con invalidación por etiquetas
Problema: al actualizar una entidad, es necesario invalidar varios cachés. Solución — etiquetas:
import Redis from 'ioredis';
class TaggedCache { private redis: Redis; private readonly keyPrefix: string;
constructor(redisUrl: string, prefix: string = 'cache') { this.redis = new Redis(redisUrl); this.keyPrefix = prefix; }
async get<T>(key: string,