5 exercices · Solutions masquées
Créez un leaderboard de jeu vidéo avec Redis Sorted Sets. Implémentez : addScore(userId, score), getTopN(n), getRank(userId), getScoreAround(userId, n) (n voisins au-dessus et en dessous), et une réinitialisation hebdomadaire.
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const LEADERBOARD_KEY = 'leaderboard:weekly';
async function addScore(userId, score) {
await redis.zadd(LEADERBOARD_KEY, score, `user:${userId}`);
}
async function getTopN(n = 10) {
const raw = await redis.zrevrange(LEADERBOARD_KEY, 0, n - 1, 'WITHSCORES');
const result = [];
for (let i = 0; i < raw.length; i += 2) {
result.push({
userId: raw[i].replace('user:', ''),
score: parseInt(raw[i + 1]),
rank: i / 2 + 1
});
}
return result;
}
async function getRank(userId) {
const rank = await redis.zrevrank(LEADERBOARD_KEY, `user:${userId}`);
if (rank === null) return null;
const score = await redis.zscore(LEADERBOARD_KEY, `user:${userId}`);
return { rank: rank + 1, score: parseInt(score) };
}
async function getScoreAround(userId, n = 3) {
const rank = await redis.zrevrank(LEADERBOARD_KEY, `user:${userId}`);
if (rank === null) return [];
const start = Math.max(0, rank - n);
const end = rank + n;
const raw = await redis.zrevrange(LEADERBOARD_KEY, start, end, 'WITHSCORES');
const result = [];
for (let i = 0; i < raw.length; i += 2) {
result.push({
userId: raw[i].replace('user:', ''),
score: parseInt(raw[i + 1]),
rank: start + i / 2 + 1,
isCurrentUser: raw[i] === `user:${userId}`
});
}
return result;
}
async function resetWeeklyLeaderboard() {
await redis.del(LEADERBOARD_KEY);
console.log('Leaderboard hebdomadaire réinitialisé');
}
Créez un système producteur/consommateur pour envoyer des emails en arrière-plan. Le producteur ajoute des tâches dans une List, le consommateur les récupère avec BLPOP (bloquant). Gérez les dead-letter queue pour les tâches échouées.
const QUEUE_KEY = 'queue:emails';
const DLQ_KEY = 'queue:emails:dead';
const MAX_RETRIES = 3;
// Producteur
async function enqueueEmail(to, subject, body) {
const task = JSON.stringify({ to, subject, body, retries: 0, createdAt: Date.now() });
await redis.rpush(QUEUE_KEY, task);
console.log(`Email enqueued for ${to}`);
}
// Consommateur (workers)
async function emailWorker() {
console.log('Email worker started...');
while (true) {
try {
const [, raw] = await redis.blpop(QUEUE_KEY, 5); // timeout 5s
if (!raw) continue;
const task = JSON.parse(raw);
try {
await simulateSendEmail(task);
console.log(`Email sent to ${task.to}`);
} catch (sendErr) {
task.retries++;
task.lastError = sendErr.message;
if (task.retries >= MAX_RETRIES) {
await redis.rpush(DLQ_KEY, JSON.stringify(task));
console.error(`Dead letter: ${task.to} after ${MAX_RETRIES} retries`);
} else {
await redis.rpush(QUEUE_KEY, JSON.stringify(task)); // re-queue
console.warn(`Retry ${task.retries}/${MAX_RETRIES} for ${task.to}`);
}
}
} catch (err) {
console.error('Worker error:', err);
await new Promise(r => setTimeout(r, 1000));
}
}
}
async function simulateSendEmail({ to, subject }) {
if (Math.random() < 0.2) throw new Error('SMTP timeout'); // 20% failure
console.log(` → Sending "${subject}" to ${to}`);
}
Créez un système de sessions avec Redis Hashes. Implémentez : createSession(userId, data), getSession(sessionId), updateSession(sessionId, fields), deleteSession(sessionId), et getUserActiveSessions(userId).
const crypto = require('crypto');
const SESSION_TTL = 86400; // 24h en secondes
const SESSION_PREFIX = 'sess:';
const USER_SESS_PREFIX = 'user:sess:';
async function createSession(userId, data = {}) {
const sessionId = crypto.randomBytes(32).toString('hex');
const key = SESSION_PREFIX + sessionId;
await redis.hset(key, {
userId: String(userId),
...Object.fromEntries(Object.entries(data).map(([k, v]) => [k, JSON.stringify(v)])),
createdAt: Date.now(),
lastActivity: Date.now()
});
await redis.expire(key, SESSION_TTL);
// Tracker les sessions actives de l'utilisateur
await redis.sadd(USER_SESS_PREFIX + userId, sessionId);
await redis.expire(USER_SESS_PREFIX + userId, SESSION_TTL);
return sessionId;
}
async function getSession(sessionId) {
const key = SESSION_PREFIX + sessionId;
const data = await redis.hgetall(key);
if (!data || !data.userId) return null;
// Renouveler le TTL (sliding expiration)
await redis.expire(key, SESSION_TTL);
await redis.hset(key, 'lastActivity', Date.now());
return data;
}
async function updateSession(sessionId, fields) {
const key = SESSION_PREFIX + sessionId;
const exists = await redis.exists(key);
if (!exists) return false;
await redis.hset(key, { ...fields, lastActivity: Date.now() });
await redis.expire(key, SESSION_TTL);
return true;
}
async function deleteSession(sessionId) {
const key = SESSION_PREFIX + sessionId;
const data = await redis.hgetall(key);
if (data?.userId) {
await redis.srem(USER_SESS_PREFIX + data.userId, sessionId);
}
return await redis.del(key) > 0;
}
async function getUserActiveSessions(userId) {
const sessionIds = await redis.smembers(USER_SESS_PREFIX + userId);
const sessions = await Promise.all(
sessionIds.map(async (sid) => {
const data = await redis.hgetall(SESSION_PREFIX + sid);
return data?.userId ? { sessionId: sid, ...data } : null;
})
);
return sessions.filter(Boolean);
}
Créez un système de statistiques de page vues en temps réel : compteurs par page, par heure et par jour. Implémentez une fonction de rapport qui retourne les 10 pages les plus vues et les pics de trafic horaires.
async function trackPageView(path, userId = null) {
const now = new Date();
const hour = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}${String(now.getHours()).padStart(2,'0')}`;
const day = hour.slice(0, 8);
const multi = redis.multi();
// Total par page
multi.zincrby('stats:pages:total', 1, path);
// Par heure
multi.zincrby(`stats:pages:hour:${hour}`, 1, path);
// Par jour
multi.zincrby(`stats:pages:day:${day}`, 1, path);
// TTL automatique
multi.expire(`stats:pages:hour:${hour}`, 7 * 86400);
multi.expire(`stats:pages:day:${day}`, 90 * 86400);
if (userId) {
// Utilisateurs uniques (HyperLogLog)
multi.pfadd(`stats:uv:${day}`, userId);
multi.expire(`stats:uv:${day}`, 90 * 86400);
}
await multi.exec();
}
async function getTopPages(n = 10) {
const raw = await redis.zrevrange('stats:pages:total', 0, n - 1, 'WITHSCORES');
const result = [];
for (let i = 0; i < raw.length; i += 2) {
result.push({ path: raw[i], views: parseInt(raw[i+1]) });
}
return result;
}
async function getHourlyReport(date) {
// date format: '20240115'
const hours = [];
for (let h = 0; h < 24; h++) {
const hour = date + String(h).padStart(2, '0');
const total = await redis.zcard(`stats:pages:hour:${hour}`);
hours.push({ hour: h, pages: total });
}
return hours;
}
Implémentez un système de cache Redis avec invalidation par tags. cacheSet(key, data, tags[], ttl) stocke les données et associe des tags. invalidateByTag(tag) supprime toutes les clés marquées avec ce tag.
const TAG_PREFIX = 'cache:tag:';
const DATA_PREFIX = 'cache:data:';
async function cacheSet(key, data, tags = [], ttl = 300) {
const dataKey = DATA_PREFIX + key;
const multi = redis.multi();
multi.set(dataKey, JSON.stringify(data), 'EX', ttl);
for (const tag of tags) {
const tagKey = TAG_PREFIX + tag;
multi.sadd(tagKey, key);
multi.expire(tagKey, ttl * 2); // Tag TTL plus long que les données
}
await multi.exec();
}
async function cacheGet(key) {
const raw = await redis.get(DATA_PREFIX + key);
return raw ? JSON.parse(raw) : null;
}
async function invalidateByTag(tag) {
const tagKey = TAG_PREFIX + tag;
const keys = await redis.smembers(tagKey);
if (keys.length) {
const multi = redis.multi();
keys.forEach(k => multi.del(DATA_PREFIX + k));
multi.del(tagKey);
await multi.exec();
console.log(`Invalidated ${keys.length} cache entries for tag: ${tag}`);
}
return keys.length;
}
// Usage
await cacheSet('products:cat:electronics', productList, ['products', 'category:electronics']);
await cacheSet('product:42', singleProduct, ['products', 'product:42']);
// Invalider tout le cache "products" quand on modifie un produit
await invalidateByTag('products');