5 exercices · Solutions masquées
Implémentez un cache avancé avec deux TTL : freshTTL (données fraîches) et staleTTL (données périmées mais utilisables). Retourner les données périmées immédiatement et les rafraîchir en arrière-plan.
async function getWithSWR(key, fetchFn, { freshTTL = 60, staleTTL = 300 } = {}) {
const freshKey = `fresh:${key}`;
const staleKey = `stale:${key}`;
// Données fraîches
const fresh = await redis.get(freshKey);
if (fresh) return JSON.parse(fresh);
// Données périmées (stale) — retourner immédiatement
const stale = await redis.get(staleKey);
if (stale) {
// Rafraîchissement en arrière-plan (non bloquant)
setImmediate(async () => {
try {
const lockKey = `lock:swr:${key}`;
const locked = await redis.set(lockKey, '1', 'EX', freshTTL, 'NX');
if (!locked) return; // quelqu'un d'autre rafraîchit déjà
const data = await fetchFn();
const multi = redis.multi();
multi.set(freshKey, JSON.stringify(data), 'EX', freshTTL);
multi.set(staleKey, JSON.stringify(data), 'EX', staleTTL);
multi.del(lockKey);
await multi.exec();
} catch (err) {
console.error('SWR background refresh failed:', err);
}
});
return JSON.parse(stale);
}
// Cache miss complet
const data = await fetchFn();
const multi = redis.multi();
multi.set(freshKey, JSON.stringify(data), 'EX', freshTTL);
multi.set(staleKey, JSON.stringify(data), 'EX', staleTTL);
await multi.exec();
return data;
}
Implémentez un rate limiter à fenêtre glissante (sliding window) avec Redis Sorted Sets, plus précis que la fenêtre fixe. Permettre 100 requêtes par minute par IP, retourner les headers X-RateLimit-*.
const RATE_LIMIT = 100;
const WINDOW_MS = 60 * 1000; // 1 minute
async function slidingWindowRateLimit(identifier) {
const key = `ratelimit:sw:${identifier}`;
const now = Date.now();
const windowStart = now - WINDOW_MS;
const multi = redis.multi();
multi.zremrangebyscore(key, 0, windowStart); // supprimer les vieilles entrées
multi.zadd(key, now, `${now}-${Math.random()}`); // ajouter la requête courante
multi.zcard(key); // compter
multi.expire(key, Math.ceil(WINDOW_MS / 1000) + 1);
const results = await multi.exec();
const count = results[2][1];
const remaining = Math.max(0, RATE_LIMIT - count);
const allowed = count <= RATE_LIMIT;
return {
allowed,
headers: {
'X-RateLimit-Limit': RATE_LIMIT,
'X-RateLimit-Remaining': remaining,
'X-RateLimit-Reset': Math.ceil((now + WINDOW_MS) / 1000),
'Retry-After': allowed ? undefined : Math.ceil(WINDOW_MS / 1000)
}
};
}
// Middleware Express
function rateLimitMiddleware(req, res, next) {
const identifier = req.ip;
slidingWindowRateLimit(identifier).then(({ allowed, headers }) => {
Object.entries(headers).forEach(([k, v]) => { if (v !== undefined) res.set(k, v); });
if (!allowed) return res.status(429).json({ error: 'Too Many Requests' });
next();
});
}
Créez un système de chat multi-salles avec Redis Pub/Sub. Chaque salle est un channel Redis. Implémentez : rejoindre une salle, envoyer un message, quitter, et historique des 50 derniers messages par salle (Redis List).
const Redis = require('ioredis');
const publisher = new Redis(process.env.REDIS_URL);
const subscriber = new Redis(process.env.REDIS_URL);
const HISTORY_PREFIX = 'chat:history:';
const MAX_HISTORY = 50;
async function joinRoom(roomId, userId, io) {
const channel = `chat:room:${roomId}`;
await subscriber.subscribe(channel);
subscriber.on('message', (ch, raw) => {
if (ch !== channel) return;
const msg = JSON.parse(raw);
io.to(`room:${roomId}`).emit('message', msg);
});
// Envoyer l'historique
const history = await redis.lrange(HISTORY_PREFIX + roomId, 0, MAX_HISTORY - 1);
return history.map(m => JSON.parse(m)).reverse();
}
async function sendMessage(roomId, userId, userName, content) {
const msg = {
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
roomId, userId, userName, content,
timestamp: Date.now()
};
// Stocker dans l'historique
const histKey = HISTORY_PREFIX + roomId;
await redis.lpush(histKey, JSON.stringify(msg));
await redis.ltrim(histKey, 0, MAX_HISTORY - 1);
await redis.expire(histKey, 7 * 86400); // 7 jours
// Publier
await publisher.publish(`chat:room:${roomId}`, JSON.stringify(msg));
return msg;
}
async function leaveRoom(roomId) {
await subscriber.unsubscribe(`chat:room:${roomId}`);
}
async function getHistory(roomId) {
const raw = await redis.lrange(HISTORY_PREFIX + roomId, 0, MAX_HISTORY - 1);
return raw.map(m => JSON.parse(m)).reverse();
}
Créez un système d'event sourcing avec Redis Streams pour tracer les événements d'un e-commerce (order.created, order.paid, order.shipped). Implémentez un consumer group avec au moins 2 workers et un mécanisme de reprise des messages non acquittés.
const STREAM_KEY = 'events:ecommerce';
const GROUP_NAME = 'order-processors';
const PENDING_TIMEOUT = 60000; // 60s
// Initialisation
async function initConsumerGroup() {
try {
await redis.xgroup('CREATE', STREAM_KEY, GROUP_NAME, '$', 'MKSTREAM');
} catch (err) {
if (!err.message.includes('BUSYGROUP')) throw err;
}
}
// Producteur
async function emitEvent(type, payload) {
return redis.xadd(STREAM_KEY, '*',
'type', type,
'payload', JSON.stringify(payload),
'timestamp', Date.now()
);
}
// Consommateur
async function startWorker(workerId) {
await initConsumerGroup();
console.log(`Worker ${workerId} started`);
while (true) {
try {
// Lire les nouveaux messages
const msgs = await redis.xreadgroup(
'GROUP', GROUP_NAME, workerId,
'COUNT', 5, 'BLOCK', 2000,
'STREAMS', STREAM_KEY, '>'
);
if (msgs) {
for (const [, messages] of msgs) {
for (const [id, fields] of messages) {
await processEvent(id, fields, workerId);
}
}
}
// Reprendre les messages pending (timeout dépassé)
const pending = await redis.xautoclaim(
STREAM_KEY, GROUP_NAME, workerId,
PENDING_TIMEOUT, '0-0', 'COUNT', 5
);
if (pending && pending[1]) {
for (const [id, fields] of pending[1]) {
await processEvent(id, fields, workerId);
}
}
} catch (err) {
console.error(`Worker ${workerId} error:`, err);
await new Promise(r => setTimeout(r, 1000));
}
}
}
async function processEvent(id, fields, workerId) {
const type = fields[fields.indexOf('type') + 1];
const payload = JSON.parse(fields[fields.indexOf('payload') + 1]);
console.log(`[${workerId}] Processing ${type}:`, payload);
await redis.xack(STREAM_KEY, GROUP_NAME, id);
}
Implémentez une fonction getDashboardStats() qui récupère en un seul pipeline Redis : total utilisateurs actifs, CA du jour, top 5 produits, sessions actives, et taux d'erreur. Comparez avec et sans pipeline.
async function getDashboardStats() {
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
// AVEC pipeline — 1 round-trip
const start = Date.now();
const pipeline = redis.pipeline();
pipeline.pfcount(`stats:uv:${today}`); // utilisateurs uniques
pipeline.get(`stats:revenue:${today}`); // CA du jour
pipeline.zrevrange('stats:pages:total', 0, 4, 'WITHSCORES'); // top 5
pipeline.scard('online:users'); // sessions actives
pipeline.get(`stats:errors:${today}`); // erreurs
const results = await pipeline.exec();
const pipelineMs = Date.now() - start;
// SANS pipeline — 5 round-trips
const start2 = Date.now();
await redis.pfcount(`stats:uv:${today}`);
await redis.get(`stats:revenue:${today}`);
await redis.zrevrange('stats:pages:total', 0, 4);
await redis.scard('online:users');
await redis.get(`stats:errors:${today}`);
const noPipelineMs = Date.now() - start2;
console.log(`Pipeline: ${pipelineMs}ms vs No pipeline: ${noPipelineMs}ms`);
const [
[, uniqueVisitors],
[, revenueRaw],
[, topPagesRaw],
[, activeSessions],
[, errorsRaw]
] = results;
const topPages = [];
for (let i = 0; i < topPagesRaw.length; i += 2) {
topPages.push({ path: topPagesRaw[i], views: parseInt(topPagesRaw[i+1]) });
}
return {
uniqueVisitors,
revenue: parseFloat(revenueRaw || '0'),
topPages,
activeSessions,
errors: parseInt(errorsRaw || '0'),
pipelineMs, noPipelineMs
};
}