Redis expliqué simplement : cache, sessions et files d'attente
Redis stocke tout en RAM et répond en moins d'une milliseconde. Voici comment l'utiliser pour accélérer vos applications, gérer les sessions et traiter les tâches en arrière-plan.
Une requête PostgreSQL complexe sur une table de 2 millions de lignes : 180ms. La même donnée mise en cache dans Redis : 0,2ms. Facteur 900.
Redis (Remote Dictionary Server) stocke les données en RAM. Pas de disque dans le chemin critique — la lecture est aussi rapide que la vitesse de la RAM. C'est son avantage fondamental et sa contrainte principale : la RAM est limitée et coûte cher. Redis est conçu pour stocker des données chaudes — celles qui sont accédées souvent et doivent répondre vite.
Démarrer en une ligne
# Docker — le plus rapide
docker run -d --name redis -p 6379:6379 redis:7-alpine
# Ou avec persistance des données
docker run -d --name redis \
-p 6379:6379 \
-v redis-data:/data \
redis:7-alpine redis-server --appendonly yes
# Connexion au CLI
docker exec -it redis redis-cli# Installation native
# Ubuntu / Debian
sudo apt install redis-server
sudo systemctl enable --now redis
# macOS
brew install redis
brew services start redis
# Vérification
redis-cli ping
# PONGLes structures de données Redis
Redis n'est pas juste un dictionnaire clé/chaîne. Il propose des structures natives qui ouvrent des patterns puissants.
String — la base
SET user:42:name "William" # Écrire
GET user:42:name # Lire → "William"
SET counter 0
INCR counter # Atomique → 1
INCRBY counter 5 # Atomique → 6
SETEX session:abc123 3600 "{...}" # Avec TTL (expire dans 3600s)
SET key value EX 300 NX # NX = seulement si n'existe pasHash — objet structuré
HSET user:42 name "William" email "w@iducation.fr" role "admin"
HGET user:42 name # "William"
HGETALL user:42 # Tous les champs
HINCRBY user:42 login_count 1 # Incrémenter un champ
HDEL user:42 role # Supprimer un champPlus efficace qu'un JSON stringifié si vous accédez à des champs individuellement.
List — file FIFO/LIFO
LPUSH queue:emails "job:123" # Ajouter à gauche
RPUSH queue:emails "job:456" # Ajouter à droite
LPOP queue:emails # Retirer à gauche
BRPOP queue:emails 30 # Bloquant — attend 30s si vide
LLEN queue:emails # Longueur
LRANGE queue:emails 0 9 # 10 premiers élémentsSet — ensemble unique
SADD online_users "42" "87" "103"
SISMEMBER online_users "42" # → 1 (présent)
SMEMBERS online_users # Tous les membres
SCARD online_users # Nombre de membres
SREM online_users "87" # SupprimerSorted Set — ensemble trié par score
ZADD leaderboard 1500 "william"
ZADD leaderboard 2300 "alice"
ZADD leaderboard 890 "bob"
ZRANK leaderboard "william" # → 1 (0-indexed, ordre croissant)
ZREVRANK leaderboard "alice" # → 0 (1er en ordre décroissant)
ZRANGE leaderboard 0 9 WITHSCORES REV # Top 10
ZINCRBY leaderboard 100 "william" # +100 pointsParfait pour les classements, les deadlines triées, les tâches planifiées.
Stream — log d'événements
XADD events * type "page_view" url "/articles/redis"
XADD events * type "click" element "nav-link"
XREAD COUNT 10 STREAMS events 0 # Lire depuis le début
XREAD BLOCK 0 STREAMS events $ # Attendre de nouveaux événements1. Cache
Le cache est l'usage le plus fréquent de Redis. Le pattern fondamental s'appelle cache-aside (ou lazy loading).
Pattern cache-aside
async function getUserById(id) {
const cacheKey = `user:${id}`
// 1. Vérifier le cache
const cached = await redis.get(cacheKey)
if (cached) {
return JSON.parse(cached) // Cache HIT — retour immédiat
}
// 2. Cache MISS — interroger la base
const user = await db.query('SELECT * FROM users WHERE id = $1', [id])
if (!user) return null
// 3. Écrire en cache avec TTL
await redis.setex(cacheKey, 300, JSON.stringify(user)) // 5 minutes
return user
}Cache HIT : Redis répond en 0,2ms, la base n'est pas interrogée. Cache MISS : La base est interrogée (180ms), le résultat est mis en cache pour les prochains appels.
Invalidation du cache
Le problème classique : après une modification, le cache contient une valeur périmée.
async function updateUser(id, data) {
// 1. Mettre à jour la base
await db.query('UPDATE users SET name = $1 WHERE id = $2', [data.name, id])
// 2. Invalider le cache — la prochaine lecture ira en base
await redis.del(`user:${id}`)
// Ou si vous voulez garder le cache à jour :
await redis.setex(`user:${id}`, 300, JSON.stringify({ id, ...data }))
}Cache stampede — le piège du traffic burst
Quand une clé expire et que 500 requêtes simultanées arrivent, toutes détectent un MISS et partent en base en même temps. La base s'effondre.
async function getUserSafe(id) {
const cacheKey = `user:${id}`
const lockKey = `lock:user:${id}`
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// Essayer d'obtenir un verrou (NX = seulement si absent)
const locked = await redis.set(lockKey, '1', { NX: true, EX: 5 })
if (!locked) {
// Un autre process reconstruit le cache — attendre et retenter
await new Promise(r => setTimeout(r, 50))
return getUserSafe(id)
}
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [id])
await redis.setex(cacheKey, 300, JSON.stringify(user))
return user
} finally {
await redis.del(lockKey)
}
}Stratégies TTL
// TTL fixe — simple, cache périmé garanti maximum TTL secondes
await redis.setex(key, 300, value)
// TTL aléatoire — évite l'expiration simultanée de beaucoup de clés (thundering herd)
const jitter = Math.floor(Math.random() * 60) // 0-60s
await redis.setex(key, 300 + jitter, value)
// Stale-while-revalidate — toujours une réponse, reconstruction en arrière-plan
async function getWithStale(key, fetchFn, ttl = 300) {
const staleKey = `stale:${key}`
const data = await redis.get(key)
if (data) return JSON.parse(data)
const stale = await redis.get(staleKey)
if (stale) {
// Reconstruire en arrière-plan, retourner le stale immédiatement
fetchFn().then(fresh => {
redis.setex(key, ttl, JSON.stringify(fresh))
redis.setex(staleKey, ttl * 10, JSON.stringify(fresh))
})
return JSON.parse(stale)
}
const fresh = await fetchFn()
await redis.setex(key, ttl, JSON.stringify(fresh))
await redis.setex(staleKey, ttl * 10, JSON.stringify(fresh))
return fresh
}2. Sessions
Les sessions HTTP classiques stockent l'état utilisateur côté serveur. Avec plusieurs instances applicatives, chaque serveur a sa propre mémoire — un utilisateur authentifié sur le serveur 1 sera déconnecté s'il est redirigé vers le serveur 2.
Redis centralise les sessions : toutes les instances lisent et écrivent au même endroit.
import session from 'express-session'
import { createClient } from 'redis'
import RedisStore from 'connect-redis'
const redisClient = createClient({ url: process.env.REDIS_URL })
await redisClient.connect()
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Inaccessible au JavaScript
secure: true, // HTTPS uniquement
sameSite: 'strict', // Protection CSRF
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours
},
}))
// Usage
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password)
req.session.userId = user.id // Stocké dans Redis automatiquement
req.session.role = user.role
res.json({ ok: true })
})
app.get('/profile', async (req, res) => {
if (!req.session.userId) return res.status(401).json({ error: 'Non authentifié' })
const user = await getUserById(req.session.userId)
res.json(user)
})
app.post('/logout', (req, res) => {
req.session.destroy() // Supprime la session dans Redis
res.clearCookie('connect.sid')
res.json({ ok: true })
})Dans Redis, chaque session est une clé sess:abc123 avec une valeur JSON et un TTL. req.session.destroy() supprime la clé immédiatement — déconnexion instantanée et complète, impossible avec JWT.
Sessions vs JWT : le vrai choix
Sessions Redis et JWT résolvent des problèmes différents. Sessions : révocation immédiate, état côté serveur, parfait pour applications web classiques. JWT : stateless, multi-service, mobile — mais pas révocables sans infrastructure supplémentaire.
3. Rate limiting
async function rateLimit(userId, limit = 100, windowSec = 60) {
const key = `ratelimit:${userId}`
const current = await redis.incr(key)
if (current === 1) {
// Première requête de la fenêtre — définir l'expiration
await redis.expire(key, windowSec)
}
return {
allowed: current <= limit,
remaining: Math.max(0, limit - current),
reset: await redis.ttl(key),
}
}
// Middleware Express
app.use('/api', async (req, res, next) => {
const { allowed, remaining, reset } = await rateLimit(req.user?.id ?? req.ip)
res.setHeader('X-RateLimit-Remaining', remaining)
res.setHeader('X-RateLimit-Reset', reset)
if (!allowed) {
return res.status(429).json({ error: 'Trop de requêtes' })
}
next()
})4. Files d'attente (Queue)
Les files d'attente permettent de différer des tâches lourdes — envoi d'email, traitement d'image, génération de PDF — pour les exécuter en arrière-plan sans bloquer la réponse HTTP.
BullMQ — le standard Node.js
npm install bullmqimport { Queue } from 'bullmq'
const emailQueue = new Queue('emails', {
connection: { host: 'localhost', port: 6379 },
})
// Ajouter un job
await emailQueue.add('welcome-email', {
to: 'william@iducation.fr',
template: 'welcome',
username: 'William',
})
// Job avec délai (envoyer dans 24h)
await emailQueue.add('reminder', { userId: 42 }, {
delay: 24 * 60 * 60 * 1000,
})
// Job récurrent (cron)
await emailQueue.add('weekly-digest', {}, {
repeat: { pattern: '0 9 * * 1' }, // Lundi 9h
})
// Job avec priorité (plus basse valeur = plus haute priorité)
await emailQueue.add('urgent-alert', { message: '...' }, { priority: 1 })
await emailQueue.add('newsletter', { ... }, { priority: 10 })import { Worker } from 'bullmq'
const worker = new Worker('emails', async (job) => {
console.log(`Processing job ${job.id}: ${job.name}`)
switch (job.name) {
case 'welcome-email':
await sendEmail(job.data.to, job.data.template, job.data)
break
case 'reminder':
const user = await getUserById(job.data.userId)
await sendReminderEmail(user)
break
}
return { sent: true, timestamp: Date.now() } // Résultat stocké dans Redis
}, {
connection: { host: 'localhost', port: 6379 },
concurrency: 5, // Traiter 5 jobs en parallèle
})
worker.on('completed', (job) => console.log(`Job ${job.id} terminé`))
worker.on('failed', (job, err) => console.error(`Job ${job.id} échoué:`, err))import { QueueEvents } from 'bullmq'
const events = new QueueEvents('emails', { connection: redisConnection })
events.on('completed', ({ jobId }) => console.log(`✓ ${jobId}`))
events.on('failed', ({ jobId, failedReason }) => console.log(`✗ ${jobId}: ${failedReason}`))
// Voir les jobs en attente
const waiting = await emailQueue.getWaiting()
const active = await emailQueue.getActive()
const failed = await emailQueue.getFailed()
// Réessayer les jobs échoués
await emailQueue.retryJobs({ status: 'failed' })BullMQ gère automatiquement les retries avec backoff exponentiel, les jobs bloquants, la priorité, les jobs récurrents, et la visibilité de l'état de chaque job.
Pub/Sub — événements en temps réel
import { createClient } from 'redis'
// Publisher
const publisher = createClient()
await publisher.connect()
await publisher.publish('notifications', JSON.stringify({
userId: 42,
type: 'new_comment',
articleId: 'docker-introduction',
}))
// Subscriber (dans un process séparé ou Worker)
const subscriber = createClient()
await subscriber.connect()
await subscriber.subscribe('notifications', (message) => {
const event = JSON.parse(message)
// Envoyer via WebSocket au bon utilisateur
wsServer.sendToUser(event.userId, event)
})Persistance : ne pas tout perdre au redémarrage
Par défaut Redis est volatile — un redémarrage vide tout. Deux modes de persistance :
RDB (Redis Database) — snapshot périodique sur disque. Rapide à restaurer, mais vous perdez les données depuis le dernier snapshot.
AOF (Append-Only File) — journalise chaque commande d'écriture. Durabilité maximale, fichier plus volumineux, restauration plus lente.
# redis.conf — les deux en même temps (recommandé en production)
save 900 1 # Snapshot si 1 clé modifiée en 15 min
save 300 10 # Snapshot si 10 clés modifiées en 5 min
appendonly yes # Activer AOF
appendfsync everysec # Flush AOF toutes les secondesPour un cache pur : pas besoin de persistance, désactivez-la. Pour les sessions et files d'attente : AOF recommandé.
Politique d'éviction mémoire
Quand Redis atteint sa limite maxmemory, il doit supprimer des clés.
# redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru # Supprimer les clés les moins récemment utiliséesPolitiques disponibles :
| Politique | Comportement |
|---|---|
noeviction | Erreur si mémoire pleine (défaut) |
allkeys-lru | Supprimer n'importe quelle clé (LRU) |
volatile-lru | Supprimer seulement les clés avec TTL (LRU) |
allkeys-lfu | Supprimer les clés les moins fréquemment utilisées |
allkeys-random | Supprimer aléatoirement |
Pour un cache : allkeys-lru. Pour des données mixtes (cache + sessions) : volatile-lru (protège les sessions sans TTL).
Commandes utiles en production
# Monitoring
redis-cli info memory # Utilisation mémoire
redis-cli info stats # Statistiques (hits, misses, connexions)
redis-cli info replication # État de la réplication
redis-cli monitor # Voir toutes les commandes en temps réel (dev only)
# Cache hit rate
redis-cli info stats | grep hit
# keyspace_hits:1523
# keyspace_misses:47
# Hit rate = 1523 / (1523 + 47) = 97%
# Inspection
redis-cli keys "user:*" # DANGER en prod — scan à la place
redis-cli scan 0 MATCH "user:*" COUNT 100
redis-cli type user:42 # Type d'une clé
redis-cli ttl user:42 # TTL restant (-1 = pas d'expiration, -2 = expirée)
redis-cli object encoding user:42 # Encodage interne
# Nettoyage
redis-cli del key1 key2 # Supprimer des clés
redis-cli flushdb # Vider la DB courante (DANGER)Redis n'est pas une base de données principale — c'est une couche de performance et d'architecture. Dans l'architecture multi-base, PostgreSQL stocke les données durables, Redis accélère les accès fréquents et décorrèle les tâches lourdes. Les deux fonctionnent mieux ensemble que séparément.
Un bon indicateur que vous avez besoin de Redis : votre application passe plus de temps à attendre la base de données qu'à traiter la logique métier. Mesurez d'abord — optimisez ensuite.