Un cache qui fonctionne parfaitement peut quand même provoquer une panne
Imaginez la clé la plus consultée de votre boutique, celle qui alimente la page d'accueil, qui expire en pleine campagne promotionnelle. En quelques centaines de millisecondes, des centaines de workers PHP-FPM constatent l'absence de la valeur, lancent chacun la même requête d'agrégation lourde, et MySQL atteint sa limite de connexions. Le cache n'a pas de bug : il fait exactement ce qu'on lui a demandé. Il n'a simplement aucune réponse à la question « que se passe-t-il quand tout le monde rate en même temps ? ». Ce phénomène porte un nom : le cache stampede, ou « ruée sur le cache ».
Cet article s'appuie sur Multi-Tier Caching & Stampede Protection with Redis/Valkey: Singleflight Locks & SWR, publié sur DEV Community, qui décrit une architecture en couches pour s'en protéger. Voici comment la comprendre et l'appliquer à un projet PHP.
Deux niveaux de cache plutôt qu'un
La première couche (L1) vit dans la mémoire du processus lui-même. En PHP-FPM, où chaque requête repart de zéro, on la réserve aux données lues plusieurs fois au cours d'une même requête, ou on s'appuie sur un cache mémoire partagé comme APCu. Elle est extrêmement rapide, mais isolée : chaque worker a la sienne.
La seconde couche (L2) est partagée par tous les workers : Redis ou Valkey. Elle est un peu plus lente, mais cohérente pour toute l'application. Le principe est simple : on interroge L1, puis L2, et seulement en dernier recours la base de données. Chaque niveau absorbe une part du trafic qui, sinon, descendrait plus bas.
Cette hiérarchie réduit la charge, mais elle ne règle pas le cas de la clé qui expire : quand L2 est vide, tout le monde descend d'un coup vers MySQL. Il faut donc ajouter des mécanismes qui contrôlent qui reconstruit la valeur.
Singleflight : un seul worker reconstruit la valeur
Le principe du singleflight (« un seul vol à la fois ») est de désigner un unique worker chargé de recalculer la clé. Les autres ne lancent pas la requête lourde : ils attendent brièvement, ou utilisent une valeur de repli.
En pratique, on pose un verrou dans Redis avec une commande atomique et une expiration de sécurité :
$verrou = $redis->set('lock:accueil', $jeton, ['NX', 'EX' => 10]);
if ($verrou) {
try {
$donnees = $this->calculerListeProduits();
$redis->setex('cache:accueil', 300, serialize($donnees));
} finally {
// libérer le verrou uniquement s'il nous appartient
}
}
Plusieurs points demandent de l'attention :
- Le verrou doit toujours avoir une durée de vie limitée, sinon un worker qui plante bloque la reconstruction indéfiniment.
- On ne libère un verrou que s'il nous appartient encore, d'où l'usage d'un jeton unique, idéalement vérifié dans un script Lua pour rester atomique.
- Les workers qui n'ont pas obtenu le verrou doivent avoir une stratégie claire : brève attente puis nouvelle lecture, ou service d'une valeur périmée (voir ci-dessous).
Le résultat : au lieu de centaines de requêtes identiques vers MySQL, une seule est exécutée.
Stale-while-revalidate : ne jamais faire attendre l'utilisateur
Le verrou protège la base, mais les visiteurs qui attendent la reconstruction subissent quand même de la latence. Le stale-while-revalidate (SWR) résout ce second problème : on conserve la valeur expirée un peu plus longtemps, et on la sert pendant qu'un worker la recalcule en arrière-plan.
On distingue donc deux durées :
- une durée de fraîcheur (par exemple 5 minutes), pendant laquelle la valeur est servie sans question ;
- une durée de tolérance (par exemple quelques minutes de plus), pendant laquelle la valeur est jugée « périmée mais utilisable ».
Passé la fraîcheur, la première requête obtient le verrou et lance le recalcul, tandis que toutes les autres reçoivent immédiatement l'ancienne version. Pour une page d'accueil ou un catalogue, une donnée vieille de quelques secondes est largement acceptable. Pour un solde bancaire ou un stock critique, il faudra évidemment un arbitrage différent.
Disperser les expirations avec du jitter
Dernier ingrédient : éviter que beaucoup de clés expirent au même instant. Si vous avez rempli votre cache lors d'un déploiement ou d'une purge, toutes les clés partagent la même date d'expiration, et le problème revient en masse quelques minutes plus tard.
La parade consiste à ajouter une petite variation aléatoire à chaque durée de vie, ce qu'on appelle le jitter :
$ttl = 300 + random_int(0, 60);
$redis->setex($cle, $ttl, $valeur);
Les expirations s'étalent dans le temps, et la charge de reconstruction se lisse naturellement.
Assembler les pièces
Chaque mécanisme traite un angle différent :
- L1 et L2 absorbent la majorité des lectures ;
- le singleflight garantit qu'une seule reconstruction a lieu à la fois ;
- le SWR évite toute attente côté utilisateur ;
- le jitter empêche les expirations synchronisées.
Ils fonctionnent ensemble, mais on peut les introduire progressivement. Un bon point de départ : identifier les deux ou trois clés les plus chaudes de votre application (page d'accueil, listing produits, configuration globale), et leur appliquer verrou et SWR en priorité. Les frameworks comme Laravel proposent déjà des verrous atomiques sur leurs caches, et Symfony Cache intègre une protection contre le stampede sur ses contrats de cache, ce qui évite souvent de partir de zéro.
Conclusion
Un cache n'est pas seulement un accélérateur : c'est un composant dont le comportement aux limites compte autant que la vitesse nominale. Penser à l'instant où une clé populaire disparaît, c'est éviter la panne en cascade au pire moment, quand le trafic est maximal. Verrou de reconstruction, service de données périmées et expirations dispersées forment un trio simple à mettre en place et très efficace.
Ce que ça change pour vous
Les pannes les plus coûteuses d'une boutique en ligne surviennent souvent aux moments où elle vend le plus : lancement d'une promotion, campagne publicitaire, soldes. Ici, un simple renouvellement de données mis en mémoire peut suffire à saturer le site, alors que rien n'est cassé. Ces protections coûtent peu à mettre en place et évitent de perdre des ventes précisément quand l'audience est la plus forte. Avant votre prochaine grosse opération commerciale, demandez à votre équipe technique ou à votre prestataire comment le site réagit si la page d'accueil doit être recalculée en plein pic de visites.
Et votre site, il vaut quoi ?
Vitesse, accessibilité, référencement technique. Le rapport est écrit pour un dirigeant, pas pour un développeur. Gratuit, sans inscription.