Volver a Redis Básico

Patrones Comunes con Redis

Patrón Cache-Aside

async function obtenerProducto(productoId: string) { const cacheKey = `producto:${productoId}`; // 1. Intentar desde caché const cached = await redis.get(cacheKey); if (cached) return JSON.parse(cached); // 2. Fallo de caché — consultar BD const producto = await db.productos.findById(productoId); if (!producto) return null; // 3. Poblar caché con TTL de 5 minutos await redis.set(cacheKey, JSON.stringify(producto), 'EX', 300); return producto; } // Invalidar al actualizar async function actualizarProducto(productoId: string, datos: Partial<Producto>) { await db.productos.update(productoId, datos); await redis.del(`producto:${productoId}`); }

Limitación de Tasa (Rate Limiting)

async function verificarLimite(ip: string, limite = 100, ventanaSegundos = 60) { const clave = `rate:${ip}:${Math.floor(Date.now() / (ventanaSegundos * 1000))}`; const actual = await redis.incr(clave); if (actual === 1) { await redis.expire(clave, ventanaSegundos); } if (actual > limite) { throw new Error('Límite de tasa superado'); } return { actual, limite, restantes: limite - actual }; }

Bloqueo Distribuido

async function adquirirBloqueo(recurso: string, ttlMs: number) { const token = `${Date.now()}-${Math.random()}`; const clave = `bloqueo:${recurso}`; // SET NX EX — atómico: establecer solo si no existe const resultado = await redis.set(clave, token, 'PX', ttlMs, 'NX'); if (resultado !== 'OK') return null; return token; } async function liberarBloqueo(recurso: string, token: string) { const clave = `bloqueo:${recurso}`; // Script Lua: verificar token y eliminar de forma atómica const script = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `; await redis.eval(script, 1, clave, token); } // Uso const token = await adquirirBloqueo('inventario:producto-42', 5000); if (token) { try { await procesarInventario(); } finally { await liberarBloqueo('inventario:producto-42', token); } }

Almacenamiento de Sesiones

import session from 'express-session'; import RedisStore from 'connect-redis'; app.use(session({ store: new RedisStore({ client: redis }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 1000 * 60 * 60 // 1 hora } }));

Limitador de Ventana Deslizante

async function limiteVentanaDeslizante(userId: string, limite: number, ventanaMs: number) { const ahora = Date.now(); const clave = `sw:${userId}`; const pipeline = redis.pipeline(); pipeline.zremrangebyscore(clave, 0, ahora - ventanaMs); pipeline.zadd(clave, ahora, `${ahora}-${Math.random()}`); pipeline.zcard(clave); pipeline.expire(clave, Math.ceil(ventanaMs / 1000)); const resultados = await pipeline.exec(); const conteo = resultados[2][1] as number; if (conteo > limite) throw new Error('Límite de tasa superado'); return conteo; }

Idempotencia con Redis

async function procesarPago(clavIdempotencia: string, pago: Pago) { const cacheKey = `idempotencia:${clavIdempotencia}`; const existente = await redis.get(cacheKey); if (existente) return JSON.parse(existente); const resultado = await pasarelaPago.cobrar(pago); // Guardar resultado por 24h await redis.set(cacheKey, JSON.stringify(resultado), 'EX', 86400); return resultado; }