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;
}