← Tous les cours / Cache & éviction Cours
00 — VUE D'ENSEMBLE

Le cache et le problème de la mémoire limitée

Un cache est une mémoire rapide mais petite — tôt ou tard, elle se remplit. La politique d'éviction décide quoi jeter.

Un cache accélère l'accès aux données en les gardant près du consommateur — en mémoire vive, dans un service dédié comme Redis, ou dans un nœud CDN. Le problème : la mémoire est chère et limitée. Tu ne peux pas tout garder. Tôt ou tard, le cache est plein et tu dois décider quoi jeter.

Politique d'éviction (eviction policy)
Règle qui décide quelle entrée supprimer quand le cache est plein et qu'une nouvelle entrée doit être ajoutée. Chaque politique fait un pari sur le futur : quelle entrée a le moins de chances d'être demandée à nouveau ?

Les trois familles

FamilleCritère de sélectionAlgorithme type
Temporelle (usage récent) La moins récemment utilisée LRU, CLOCK
Fréquentielle (nb accès) La moins fréquemment utilisée LFU, TinyLFU
Expiration (âge) La plus ancienne / expirée TTL, FIFO

Dans la pratique, les systèmes combinent ces familles. Redis supporte 8 politiques qui croisent famille et périmètre (toutes les clés, ou seulement les clés avec TTL). On les découvrira section 05.

Mesurer l'efficacité : hit rate

La métrique clé d'un cache est son taux de succès (hit rate) :

hit rate = hits / (hits + misses)

// Un hit rate de 95% signifie :
// 95 requêtes sur 100 trouvent la donnée en cache
// seulement 5 vont en base
💡 À retenir : le hit rate dépend autant de la taille du cache que de la politique d'éviction. Doubler la taille du cache peut faire passer le hit rate de 80% à 95% pour certaines distributions d'accès. La loi de Zipf s'applique souvent : 20% des clés représentent 80% des accès.

Simule ci-dessous un cache LRU de capacité 4 — ajoute des entrées, observe les évictions :

Cache LRU — capacité 4. Clique GET pour accéder, PUT pour insérer.

Hits: 0 Misses: 0 Hit rate: —
01 — LRU

LRU — Least Recently Used

Jeter ce qui n'a pas été utilisé depuis le plus longtemps — l'intuition derrière LRU et son implémentation O(1).

LRU (Least Recently Used) repose sur un pari : si tu n'as pas utilisé une donnée depuis longtemps, tu ne l'utiliseras probablement pas bientôt. C'est la logique du bureau : les dossiers que tu n'as pas touchés depuis des semaines vont dans un tiroir, les dossiers récents restent à portée de main.

Implémentation O(1)

L'implémentation naïve (liste triée par date d'accès) est O(n) sur les accès. L'implémentation standard est O(1) pour les deux opérations get et put :

Doubly Linked List + HashMap
La liste doublement chaînée maintient l'ordre d'accès (tête = MRU, queue = LRU). La HashMap donne un accès O(1) à n'importe quel nœud par sa clé. Quand on accède à un nœud, on le détache et on le recolle en tête — O(1) grâce aux pointeurs prev/next.
// Structure interne du cache LRU
class LRUCache {
    private head: Node; // sentinel MRU (le plus récent est après)
    private tail: Node; // sentinel LRU (le plus ancien est avant)
    private map: Map<string, Node>;
    private capacity: number;

    get(key: string): any | undefined {
        const node = this.map.get(key);
        if (!node) return undefined; // miss
        this.moveToFront(node); // hit → MRU
        return node.value;
    }

    put(key: string, value: any): void {
        if (this.map.has(key)) {
            this.moveToFront(this.map.get(key)!);
            return;
        }
        if (this.map.size >= this.capacity) {
            this.evict(); // supprimer le nœud avant tail (LRU)
        }
        const node = new Node(key, value);
        this.map.set(key, node);
        this.insertAfterHead(node);
    }
}

Cas d'usage réels

SystèmeUsage LRU
Redis (allkeys-lru) Éviction approximative sur l'ensemble des clés
OS (page cache Linux) Pages mémoire : évincer les moins récemment utilisées
CPU (L1/L2 cache) Lignes de cache : variante pseudo-LRU (PLRU)
Navigateur (cache HTTP) Objets en mémoire : souvent LRU ou variante Clock

Variante CLOCK (Second Chance)

LRU exact requiert des pointeurs par nœud. L'algorithme CLOCK est une approximation O(1) en espace plus compacte : un tableau circulaire avec un bit "récemment utilisé" par case. En cas d'éviction, on tourne l'horloge ; si le bit est à 1, on le met à 0 et on continue ; si le bit est à 0, on évince cette case.

C'est l'algorithme utilisé par le noyau Linux pour la gestion des pages (swap).

💡 LRU-K : variante qui tient compte des K derniers accès plutôt du seul dernier. Un item accédé une seule fois de manière opportuniste (scan séquentiel d'une grande table) ne déplace pas les items chauds. PostgreSQL utilise LRU-2 pour son buffer pool.
🔑 Scan resistance : LRU souffre du scan sequentiel — parcourir une table entière remplit le cache de données "froides" et évince toutes les données chaudes. LRU-K, ARC (Adaptive Replacement Cache) et TinyLFU (utilisé par Caffeine/Java, Nginx) résistent mieux à ce pattern.
02 — LFU

LFU — Least Frequently Used

Jeter ce qui est le moins souvent demandé — LFU est plus précis que LRU, mais son vieillissement pose problème.

LFU (Least Frequently Used) parie que les données les plus souvent demandées dans le passé seront les plus souvent demandées dans le futur. Contrairement à LRU qui regarde quand tu as accédé à une donnée, LFU regarde combien de fois.

Implémentation O(1) — la structure de O'Neil

L'implémentation naïve (tri par compteur) est O(n log n). L'implémentation O(1) de O'Neil utilise :

Double niveau de listes chaînées
  • Une liste de "buckets de fréquence" (freq=1, freq=2, freq=3…)
  • Chaque bucket contient une liste de clés qui ont cette fréquence
  • Une HashMap clé → (nœud dans la liste de son bucket)
  • Un pointeur minFreq vers le bucket de fréquence minimale
Quand on accède à une clé de fréquence N : on la retire du bucket N, on la place dans bucket N+1. Si bucket N est vide, on l'enlève. Si N == minFreq et le bucket est vide, minFreq++.
// get : O(1)
get(key):
  node = map[key]           // O(1)
  freq[key]++               // O(1)
  déplacer node vers bucket freq[key]  // O(1) — pointeurs
  if (minFreq bucket vide) minFreq++
  return node.value

// put : O(1)
put(key, value):
  if (cache plein) évince minFreq bucket.LRU  // O(1)
  insérer key dans bucket freq=1
  minFreq = 1             // le nouveau nœud a fréquence 1

Le problème du vieillissement (aging)

LFU souffre du cache pollution par les anciens accès : une clé très populaire le mois dernier mais plus du tout accédée aujourd'hui gardera un compteur élevé et ne sera jamais évincée, même si de nouvelles clés chaudes ont besoin de cette place.

TinyLFU — la solution moderne
TinyLFU (utilisé par Caffeine en Java, et comme base de allkeys-lfu dans Redis ≥ 4.0) résout le vieillissement avec une fenêtre glissante : les compteurs sont divisés périodiquement par 2 (decay). Une clé qui n'est plus accédée voit son compteur fondre vers zéro et peut être évincée.

LRU vs LFU — quand choisir lequel

LRU
  • Workload temporel : les accès récents prédisent les accès futurs
  • Données de session, pages web visitées
  • Simple à implémenter et déboguer
  • Mauvais avec les scans séquentiels
LFU
  • Workload stable : certaines clés sont structurellement plus populaires
  • Catalogue de produits, pages d'accueil, clés de config
  • Résiste mieux aux scans séquentiels
  • Vieillissement problématique sans decay
💡 ARC (Adaptive Replacement Cache) : IBM Research a proposé ARC, qui combine LRU et LFU en maintenant deux listes : une pour les clés vues une seule fois récemment, une pour les clés vues plusieurs fois. La taille relative des deux listes s'adapte au workload. ZFS utilise ARC pour son cache disque.
03 — TTL

TTL — l'expiration temporelle

Le TTL ajoute une dimension temps au cache — une entrée périme non pas parce qu'elle est déplacée, mais parce qu'elle est trop vieille.

Le TTL (Time To Live) répond à un besoin différent de LRU/LFU : une donnée peut être "fraîche" du point de vue des accès récents, mais périmée du point de vue métier. Le prix d'un produit mis en cache il y a 24 heures n'est plus fiable, même s'il a été accédé il y a 2 secondes.

Deux mécanismes d'expiration

Expiration passive (lazy expiry)
Le cache ne supprime pas les clés expirées proactivement. Quand une requête arrive pour une clé, il vérifie si elle est expirée. Si oui, il la traite comme un miss et la supprime. Avantage : zéro overhead CPU si les clés ne sont jamais ré-accédées. Inconvénient : les clés expirées occupent de la mémoire jusqu'à leur prochain accès.
Expiration active (active eviction)
Un processus de fond scanne périodiquement le cache pour supprimer les clés expirées, même si personne ne les demande. Garantit que la mémoire est libérée rapidement. Redis combine les deux : lazy expiry + balayage probabiliste actif toutes les 100ms.

Redis — l'algorithme d'expiration active

Redis ne peut pas se permettre de scanner toutes les clés (O(n)). Son algorithme actif est probabiliste :

// Toutes les 100ms, Redis fait :
repeat:
  sample = tirer 20 clés au hasard parmi les clés avec TTL
  supprimer toutes celles qui sont expirées
  if plus de 25% des clés sampléees étaient expirées:
      recommencer (il y en a probablement beaucoup d'autres)
  else:
      arrêter

Si le taux d'expiration est faible, l'algorithme s'arrête vite. Si beaucoup de clés expirent en même temps (par exemple, tout un batch mis en cache simultanément), Redis boucle jusqu'à nettoyer suffisamment.

TTL : défini à la création vs renouvelé à chaque accès

ComportementCommande RedisCas d'usage
TTL absolu (depuis la création) SET key val EX 3600 Token d'authentification — périme à heure fixe
TTL renouvelé à chaque accès GET + EXPIRE manuellement Session — périme après N secondes d'inactivité
TTL absolu depuis date absolue EXPIREAT key timestamp Promotion flash — périme à minuit
💡 Jitter sur les TTL : si des milliers de clés sont créées simultanément avec le même TTL (ex. après un déploiement), elles expirent toutes en même temps — provoquant un stampede massif (section 06). Ajoute un jitter aléatoire : TTL = base_ttl + rand(0, base_ttl * 0.1) pour étaler les expirations.

Stale-while-revalidate

La directive HTTP Cache-Control: max-age=60, stale-while-revalidate=30 est une application du TTL avec grâce : pendant 60 secondes, la réponse est fraîche. Entre 60 et 90 secondes, elle est périmée mais utilisable — le cache la sert immédiatement et relance la requête d'origine en arrière-plan. Après 90 secondes, elle est périmée et bloquante.

0–60s
Fraîche → sert directement
60–90s
Périmée → sert + revalide en fond
> 90s
Expiré → bloque jusqu'à revalidation
04 — STRATÉGIES D'ÉCRITURE

Write-through, write-behind, write-around

Écrire dans le cache est aussi complexe que lire — chaque stratégie fait un compromis différent entre latence, cohérence et durabilité.

Écrire dans le cache n'est pas symétrique à la lecture. Quand tu mets à jour une donnée, tu dois décider : est-ce que tu écris aussi en base immédiatement ? En différé ? Tu bypasses le cache ? Chaque réponse est une stratégie différente avec ses propres compromis.

Write-through — la sécurité

Write-through
Chaque écriture met à jour le cache et la base de données de manière synchrone. Le consommateur attend que les deux soient écrits avant d'avoir une confirmation.
Cohérence forte — cache et DB toujours synchrones.
Latence d'écriture élevée — deux round-trips à chaque write.
Le cache peut contenir des données jamais relues (write-heavy workloads).
Client
→ write
Cache
→ write
DB
→ ack
Client ✓

Write-behind (write-back) — la vitesse

Write-behind
L'écriture va dans le cache immédiatement. La base de données est mise à jour en différé (quelques millisecondes à quelques secondes plus tard, via un flush périodique ou un changelog).
Latence d'écriture très faible.
Risque de perte de données si le cache crash avant le flush.
Complexité : gestion du changelog, ordre des mises à jour.
Client
→ write
Cache
→ ack immédiat
Client ✓

… puis en arrière-plan : Cache → DB (flush asynchrone)

Write-around — les données éphémères

Write-around
Les écritures byppassent le cache et vont directement en base. Le cache n'est peuplé que par les lectures (read-on-miss).
Évite de polluer le cache avec des données écrites une seule fois.
Le premier accès en lecture après une écriture est toujours un miss.
Adapté pour les logs, les imports batch, les données rarement relues.

Refresh-ahead — l'anticipation

Le cache prédit les futures expirations et recharge proactivement les données avant qu'elles expirent. Au lieu d'attendre un miss, un agent surveille les TTL et déclenche une revalidation quand le TTL passe sous un seuil.

💡 Read-through : souvent mentionné aux côtés des write strategies. En read-through, c'est le cache lui-même (pas l'application) qui charge la donnée depuis la base en cas de miss. L'application ne voit jamais la source de vérité directement — elle ne parle qu'au cache. C'est le modèle de Redis comme cache-aside vs le modèle Redis comme cache-through.

Compare les trois stratégies principales ci-dessous :

Sélectionne une stratégie puis clique Écrire pour voir les étapes.

05 — REDIS & ÉVICTION

Redis — les 8 politiques d'éviction

Redis ne choisit pas ta politique d'éviction à ta place — tu la declares dans redis.conf et tu en assumes les conséquences.

Redis expose 8 politiques d'éviction, configurables via maxmemory-policy dans redis.conf ou CONFIG SET maxmemory-policy à chaud. Elles se croisent selon deux axes : le périmètre (toutes les clés, ou seulement celles avec un TTL) et le critère (LRU, LFU, random, TTL, rien).

Les 8 politiques

PolitiquePérimètreCritèreCas d'usage typique
noeviction N'évince rien — retourne une erreur OOM Données critiques, durabilité exigée
allkeys-lru Toutes LRU approx. Cache généraliste — workload temporel
allkeys-lfu Toutes LFU (TinyLFU) Catalogue, config — workload stable
allkeys-random Toutes Aléatoire Workload uniforme (rare en prod)
volatile-lru Clés avec TTL LRU approx. Mix cache+persistent — protège les clés sans TTL
volatile-lfu Clés avec TTL LFU (TinyLFU) Mix cache+persistent, stable
volatile-random Clés avec TTL Aléatoire Rarement utile
volatile-ttl Clés avec TTL TTL le plus court Tu veux que les plus proches d'expirer partent en premier

LRU approximatif de Redis

Redis n'implémente pas un LRU exact avec doubly linked list — ce serait trop coûteux en mémoire (2 pointeurs par clé). Il utilise un LRU probabiliste :

// Pour chaque éviction, Redis :
sample_size = maxmemory-samples  // défaut : 5
candidates = tirer sample_size clés au hasard dans le keyspace
evict = candidates.argmin(last_access_time)  // évince le moins récent du sample

Avec maxmemory-samples 10, l'approximation est très proche du LRU exact. Le coût : O(sample_size) par éviction au lieu de O(1) pour le LRU exact, mais sans overhead mémoire.

🔑 LFU dans Redis : Redis 4.0 a introduit les politiques *-lfu. Chaque clé stocke un compteur logarithmique sur 8 bits (0–255) avec decay. La formule de decay est configurable via lfu-decay-time (défaut : 1 minute). Le compteur est incrémenté de manière probabiliste pour ne pas saturer à 255 trop vite.

Configurer la limite mémoire

# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
maxmemory-samples 10  # augmenter pour une meilleure approximation LRU

# À chaud :
CONFIG SET maxmemory 2gb
CONFIG SET maxmemory-policy allkeys-lru
⚠️ Piège noeviction : c'est la valeur par défaut de Redis jusqu'à la version 6.2 (ensuite noeviction reste le défaut). Si tu oublies de configurer maxmemory et maxmemory-policy, Redis refusera les écritures avec OOM command not allowed quand la RAM est saturée. Toujours configurer ces deux directives explicitement en production.
06 — CACHE STAMPEDE

Cache stampede — le troupeau de requêtes

Quand une clé populaire expire, des centaines de requêtes foncent simultanément reconstruire le cache — c'est le stampede.

Imagine une clé très populaire — la page d'accueil de ton site, mise en cache avec un TTL de 5 minutes. Quand ce TTL expire, des centaines de requêtes arrivent simultanément. Chacune fait un miss, chacune pense qu'elle doit reconstruire le cache, toutes foncent sur la base de données en même temps. C'est le cache stampede.

Cache stampede / Thundering herd
Phénomène où l'expiration d'une clé populaire provoque une avalanche de requêtes simultanées vers la source de données, potentiellement saturant la base sous la charge. Aussi appelé "dog-piling" ou "cache miss storm".

Solution 1 — Mutex (verrou de reconstruction)

La première requête qui détecte le miss pose un verrou. Les autres requêtes attendent que le verrou soit libéré et lisent ensuite la valeur reconstruite depuis le cache.

function getWithMutex($key):
  value = cache.get($key)
  if value != null: return value    // hit

  // miss — essayer d'acquérir le verrou
  acquired = redis.set("lock:{$key}", 1, NX, EX 10)
  if acquired:
    value = db.fetch($key)           // seul à reconstruire
    cache.set($key, value, TTL)
    redis.del("lock:{$key}")
    return value
  else:
    sleep(50ms)                       // attendre que le verrou soit libéré
    return getWithMutex($key)        // retry
⚠️ Piège : si la reconstruction plante, le verrou doit expirer automatiquement (TTL sur le verrou lui-même). Sans TTL, un crash laisse le verrou indéfiniment et bloque toutes les requêtes. Toujours définir une expiration sur les verrous Redis.

Solution 2 — Probabilistic Early Expiry (XFetch)

L'algorithme XFetch (Vattani, Chierichetti, Lowenstein, 2015) évite la saturation en déclenchant la revalidation avant l'expiration, de manière probabiliste :

// À chaque lecture :
now = time()
ttl_remaining = cache.ttl(key)
beta = 1.0  // paramètre d'agressivité (1.0 = standard)

// Probabilité de revalider AVANT l'expiration
if now - beta * delta * ln(rand()) > expiry:
  // "Je me comporte comme si c'était expiré"
  // delta = temps de reconstruction observé en secondes
  revalider(key)  // cette requête seule reconstruit
else:
  return cache.get(key)

Plus la reconstruction est longue (delta élevé), plus tôt la revalidation est déclenchée. Le résultat : une seule requête reconstruit, les autres continuent de servir la valeur périmée mais fonctionnelle, sans verrou.

Solution 3 — Stale-while-revalidate

Servir la valeur expirée pendant la reconstruction : le premier miss déclenche une revalidation asynchrone et sert l'ancienne valeur. La valeur "stale" est préférable à un timeout ou une erreur.

Mutex
  • Cohérence forte — une seule reconstruction à la fois
  • Les autres requêtes attendent (latence visible)
  • Adapté aux données critiques qui ne peuvent pas être stale
XFetch / SWR
  • Zéro attente — sert toujours une valeur (peut être stale)
  • Reconstruction en fond ou probabiliste
  • Adapté aux données qui tolèrent quelques secondes de décalage
07 — CACHE WARMING

Cache warming et cold start

Un cache vide après un déploiement répercute tout le trafic sur la base de données — le cache warming évite ce gouffre.

Un cache vide après un déploiement, une mise à l'échelle, ou un flush d'urgence répercute l'intégralité du trafic sur la base de données. Le temps que le cache se remplisse organiquement (quelques minutes à quelques heures selon le TTL), la base agonise sous la charge. C'est le cold start.

Cache warming
Pré-peupler le cache avec les données probablement demandées, avant d'exposer le nouveau serveur au trafic de production. Le cache est "chaud" dès le démarrage.

Stratégies de warming

1. Warming depuis la base de données

Un script charge les données les plus populaires directement en cache avant que le serveur commence à servir des requêtes. Les données "top-N" peuvent être connues depuis les logs ou les statistiques d'accès.

// Warm les 1000 produits les plus consultés
$topProducts = $db->query(
    'SELECT * FROM products ORDER BY views DESC LIMIT 1000'
);
foreach ($topProducts as $product) {
    $redis->set("product:{$product['id']}", json_encode($product), 3600);
}

2. Warming depuis un snapshot

Avant le déploiement, exporter l'état du cache existant (RDB dump Redis) et le charger sur le nouveau serveur. Rapide, mais le snapshot peut être légèrement stale.

3. Shadow traffic / Replay

Dupliquer le trafic de production vers le nouveau serveur (en lecture seule, sans impacter les utilisateurs) pour laisser le cache se remplir organiquement avant le switch DNS.

4. Warming progressif (canary)

Router 1% du trafic vers le nouveau serveur pendant N minutes. Quand le hit rate monte à un niveau acceptable, augmenter progressivement le pourcentage.

🔑 Mesurer le warming : surveille le hit rate en temps réel pendant le warming. En Redis : INFO stats donne keyspace_hits et keyspace_misses. Le rapport hits/(hits+misses) est le hit rate instantané.

Circuit breaker pendant le cold start

Même avec du warming, les premières secondes après un déploiement peuvent voir des pics de charge. Un circuit breaker protège la base :

if (db_error_rate > 50%):
    // Ouvrir le circuit — ne plus appeler la DB
    return serve_stale_or_default()
else if (db_error_rate > 20%):
    // Semi-open — laisser passer 10% des requêtes pour tester
    if (rand() > 0.9): return serve_stale()
    probe = db.fetch()
    if probe.ok: close_circuit()
    else: extend_open()
💡 Cache-aside vs Cache-through pendant le warming : en cache-aside (l'application gère elle-même les miss), le cache se remplit au rythme des accès réels. En cache-through (le cache gère les miss), tu peux implémenter le warming dans le cache lui-même, invisible pour l'application. Symfony Cache Tags ou Redis avec Lua sont des vecteurs pratiques pour le warming programmatique.
08 — CACHE DISTRIBUÉ

Cache distribué — consistent hashing et cohérence

Répartir un cache sur plusieurs nœuds introduit deux problèmes : où ranger chaque clé, et comment garder les copies cohérentes.

Quand un seul serveur Redis ne suffit plus (trop de données, besoin de HA, géographie), on distribue le cache sur plusieurs nœuds. Deux problèmes émergent : comment savoir sur quel nœud une clé est stockée, et comment garder les copies cohérentes.

Consistent Hashing

Le hachage simple (node = hash(key) % N) a un problème majeur : si N change (ajout ou retrait d'un nœud), presque toutes les clés changent de nœud — le cache entier est invalidé.

Consistent Hashing
On place les nœuds et les clés sur un anneau de hachage (0 à 2³²). Une clé est assignée au premier nœud rencontré en tournant dans le sens horaire. Quand un nœud est ajouté ou retiré, seules les clés entre lui et son voisin antihoraire sont redistribuées — en moyenne K/N clés (K = total clés, N = nombre de nœuds).
// Trouver le nœud pour une clé :
hash_value = hash(key)           // ex. 0x3A4F
nodes_sorted = [0x10, 0x40, 0x80, 0xF0]  // positions sur l'anneau
// Premier nœud >= hash_value :
node = nodes_sorted.find(n => n >= hash_value) ?? nodes_sorted[0]  // 0x40

Virtual nodes (vnodes)

Pour équilibrer la distribution, chaque nœud physique est représenté par N nœuds virtuels sur l'anneau (typiquement 150-200). Un serveur puissant peut avoir plus de vnodes.

Nœud A
A-v1, A-v2… A-v150
Répartis sur l'anneau

Invalidation de cache — le problème le plus dur

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

Dans un cache distribué, mettre à jour une clé sur un nœud ne met pas à jour les copies sur les autres. Trois stratégies d'invalidation :

StratégieMécanismeCohérence
TTL-based Toute copie expire après TTL secondes Éventuelle (stale pendant TTL)
Event-driven Pub/Sub : mise à jour DB → event → invalidation de tous les nœuds Quasi-immédiate (propagation < 1ms)
Version/ETag La clé contient une version ; la valeur est rejetée si version < attendue Forte (avec coordination)
🔑 Cache Tags (Symfony/Varnish) : chaque entrée de cache est taguée avec les entités qu'elle dépend (product:42, category:electronics). Quand un produit est mis à jour, invalider le tag product:42 invalide toutes les pages qui le dépendent, en une seule opération. Symfony Cache implementece mécanisme nativement via TagAwareCacheInterface.
⚠️ Redis Cluster vs Redis Sentinel : Redis Cluster répartit les données sur plusieurs nœuds (sharding natif avec 16384 slots hash). Redis Sentinel gère la haute disponibilité d'un seul nœud primaire (failover automatique). Ce sont deux problèmes distincts — Cluster pour la capacité, Sentinel pour la disponibilité.
09 — SYNTHÈSE

Synthèse — le guide de choix

LRU, LFU ou TTL ? Write-through ou write-behind ? Mutex ou probabilistic expiry ? Voici comment choisir.

LRU, LFU, TTL, write-through, Redis — beaucoup d'outils pour un seul problème. Voici comment choisir selon la situation.

Quelle politique d'éviction ?

ContextePolitique recommandéeRaison
Cache généraliste, workload inconnu allkeys-lru Bonne approximation dans la plupart des cas
Catalogue, données "froides puis stables" allkeys-lfu Les accès fréquents sont meilleurs prédicteurs que les accès récents
Mix données persistantes + cache volatile-lru ou volatile-lfu Protège les clés sans TTL (données critiques)
Redis comme base principale (pas cache) noeviction Erreur OOM préférable à la perte silencieuse de données

Quelle stratégie d'écriture ?

Contrainte principaleStratégie
Cohérence forte — données financières, stock Write-through
Faible latence d'écriture — compteurs, analytics Write-behind
Données écrites une seule fois — logs, imports Write-around
Clés très populaires, expiration prévisible Refresh-ahead

Comment protéger contre le stampede ?

Tolérance à la stale data ?Solution
Non — les données doivent être fraîches Mutex Redis (SET NX EX) + TTL sur le verrou
Oui — quelques secondes de décalage OK Stale-while-revalidate ou XFetch probabiliste
Prévention — éviter l'expiration simultanée Jitter sur les TTL (TTL + rand(0, TTL * 0.1))

Anti-patterns à éviter

🚫 Cacher sans maxmemory configuré.
Redis en mode noeviction sans limite mémoire explicite sature la RAM du serveur. Toujours définir maxmemory et maxmemory-policy.
🚫 TTL identique pour toutes les clés du même batch.
Un batch inséré simultanément avec le même TTL expire simultanément → stampede garanti. Ajouter un jitter aléatoire.
🚫 Write-behind sans durabilité.
Write-behind avec Redis en mode AOF désactivé : un crash entre l'écriture cache et le flush DB perd les données silencieusement. Toujours activer appendonly yes ou accepter explicitement le risque de perte.
🚫 Invalider par pattern KEYS* en production.
KEYS product:* scanne l'intégralité du keyspace — O(n) bloquant. Utiliser SCAN (non-bloquant, itératif) ou les Cache Tags (Symfony/Varnish).

Récapitulatif des métriques à surveiller

MétriqueRedisSeuil d'alerte
Hit rate INFO stats → keyspace_hits / (hits + misses) < 80% → cache trop petit ou mauvaise politique
Mémoire utilisée INFO memory → used_memory > 75% de maxmemory → risque d'éviction aggressive
Évictions INFO stats → evicted_keys Croissance rapide → maxmemory trop petit
Latence LATENCY HISTORY > 1ms pour les GETs → congestion ou slow keys