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 :

StructureCommandes typesCas d’usage
StringSET/GET, INCR atomiqueCache, compteurs, rate limiting
HashHSET/HGETALLObjet léger (user:42 → champs)
ListLPUSH + BRPOP (pop bloquant)File de tâches basique
SetSADD/SINTERTags, visiteurs uniques
Sorted setZADD/ZRANGE (tri par score)Leaderboards, sliding window, priorités
StreamXADD/XREADGROUP/XACKFile 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éa dé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 fork et 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, fsync configurable (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, avec allkeys-lru des 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 → HGETALL bloque l’event loop pour tout le monde ; DEL d’une grosse clé bloque aussi → UNLINK (asynchrone) et parcours par lots (HSCAN). Et jamais KEYS * 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, puis redis-cli MONITOR pendant 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:

StructureTypical commandsUse cases
StringSET/GET, atomic INCRCache, counters, rate limiting
HashHSET/HGETALLLightweight object (user:42 → fields)
ListLPUSH + BRPOP (blocking pop)Basic task queue
SetSADD/SINTERTags, unique visitors
Sorted setZADD/ZRANGE (score-ordered)Leaderboards, sliding windows, priorities
StreamXADD/XREADGROUP/XACKMessage 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 ± random desynchronizes expirations: the cheapest defense against a mass stampede.

Persistence — Redis can survive a restart:

  • RDB: periodic binary snapshots (via fork and 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, with allkeys-lru data 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 → HGETALL blocks the event loop for everyone; DEL on a big key blocks too → UNLINK (asynchronous) and batched traversal (HSCAN). And never KEYS * 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, then redis-cli MONITOR while your app runs — watch the commands go by for real

S'entraîner sur ce sujet → Practice this topic →