Jour 7 Day 7 · mercredi 5 août 2026 Wednesday 5 August 2026 Backend Intermédiaire
Redis & le caching Redis & caching
Event loop, TTL, cache-aside, stampede : Redis est partout en production, et le caching est le sujet backend où l'on repère vite, en entretien, ceux qui ont vraiment compris. Event loop, TTL, cache-aside, stampede: Redis is everywhere in production, and caching is the backend topic where interviewers quickly spot who really gets it.
L’essentiel
Redis (REmote DIctionary Server) est une base in-memory : des structures de données riches servies depuis la RAM, avec des latences sous la milliseconde et plus de 100 000 opérations/s sur un seul cœur. Usage n°1 : le cache devant une base plus lente. Mais aussi : session store, rate limiter, pub/sub, files de tâches, leaderboards.
Le point d’architecture à connaître absolument : Redis exécute les commandes sur un seul thread, via une event loop (I/O multiplexé). Deux conséquences directes : chaque commande est atomique (aucun lock à gérer), et une commande lente bloque tout le serveur. Depuis Redis 6, des threads gèrent l’I/O réseau, mais l’exécution des commandes reste mono-thread.
Comment ça marche
Redis n’est pas qu’une Map<String, String> géante. Ses structures de données :
| Structure | Commandes types | Cas d’usage |
|---|---|---|
| String | SET/GET, INCR atomique | Cache, compteurs, rate limiting |
| Hash | HSET/HGETALL | Objet léger (user:42 → champs) |
| List | LPUSH + BRPOP (pop bloquant) | File de tâches basique |
| Set | SADD/SINTER | Tags, visiteurs uniques |
| Sorted set | ZADD/ZRANGE (tri par score) | Leaderboards, sliding window, priorités |
| Stream | XADD/XREADGROUP/XACK | File de messages : acks + consumer groups |
TTL et éviction : SET key val EX 300 ou EXPIRE. L’expiration est paresseuse (vérifiée à l’accès) plus un cycle actif d’échantillonnage. Quand maxmemory est atteint, la politique d’éviction tranche : noeviction (les écritures échouent — c’est le défaut !), allkeys-lru (le grand classique du cache), volatile-lru (seulement les clés à TTL), allkeys-lfu (par fréquence, souvent meilleur pour un vrai cache).
Les trois patterns de cache :
- Cache-aside (lazy loading — le standard) : l’app lit le cache ; sur un miss, elle lit la DB puis peuple le cache avec un TTL. Simple, et le cache peut tomber sans casser l’app ; en échange : premier accès lent et fenêtre d’incohérence après une écriture en DB.
- Write-through : chaque écriture passe par le cache, qui écrit dans la DB de façon synchrone. Cache toujours frais, écritures plus lentes.
- Write-behind (write-back) : on écrit dans le cache, qui flushe vers la DB en asynchrone. Écritures ultra-rapides, mais perte possible de données si crash avant le flush.
🎤 En entretien — ne pas réciter les trois patterns : nommer le défaut de chacun. Cache-aside = fenêtre d’incohérence, write-through = latence à l’écriture, write-behind = perte possible de données. C’est le défaut qui prouve qu’on a compris.
Cache-aside en pratique (Node) :
// Lecture : cache d'abord, DB sur miss, puis on repeuple
async function getUser(id) {
const key = `user:${id}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit); // ~1 ms, DB épargnée
const user = await db.users.findById(id); // miss → DB
const ttl = 300 + Math.floor(Math.random() * 60); // jitter
await redis.set(key, JSON.stringify(user), 'EX', ttl);
return user;
}
// Écriture : DB d'abord, puis invalidation de la clé
async function updateUser(id, data) {
await db.users.update(id, data);
await redis.del(`user:${id}`); // le prochain miss repeuplera
}
💡 Le jitter coûte une ligne — mille clés créées au même moment expirent au même moment.
TTL ± aléadésynchronise les expirations : la parade la moins chère contre le stampede de masse.
Persistance — Redis peut survivre à un restart :
- RDB : snapshots binaires périodiques (via
forket copy-on-write). Compact, redémarrage rapide ; mais tout ce qui suit le dernier snapshot est perdu au crash. - AOF : journal de toutes les écritures,
fsyncconfigurable (everysec: au plus ~1 s de perte). Fichier plus gros, rejoué au démarrage, réécrit périodiquement pour compacter. - En pratique : les deux combinés — ou aucun, si Redis n’est qu’un cache reconstructible.
Concepts clés à maîtriser
- L’invalidation, le problème difficile (« There are only two hard things in computer science… »). Trois approches : TTL (staleness bornée, filet de sécurité universel), invalidation explicite (supprimer la clé quand la source change — précis, mais il ne faut oublier aucun chemin d’écriture), clés versionnées (on change la clé, l’ancienne expire d’elle-même). En pratique : TTL partout, plus invalidation explicite sur les données critiques.
- Cache stampede (dogpile) : une clé chaude expire → des centaines de requêtes font miss en même temps → toutes frappent la DB, qui s’écroule. Parades : lock (
SET lock:k v NX EX 10— un seul recalcule, les autres attendent ou servent l’ancienne valeur), TTL jitter (TTL ± aléa pour désynchroniser les expirations), recalcul anticipé avant expiration.
Le stampede en image :
t=0 : la clé chaude expire
│
▼ 500 requêtes simultanées → 500 miss
┌───────┐ 0 hit ┌──────┐
│ Redis │─────────────▶│ DB │ ×500 → surcharge
└───────┘ └──────┘
Parade : SET lock:k v NX EX 10
→ 1 seule requête recalcule la valeur,
les 499 autres attendent ou servent l'ancienne
- Rate limiting : fenêtre fixe =
INCR+EXPIRE(simple, mais effet de bord aux frontières de fenêtre) ; fenêtre glissante = sorted set des timestamps (ZADD+ZREMRANGEBYSCORE+ZCARD). - Atomicité multi-commandes :
MULTI/EXEC(transaction sans rollback) et surtout les scripts Lua (EVAL), exécutés atomiquement — c’est comme ça qu’on écrit un rate limiter correct. - Redis comme file de messages — et ses limites : pub/sub = fire-and-forget (un abonné déconnecté perd tout) ; lists = pas d’acquittement (crash après
BRPOP= message perdu) ; streams = acks, consumer groups, relecture. Pour du routage riche, des dead-letter queues et des garanties contractuelles, une vraie MQ (RabbitMQ, Kafka) reste l’outil dédié.
En entretien
« Pourquoi Redis est-il si rapide ? » — Réponse complète attendue : données en RAM (aucune I/O disque sur le chemin des requêtes), event loop mono-thread (zéro lock, zéro context switch), structures de données optimisées en C, protocole minimal (RESP). « Parce que c’est de la RAM » ne suffit pas.
« Décris le pattern cache-aside. » — Lecture : cache d’abord ; miss → DB → SET avec TTL. Écriture : DB puis invalidation de la clé. Nommer le défaut : entre l’écriture en DB et l’invalidation (ou pendant le TTL), les lecteurs voient l’ancienne valeur.
« Une clé expire et 500 requêtes arrivent en même temps : que se passe-t-il ? » — C’est le cache stampede : toutes font miss et frappent la DB simultanément. Parades : lock distribué (un seul recalcule), TTL jitter, servir la valeur périmée pendant le recalcul.
« RDB vs AOF ? » — RDB : snapshots compacts, restart rapide, perte potentielle de plusieurs minutes. AOF : journal quasi exhaustif (fsync everysec ≈ 1 s de perte max), fichiers plus gros, restart plus lent. Les deux se combinent ; un pur cache peut désactiver les deux.
« Peut-on remplacer RabbitMQ par Redis ? » — Nuancer : pour des jobs simples, lists ou streams suffisent (et les streams ont consumer groups + acks). Pour du routage complexe, des dead-letter queues, des garanties de livraison fortes : une vraie MQ. Montrer qu’on connaît la frontière vaut tous les buzzwords.
Pièges & idées reçues
⚠️ Cache sans TTL — la mémoire se remplit inexorablement ; avec
noeviction(le défaut !) les écritures finissent par échouer, avecallkeys-lrudes données qu’on croyait durables disparaissent en silence. Un cache sans TTL est une fuite mémoire polie : un TTL partout, même long.
- Hot key : une clé ultra-demandée (le profil d’une célébrité) sature le thread unique ou un seul nœud du cluster. Parades : cache local in-process devant Redis, duplication de la clé (
key:1,key:2… lues aléatoirement). - Big key : un hash d’un million de champs →
HGETALLbloque l’event loop pour tout le monde ;DELd’une grosse clé bloque aussi →UNLINK(asynchrone) et parcours par lots (HSCAN). Et jamaisKEYS *en prod :SCAN. - Redis comme base primaire sans réfléchir : entre deux snapshots RDB, un crash perd des minutes de données. Si les données sont précieuses : AOF everysec minimum, réplication — et se demander si une vraie DB ne ferait pas mieux.
- Cacher sans mesurer : un cache se juge à son hit ratio (
INFO stats: keyspace_hits/misses). Cacher des données jamais relues ou des objets énormes sérialisés coûte plus que ça ne rapporte.
Pour aller plus loin
- Redis — Data types et Persistence : les deux pages officielles à lire en entier
- Redis University : cours gratuits, RU101 pour les structures de données
- Valkey : le fork open source (Linux Foundation) né du changement de licence de Redis en 2024 — bon point de culture générale en entretien
- Essayer en local :
docker run --rm -p 127.0.0.1:6379:6379 redis, puisredis-cli MONITORpendant que votre app tourne — voir passer les commandes en vrai
The essentials
Redis (REmote DIctionary Server) is an in-memory database: rich data structures served from RAM, with sub-millisecond latencies and over 100,000 operations/s on a single core. Use case #1: a cache in front of a slower database. But also: session store, rate limiter, pub/sub, task queues, leaderboards.
The architecture point you absolutely must know: Redis executes commands on a single thread, through an event loop (multiplexed I/O). Two direct consequences: every command is atomic (no locks to manage), and a slow command blocks the entire server. Since Redis 6, threads handle network I/O, but command execution remains single-threaded.
How it works
Redis is not just a giant Map<String, String>. Its data structures:
| Structure | Typical commands | Use cases |
|---|---|---|
| String | SET/GET, atomic INCR | Cache, counters, rate limiting |
| Hash | HSET/HGETALL | Lightweight object (user:42 → fields) |
| List | LPUSH + BRPOP (blocking pop) | Basic task queue |
| Set | SADD/SINTER | Tags, unique visitors |
| Sorted set | ZADD/ZRANGE (score-ordered) | Leaderboards, sliding windows, priorities |
| Stream | XADD/XREADGROUP/XACK | Message queue: acks + consumer groups |
TTL and eviction: SET key val EX 300 or EXPIRE. Expiration is lazy (checked on access) plus an active sampling cycle. When maxmemory is reached, the eviction policy decides: noeviction (writes fail — it’s the default!), allkeys-lru (the caching classic), volatile-lru (only keys with a TTL), allkeys-lfu (by frequency, often better for a real cache).
The three caching patterns:
- Cache-aside (lazy loading — the standard): the app reads the cache; on a miss, it reads the DB then populates the cache with a TTL. Simple, and the cache can go down without breaking the app; in exchange: slow first access and an inconsistency window after a DB write.
- Write-through: every write goes through the cache, which writes to the DB synchronously. Cache always fresh, slower writes.
- Write-behind (write-back): you write to the cache, which flushes to the DB asynchronously. Ultra-fast writes, but possible data loss if a crash occurs before the flush.
🎤 In an interview — don’t just recite the three patterns: name each one’s flaw. Cache-aside = inconsistency window, write-through = write latency, write-behind = possible data loss. The flaw is what proves you understood.
Cache-aside in practice (Node):
// Read: cache first, DB on miss, then repopulate
async function getUser(id) {
const key = `user:${id}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit); // ~1 ms, DB spared
const user = await db.users.findById(id); // miss → DB
const ttl = 300 + Math.floor(Math.random() * 60); // jitter
await redis.set(key, JSON.stringify(user), 'EX', ttl);
return user;
}
// Write: DB first, then invalidate the key
async function updateUser(id, data) {
await db.users.update(id, data);
await redis.del(`user:${id}`); // next miss repopulates
}
💡 Jitter costs one line — a thousand keys created at the same moment expire at the same moment.
TTL ± randomdesynchronizes expirations: the cheapest defense against a mass stampede.
Persistence — Redis can survive a restart:
- RDB: periodic binary snapshots (via
forkand copy-on-write). Compact, fast restart; but everything after the last snapshot is lost on a crash. - AOF: a journal of every write, with configurable
fsync(everysec: at most ~1 s of loss). Bigger file, replayed at startup, periodically rewritten to compact it. - In practice: both combined — or neither, if Redis is just a rebuildable cache.
Key concepts to master
- Invalidation, the hard problem (“There are only two hard things in computer science…”). Three approaches: TTL (bounded staleness, the universal safety net), explicit invalidation (delete the key when the source changes — precise, but you must not miss a single write path), versioned keys (change the key, the old one expires on its own). In practice: TTL everywhere, plus explicit invalidation on critical data.
- Cache stampede (dogpile): a hot key expires → hundreds of requests miss at the same time → all hit the DB, which collapses. Countermeasures: a lock (
SET lock:k v NX EX 10— only one recomputes, the others wait or serve the old value), TTL jitter (TTL ± random to desynchronize expirations), early recomputation before expiry.
The stampede in one picture:
t=0 : the hot key expires
│
▼ 500 simultaneous requests → 500 misses
┌───────┐ 0 hits ┌──────┐
│ Redis │─────────────▶│ DB │ ×500 → overload
└───────┘ └──────┘
Fix: SET lock:k v NX EX 10
→ only 1 request recomputes the value,
the other 499 wait or serve the stale one
- Rate limiting: fixed window =
INCR+EXPIRE(simple, but edge effects at window boundaries); sliding window = a sorted set of timestamps (ZADD+ZREMRANGEBYSCORE+ZCARD). - Multi-command atomicity:
MULTI/EXEC(transaction without rollback) and above all Lua scripts (EVAL), executed atomically — that’s how you write a correct rate limiter. - Redis as a message queue — and its limits: pub/sub = fire-and-forget (a disconnected subscriber loses everything); lists = no acknowledgment (crash after
BRPOP= message lost); streams = acks, consumer groups, replay. For rich routing, dead-letter queues and strong contractual guarantees, a real MQ (RabbitMQ, Kafka) remains the dedicated tool.
In an interview
“Why is Redis so fast?” — Full answer expected: data in RAM (no disk I/O on the request path), single-threaded event loop (zero locks, zero context switches), data structures optimized in C, minimal protocol (RESP). “Because it’s RAM” isn’t enough.
“Describe the cache-aside pattern.” — Read: cache first; miss → DB → SET with a TTL. Write: DB then key invalidation. Name the flaw: between the DB write and the invalidation (or during the TTL), readers see the old value.
“A key expires and 500 requests arrive at the same time: what happens?” — That’s the cache stampede: all of them miss and hit the DB simultaneously. Countermeasures: distributed lock (only one recomputes), TTL jitter, serving the stale value during recomputation.
“RDB vs AOF?” — RDB: compact snapshots, fast restart, potential loss of several minutes. AOF: near-exhaustive journal (fsync everysec ≈ at most 1 s of loss), bigger files, slower restart. They combine; a pure cache can disable both.
“Can Redis replace RabbitMQ?” — Nuance it: for simple jobs, lists or streams are enough (and streams have consumer groups + acks). For complex routing, dead-letter queues, strong delivery guarantees: a real MQ. Showing you know the boundary is worth more than any buzzword.
Pitfalls & misconceptions
⚠️ Cache without a TTL — memory fills up inexorably; with
noeviction(the default!) writes eventually start failing, withallkeys-lrudata you thought durable silently disappears. A cache without a TTL is a polite memory leak: put a TTL everywhere, even a long one.
- Hot key: one extremely popular key (a celebrity’s profile) saturates the single thread or a single cluster node. Countermeasures: a local in-process cache in front of Redis, key duplication (
key:1,key:2… read at random). - Big key: a hash with a million fields →
HGETALLblocks the event loop for everyone;DELon a big key blocks too →UNLINK(asynchronous) and batched traversal (HSCAN). And neverKEYS *in production:SCAN. - Redis as a primary database without thinking: between two RDB snapshots, a crash loses minutes of data. If the data is precious: AOF everysec at minimum, replication — and ask yourself whether a real DB wouldn’t do better.
- Caching without measuring: a cache is judged by its hit ratio (
INFO stats: keyspace_hits/misses). Caching data that’s never read again, or huge serialized objects, costs more than it earns.
Going further
- Redis — Data types and Persistence: the two official pages to read in full
- Redis University: free courses, RU101 for the data structures
- Valkey: the open-source fork (Linux Foundation) born from Redis’s 2024 license change — a good general-culture point in interviews
- Try it locally:
docker run --rm -p 127.0.0.1:6379:6379 redis, thenredis-cli MONITORwhile your app runs — watch the commands go by for real