Tout est rapide en préproduction. Puis la production arrive avec un 99e centile qui ressemble à une piste de ski et un cluster Redis « ok » jusqu’à ce qu’il ne le soit plus. Quelqu’un ajoute du caching pour corriger un hotspot MySQL. Le hotspot bouge. Puis le pager débarque dans votre chambre.
Voici la vérité peu glamour : MySQL et Redis ne sont pas des concurrents dans les systèmes réels ; ce sont des collègues. Votre travail consiste à éviter qu’ils ne se contredisent silencieusement sur la réalité. Voilà ce que sont write-through et cache-aside : des contrats entre votre appli, votre cache et votre base de données. Un contrat casse moins souvent — si vous le choisissez pour les bonnes raisons et que vous l’exploitez correctement.
The real question: what breaks less
« MySQL vs Redis » est un faux débat. MySQL est votre système de référence. Redis est votre pari performance. La question qui compte est : quel pattern de mise en cache produit le moins de pannes visibles client en conditions d’exploitation normales — déploiements, pannes partielles, pics de latence et le classique « quelqu’un a exécuté une commande dans le mauvais terminal ».
Si vous voulez l’opinion tout de suite :
- Cache-aside casse moins pour la plupart des applications produit car il échoue en mode ouvert : quand Redis est mal en point, vous pouvez toujours lire et écrire dans MySQL et continuer tant bien que mal.
- Write-through casse moins seulement si vous investissez dans des garanties opérationnelles solides : timeouts disciplinés, mise en file ou retries idempotents, et règles claires sur ce qui se passe si Redis ou MySQL est indisponible.
- Write-through casse plus fort. Ce n’est pas toujours mauvais. Les pannes bruyantes sont débogables. L’incohérence silencieuse est celle qui vous coûte des week-ends.
Il n’y a pas de repas gratuit ; seulement des endroits où vous voulez payer. Cache-aside paie généralement en lectures obsolètes et en stampedes occasionnels. Write-through paie en latence sur le chemin d’écriture et offre plus de façons de bloquer toute l’appli si votre cache décroche.
Une citation à coller sur votre écran, parce qu’elle a sauvé beaucoup de rotations on-call (idée paraphrasée) : Werner Vogels : concevez pour l’échec — supposez que tout échoue et concevez pour que cela ne devienne pas une catastrophe.
Définitions que vous pouvez déployer
MySQL
Base de données relationnelle persistante. Stockage durable, sémantique transactionnelle et souplesse de requête. Aussi : le truc que tout le monde blâme quand l’appli est lente, même si le problème est le réseau, le pool, ou le fait qu’un design de clé de cache soit devenu de l’art performance.
Redis
Magasin de structures de données en mémoire. Utilisé comme cache, file, limiteur de débit, store de sessions, et « base de données temporaire qu’on promet de ne pas traiter comme une base de données ». Redis peut persister (snapshots RDB, logs AOF), peut répliquer et cluser, mais reste une bête différente de MySQL : il optimise la vitesse et la simplicité, pas la correction relationnelle.
Cache-aside (chargement paresseux)
L’application est responsable des lectures et des remplissages du cache :
- Chemin lecture : vérifier Redis → si miss, lire MySQL → écrire dans Redis → retourner.
- Chemin écriture : écrire MySQL → invalider ou mettre à jour Redis.
Point clé : le cache est optionnel. Si Redis échoue, vous pouvez le contourner et interroger MySQL. Votre pire journée devient « lente » plutôt que « indisponible », si MySQL peut supporter la charge.
Write-through (population synchrone)
L’application écrit dans le cache et la base de données dans la même opération logique. Les variantes diffèrent, mais l’esprit est :
- Chemin écriture : écrire dans Redis (ou la couche cache) et MySQL dans le cadre d’une requête unique.
- Chemin lecture : lire depuis Redis ; le cache est supposé être chaud et correct.
Point clé : le cache devient partie intégrante de la correction. Si Redis est malade, votre chemin d’écriture est impacté. Si votre couche write-through ment, votre appli ment.
Blague courte #1 : Write-through, c’est comme une « demande de changement rapide » dans une grosse boîte — rapide jusqu’à ce que les approvals commencent.
Faits & histoire qui comptent vraiment
- Redis a démarré (2009) comme réponse pratique aux accès lents des web apps, pas comme une plateforme unifiée. Son ADN « faire des choses simples extrêmement vite » est toujours présent.
- Memcached a popularisé le cache-aside mainstream. Beaucoup d’habitudes Redis viennent de cette époque : TTL partout, invalidation best-effort, et tolérance à l’incohérence occasionnelle.
- Le query cache de MySQL a été retiré (MySQL 8.0) car il causait de la contention et des performances imprévisibles. Le caching a été poussé vers les couches applicatives et les caches dédiés.
- Redis exécute les commandes en un seul thread (avec un I/O multi-thread dans les versions récentes). C’est pourquoi il est rapide et prévisible — jusqu’à ce que vous lanciez des commandes lentes qui bloquent le monde.
- La persistance Redis est optionnelle et paramétrable : les snapshots RDB échangent durabilité contre vitesse ; AOF échange surcharge disque contre granularité de récupération. Le choix change ce que « write-through » veut dire.
- La réplication est asynchrone par défaut dans Redis et dans beaucoup de topologies MySQL. « Je l’ai écrit » peut signifier « le primaire l’a accepté », pas « c’est à l’abri d’une panne ».
- Le stampede de cache (thundering herd) est connu depuis des décennies dans les gros systèmes web ; des mitigations comme le coalescing de requêtes et les TTLs avec jitter sont des idées anciennes — encore ignorées chaque semaine.
- Redis Cluster sharde par hash slot. Les opérations multi-clés deviennent rapidement compliquées, et les opérations cross-slot peuvent devenir des pièges silencieux pour les workflows write-through.
Write-through vs cache-aside : comparaison décisionnelle
Ce que vous optimisez
Si votre douleur principale est la latence en lecture et que votre jeu de données est relativement stable, cache-aside suffit généralement. Si votre douleur principale est l’amplification des lectures causée par des objets calculés complexes (ex. profil utilisateur assemblé + permissions + compteurs), et que vous voulez un cache constamment chaud, le write-through devient attractif.
Mais ne confondez pas « chaud » et « correct ». Chaud est facile. Correct, c’est là que la facture arrive.
Rayon d’explosion opérationnel
- Cache-aside : panne Redis → charge MySQL plus élevée → possible saturation MySQL → lenteur ou outage partiel. Vous pouvez dégrader gracieusement si vous avez préparé cela.
- Write-through : panne Redis → votre chemin d’écriture peut bloquer ou échouer → défaillances en cascade dans la couche applicative → outage même si MySQL va bien.
Profil de cohérence
Aucun pattern ne vous donne la cohérence transactionnelle entre MySQL et Redis sans machinerie supplémentaire. Le choix est l’incohérence que vous préférez :
- Cache-aside risque des lectures obsolètes après des écritures (courses d’invalidation, latence de réplication, deletes manqués).
- Write-through risque un split-brain de vérité si une écriture réussit et l’autre échoue ou si les retries réordonnent incorrectement. Ce n’est pas obsolète ; c’est contradictoire.
Profil de latence
Cache-aside laisse le chemin d’écriture principalement borné par MySQL, que vous avez déjà optimisé. Write-through ajoute Redis au chemin d’écriture. Si Redis est dans une AZ différente, derrière TLS, ou simplement occupé, félicitations : vous venez de transformer un problème de disque local en un problème de systèmes distribués.
Quand je choisis cache-aside
- Workloads orientés lecture avec cohérence éventuelle tolérable.
- Objets pouvant être régénérés depuis MySQL à la demande.
- Équipes qui veulent un cache qu’on peut bypasser pendant les incidents.
- Modèles de données avec écritures fréquentes et invalidation complexe peuvent fonctionner — si vous gardez les règles simples.
Quand je choisis write-through
- Vous avez une couche/service de cache claire qui porte le contrat write-through et peut être opérée comme un composant de base de données.
- Vous pouvez imposer idempotence et ordering pour les écritures (ou tolérer last-write-wins).
- Vous êtes prêt à budgéter latence et disponibilité pour Redis comme pour une infrastructure critique.
- Vous voulez une chaleur de cache prévisible pour les lectures et pouvez garder les structures de données simples.
Modes de panne : comment chaque pattern meurt
Cache-aside : les classiques
1) Lectures obsolètes dues aux courses d’invalidation
Séquence typique :
- Requête A lit un miss dans le cache, récupère la ligne MySQL (ancienne valeur), puis s’apprête à écrire dans Redis.
- Requête B met à jour la ligne MySQL (nouvelle valeur), supprime la clé Redis.
- Requête A écrit ensuite dans Redis la vieille valeur après le DEL de B.
Résultat : Redis contient des données obsolètes jusqu’à l’expiration TTL ou la prochaine invalidation. Les clients voient un état ancien. Les ingénieurs entendent « mais on a supprimé la clé ». Les deux sont vrais.
Mitigation : clés versionnées, compare-and-set (script Lua avec version), ou écrire dans le cache avec un numéro de version/timestamp monotone.
2) Stampede du cache après expiration
Une clé chaude expire. Des milliers de requêtes missent en même temps. Elles frappent toutes MySQL. MySQL s’effondre. Redis reste sain, regardant le spectacle comme un chat regarde un pointeur laser.
Mitigation : request coalescing (single flight), rafraîchissement probabiliste anticipé, mutex par clé, jitter sur les TTL, et « servir obsolète pendant la revalidation ».
3) Pénétration du cache (misses pour clés inexistantes)
Trafic d’attaque ou clients bogués demandent des IDs inexistants. Les misses du cache ne sont pas mis en cache, donc MySQL se prend une rafale de requêtes inutiles.
Mitigation : negative caching avec TTL court, filtres Bloom, limites de débit.
4) Pannes partielles silencieuses
Les timeouts Redis ne sont pas traités comme des échecs. L’appli attend trop longtemps et bloque des threads. Ou l’appli retry agressivement et devient du DoS.
Mitigation : timeouts stricts, circuit breakers, et sémantique claire « le cache est best-effort ».
Write-through : moins de misses, plus de pièges de correction
1) Incohérence dual-write
Écriture dans Redis réussit ; écriture dans MySQL échoue. Ou inverse. Ou les deux réussissent mais les retries réordonnent les opérations. Maintenant votre cache et votre DB divergent, et votre chemin de lecture est garanti de renvoyer quelque chose — possiblement la mauvaise chose.
Mitigation : outbox transactionnel, change-data-capture (CDC) pour piloter les mises à jour du cache, ou faire de MySQL l’autorité et traiter les écritures cache comme dérivées.
2) Amplification de latence sur le chemin d’écriture
Un hic Redis (fsync AOF lent, jitter réseau, pics CPU). Write-through transforme cela en latence d’écriture visible par l’utilisateur. Les timeouts provoquent des retries. Les retries provoquent de la charge. La charge provoque plus de timeouts.
3) Surprises liées à la persistance Redis
Si vous comptez sur Redis pour la correction write-through, mais que Redis est configuré en snapshot seulement, un crash peut perdre des écritures récentes. MySQL peut être correct ; Redis peut être dans le passé. Si votre appli lit Redis en priorité, vous servirez des voyages dans le temps.
4) Topologie de cluster + pièges de design de clés
Write-through veut souvent une atomicité multi-clés (mettre à jour l’objet + index + compteurs). Redis Cluster ne peut pas exécuter de transactions multi-clés à travers les hash slots. On contourne souvent avec des hash tags, puis on découvre qu’on a construit un hotspot shard.
Blague courte #2 : L’invalidation de cache est un des problèmes difficiles, mais au moins elle n’a pas de réunions. Les dual writes oui.
Trois mini-récits d’entreprise depuis les tranchées
Incident #1 : la panne causée par une mauvaise hypothèse
Une entreprise SaaS de taille moyenne utilisait un setup cache-aside classique : primaire MySQL avec réplicas, Redis pour les objets chauds. L’équipe a supposé « les lectures Redis sont bon marché, donc on peut les faire partout ». Ils ont saupoudré des appels Redis dans tout le code : feature flags, rate limits, sessions utilisateur et quelques checks d’autorisation critiques.
Puis un incident réseau régional a augmenté la perte de paquets entre la couche appli et les nœuds Redis. Redis lui-même était sain. La latence n’était même pas terrible — juste instable, avec des timeouts. Le client Redis de l’appli avait un timeout par défaut de 2 secondes et un retry. Sous charge, les threads se sont accumulés en attendant Redis. Finalement les workers web ont atteint la concurrence maximale et ont cessé d’accepter des requêtes.
MySQL allait bien. Le CPU allait bien. Le responsable incident n’arrêtait pas d’entendre « mais Redis est up ». Oui. Un serveur peut être « up » comme une porte peut être « fermée ». Les deux sont techniquement vrais et opérationnellement inutiles.
Le correctif n’a pas été héroïque. Ils ont réduit les timeouts Redis à quelque chose de réaliste (quelques dizaines de millisecondes, pas des secondes), ajouté un circuit breaker, et — ceci est clé — arrêté d’utiliser Redis comme dépendance dure pour les décisions d’autorisation. Pour l’auth, ils ont mis en cache en processus avec des TTL courts et sont retombés sur MySQL si nécessaire. Redis est redevenu un cache, pas un juge.
Incident #2 : l’optimisation qui a mal tourné
Une plateforme marketplace voulait des lectures de profil plus rapides. Ils ont construit un flux write-through : quand un utilisateur mettait à jour son profil, le service écrivait le blob de profil dénormalisé dans Redis puis mettait à jour MySQL. Les lectures étaient Redis-first, sans fallback MySQL sauf en « maintenance ».
Ça fonctionnait magnifiquement jusqu’à ce qu’un déploiement introduise un bug subtil : les retries sur les échecs d’écriture MySQL n’étaient pas idempotents. Le code ajoutait de nouvelles préférences au lieu de les remplacer, et l’écriture du cache arrivait avant l’écriture MySQL. Lors d’un bref épisode de contention de verrous MySQL, le service a retrié. Redis contenait désormais le blob le plus récent (avec entrées de préférences dupliquées), tandis que MySQL présentait un mélange de lignes anciennes et partiellement mises à jour selon quel retry avait réussi.
Les clients voyaient des profils incohérents selon l’instance de service qui les servait et la clé de cache qu’ils touchaient. Le support décrivait « les paramètres ne tiennent pas ». Les ingénieurs décrivaient « on ne sait pas quel système dit la vérité ». Le cache était devenu une seconde source de vérité, sans la maturité opérationnelle d’une base de données.
Le rollback a rétabli un peu de sanity, mais la vraie réparation a pris plus de temps : ils ont fait de MySQL l’autorité pour les écritures et ont utilisé un flux de changements pour mettre à jour Redis après commit. Ils ont aussi ajouté un champ version et rejeté les écritures de cache plus anciennes. L’optimisation était plus rapide. Elle était aussi menteuse.
Incident #3 : la pratique ennuyeuse mais correcte qui a sauvé la mise
Un service adjacent aux paiements (pas le grand livre principal, mais assez sensible) utilisait cache-aside avec des règles strictes : les caches pouvaient être obsolètes, mais jamais utilisés pour les décisions de solde finales. Chaque clé de cache avait un TTL, une version et un propriétaire. Chaque appel Redis avait un timeout serré et une stratégie de fallback documentée dans un runbook.
Un après-midi, un basculement Redis (triggered par Sentinel) a causé une fenêtre où certains clients écrivaient sur l’ancien primaire, d’autres sur le nouveau. Ce n’était pas catastrophique en soi ; c’est le genre de chaos attendu. Leur appli l’a géré parce qu’ils traitaient Redis comme best effort. Les écritures allaient dans MySQL ; les invalidations de cache étaient tentées mais non requises pour la correction.
Le système a ralenti. Les alertes ont sonné. Mais le service est resté disponible, et la frontière de correction est restée intacte. L’on-call a suivi le runbook : désactiver temporairement les lectures de cache pour les endpoints les plus chauds, laisser MySQL gérer les lectures pendant un moment, et observer la stabilisation des taux d’erreur.
Pas de héros. Pas d’algorithmes novateurs. Juste des contrats clairs et la volonté d’accepter une perte de performance temporaire en échange de ne pas corrompre l’état. Cette discipline « ennuyeuse » est ce que les gens veulent dire quand ils disent que la fiabilité est une fonctionnalité.
Playbook de diagnostic rapide
Quand quelque chose devient lent ou incohérent, vous n’avez pas le temps pour la philosophie. Vous avez besoin d’une séquence courte qui identifie le goulot d’étranglement et le domaine de panne.
Première étape : décider si c’est le chemin Redis, le chemin MySQL, ou l’appli
- Vérifiez les symptômes côté utilisateur : les lectures sont-elles lentes, les écritures lentes, ou les deux ? Les erreurs sont des timeouts ou des désaccords de données ?
- Vérifiez la latence et la saturation de Redis : pics de latence instantanés, clients bloqués, évictions.
- Vérifiez la concurrence MySQL : requêtes en cours, waits de verrous, lag de réplication.
- Vérifiez la santé du pool applicatif : pools de threads/connexions, profondeur des queues, pauses GC.
Deuxième étape : tester les chemins de contournement
- Si vous pouvez sans risque bypasser les lectures Redis pour un endpoint chaud, faites-le et observez si la latence revient à la normale. Si oui, c’est le chemin Redis (ou la librairie cliente) le coupable.
- Si vous pouvez lire temporairement depuis le primaire MySQL au lieu des réplicas, faites-le pour valider un lag de réplication.
Troisième étape : valider la frontière de correction
- Choisissez un utilisateur/objet avec une mise à jour récente connue et comparez directement les valeurs MySQL vs Redis.
- S’ils diffèrent, découvrez si votre pattern peut produire cette différence (course d’invalidation vs échec de dual-write) et agissez en conséquence.
Tâches pratiques : commandes, sorties et décisions
Ce ne sont pas des commandes jouets. Ce sont celles que vous lancez à 2 h du matin pour arrêter de deviner. Chaque tâche inclut ce que signifie la sortie et la décision à prendre.
Tâche 1 : Redis répond-il rapidement depuis l’hôte applicatif ?
cr0x@server:~$ redis-cli -h redis-01 -p 6379 --latency -i 1
min: 0, max: 2, avg: 0.31 (1000 samples)
min: 0, max: 85, avg: 1.12 (1000 samples)
Signification : La seconde ligne montre des spikes occasionnels à 85 ms. Ce n’est pas fatal, mais si votre timeout appli est 50 ms, ça devient des erreurs.
Décision : Si max/avg dépasse votre SLO, enquêtez sur le CPU Redis, les paramètres de fsync de persistance, le jitter réseau, ou les commandes lentes. Envisagez de détendre temporairement la dépendance au cache (fallback) si vous êtes en write-through.
Tâche 2 : les clients Redis s’accumulent-ils ?
cr0x@server:~$ redis-cli -h redis-01 INFO clients | egrep 'connected_clients|blocked_clients'
connected_clients:1248
blocked_clients:37
Signification : blocked_clients > 0 indique souvent des consommateurs BLPOP/BRPOP, des scripts Lua, ou des clients en attente de quelque chose qui n’arrive pas. Dans les caches, les clients bloqués sont généralement mauvais signe.
Décision : Identifiez les commandes bloquantes, vérifiez les scripts Lua lents, ou les transactions coincées. Si blocked_clients augmente avec la latence, traitez cela comme une dégradation de service et réduisez la charge du cache.
Tâche 3 : Redis évince-t-il des clés (pression mémoire) ?
cr0x@server:~$ redis-cli -h redis-01 INFO stats | egrep 'evicted_keys|keyspace_hits|keyspace_misses'
keyspace_hits:93811233
keyspace_misses:12100444
evicted_keys:482919
Signification : Les évictions signifient que votre cache n’est plus un cache mais une machine à churn. Les misses élevés amplifient la charge MySQL. Les évictions détruisent aussi l’hypothèse de « warmness » du write-through.
Décision : Augmentez maxmemory, corrigez les TTL, réduisez la taille des valeurs, améliorez la distribution des clés, ou changez la politique d’éviction. En incident : désactivez le caching pour les endpoints à faible valeur pour réduire le churn.
Tâche 4 : Quelle politique d’éviction est configurée ?
cr0x@server:~$ redis-cli -h redis-01 CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"
Signification : allkeys-lru évince n’importe quelle clé sous pression. Si vous stockez sessions/verrous à côté d’entrées de cache, ils seront aussi évincés. C’est comme ça que vous obtenez des déconnexions aléatoires et des bugs « pourquoi le job a-t-il tourné deux fois ? ».
Décision : Séparez les clés critiques dans une instance/db Redis différente ou utilisez des politiques comme volatile-ttl pour les caches purs. Ne mélangez pas les données « ne doivent pas disparaître » avec les données best-effort.
Tâche 5 : Des commandes lentes bloquent-elles Redis ?
cr0x@server:~$ redis-cli -h redis-01 SLOWLOG GET 5
1) 1) (integer) 912341
2) (integer) 1766812230
3) (integer) 58321
4) 1) "ZRANGE"
2) "leaderboard"
3) "0"
4) "50000"
5) "WITHSCORES"
5) "10.21.4.19:51722"
6) ""
Signification : Un ZRANGE de 58 ms retournant 50k éléments va bloquer la boucle d’événement. Redis est rapide, mais ce n’est pas un miracle. Les grosses réponses coûtent cher.
Décision : Limitez les ranges, paginez, ou repensez l’accès aux données. Si c’est un cache, les grands sorted sets sont souvent des exigences produits accidentelles déguisées.
Tâche 6 : La persistance Redis cause-t-elle la latence d’écriture ?
cr0x@server:~$ redis-cli -h redis-01 INFO persistence | egrep 'aof_enabled|aof_last_write_status|rdb_bgsave_in_progress'
aof_enabled:1
aof_last_write_status:ok
rdb_bgsave_in_progress:0
Signification : AOF est activé. Si le disque est lent ou si fsync est agressif, la latence d’écriture peut spike, particulièrement pénible en write-through.
Décision : Pour un cache pur, envisagez de désactiver AOF ou d’utiliser une politique fsync moins stricte. Pour des usages proches de la correction, mesurez la latence disque et assurez-vous que les paramètres de persistance correspondent à votre modèle de risque.
Tâche 7 : MySQL est-il saturé ou en attente de verrous ?
cr0x@server:~$ mysql -h mysql-01 -e "SHOW PROCESSLIST" | head
Id User Host db Command Time State Info
4123 app 10.21.5.11:53312 prod Query 12 Waiting for table metadata lock UPDATE users SET ...
4188 app 10.21.5.18:50221 prod Query 9 Sending data SELECT ...
Signification : Les waits de verrous de métadonnées suggèrent un DDL ou des changements de schéma en collision avec le trafic. Cache-aside ne vous sauvera pas si les écritures sont bloquées sur des verrous.
Décision : Arrêtez/rollbackez le DDL, ou déplacez-le hors pic avec des outils de changement de schéma en ligne. En attendant, réduisez la concurrence d’écriture ou désactivez les fonctionnalités qui frappent les tables verrouillées.
Tâche 8 : Que dit InnoDB à propos de ce qui se passe maintenant ?
cr0x@server:~$ mysql -h mysql-01 -e "SHOW ENGINE INNODB STATUS\G" | egrep -i 'LATEST DETECTED DEADLOCK|Mutex spin waits|history list length' | head -n 20
LATEST DETECTED DEADLOCK
Mutex spin waits 0, rounds 0, OS waits 0
History list length 987654
Signification : Une grande history list length peut indiquer un lag de purge dû à des transactions longues, menant à du bloat et des performances dégradées. Les deadlocks peuvent montrer des schémas de contention d’écriture.
Décision : Identifiez les transactions longues, corrigez la portée des transactions dans l’appli, ou ajustez l’isolation et l’indexation. Si les invalidations cache-aside dépendent de ces écritures, elles vont aussi se boucher.
Tâche 9 : Le lag de réplication cause-t-il des lectures obsolètes (qu’on blâme sur le cache) ?
cr0x@server:~$ mysql -h mysql-replica-01 -e "SHOW REPLICA STATUS\G" | egrep 'Seconds_Behind_Source|Replica_IO_Running|Replica_SQL_Running'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 47
Signification : 47 secondes de lag ressembleront exactement à une « stale cache » si votre appli lit depuis des réplicas. Vous invaliderez le cache et lirez quand même des données anciennes.
Décision : Routez le trafic read-after-write vers le primaire (ou utilisez une consistance de lecture basée sur GTID). Résolvez les goulots de réplication avant de réécrire la logique de caching.
Tâche 10 : Les clés Redis expirent-elles en vague synchronisée ?
cr0x@server:~$ redis-cli -h redis-01 --scan --pattern 'user:*' | head -n 5
user:10811
user:10812
user:10813
user:10814
user:10815
Signification : Vous échantillonnez l’espace de clés. Si la plupart des clés partagent des TTL identiques (vous le vérifierez ensuite), vous vous préparez à des stampedes.
Décision : Ajoutez du jitter sur les TTL (offset aléatoire), ou implémentez un rafraîchissement anticipé et des locks single-flight pour les clés chaudes.
Tâche 11 : Vérifier la distribution des TTL pour une clé chaude
cr0x@server:~$ redis-cli -h redis-01 TTL user:10811
(integer) 60
Signification : Un TTL propre de 60 secondes est suspect s’il est appliqué largement. Beaucoup d’applis font exactement ça et se demandent ensuite pourquoi la DB fond chaque minute.
Décision : Changez pour quelque chose comme 60±15 secondes de jitter, ou utilisez un soft TTL (servir obsolète pendant la revalidation).
Tâche 12 : Valider une incohérence suspectée (Redis vs MySQL)
cr0x@server:~$ redis-cli -h redis-01 GET user:10811
{"id":10811,"email":"old@example.com","version":17}
cr0x@server:~$ mysql -h mysql-01 -e "SELECT id,email,version FROM users WHERE id=10811"
id email version
10811 new@example.com 18
Signification : Redis est en retard. Avec cache-aside, cela peut être une course d’invalidation ou un delete manqué. Avec write-through, cela peut être un échec dual-write partiel ou un bug d’ordonnancement de replay.
Décision : Si cache-aside : supprimez la clé et auditez les chemins d’invalidation, ajoutez des écritures versionnées. Si write-through : considérez cela comme un incident de correction — stoppez la ligne, trouvez le chemin d’échec du dual-write, envisagez de passer à des mises à jour du cache pilotées par la DB.
Tâche 13 : Vérifier la santé de réplication Redis (si vous avez des replicas)
cr0x@server:~$ redis-cli -h redis-01 INFO replication | egrep 'role|connected_slaves|master_link_status|master_last_io_seconds_ago'
role:master
connected_slaves:1
master_link_status:up
master_last_io_seconds_ago:1
Signification : Si master_link_status est down ou last_io est élevé, un failover peut être en cours ou les replicas sont en retard. En write-through avec lectures depuis des replicas, c’est un piège de cohérence.
Décision : Préférez lire depuis le master pour une cohérence forte-ish, ou acceptez la cohérence éventuelle et traitez les lectures obsolètes comme attendues. Ne prétendez pas avoir les deux.
Tâche 14 : Vérifier la douleur réseau au niveau Linux entre l’appli et Redis/MySQL
cr0x@server:~$ ss -tan state established '( dport = :6379 or dport = :3306 )' | wc -l
842
Signification : Un grand nombre de connexions établies peut indiquer un manque de pooling, du churn de connexions, ou des sockets bloqués. Cela devient de la latence puis des timeouts.
Décision : Assurez-vous que l’appli poolise les connexions, ajustez les limites clients, et vérifiez Redis maxclients / MySQL max_connections. En incident, limitez la concurrence à l’ingress.
Tâche 15 : Vérifier mémoire et fragmentation Redis
cr0x@server:~$ redis-cli -h redis-01 INFO memory | egrep 'used_memory_human|used_memory_rss_human|mem_fragmentation_ratio'
used_memory_human:12.31G
used_memory_rss_human:16.02G
mem_fragmentation_ratio:1.30
Signification : Un ratio de fragmentation de 1.30 suggère un overhead d’allocateur/fragmentation. Pas toujours fatal, mais cela réduit la taille effective du cache et peut déclencher des évictions.
Décision : Si vous êtes bound par éviction, envisagez un redémarrage en fenêtre de maintenance, activez la defrag active, ou redimensionnez la mémoire. Réduisez aussi le churn des valeurs.
Erreurs fréquentes (symptôme → cause racine → correctif)
1) « Redis est up mais le site est down »
Symptôme : Latence élevée des requêtes et timeouts ; Redis montre un CPU et une mémoire sains.
Cause racine : Timeouts côté client trop élevés + retries + exhaustion du thread pool. Redis est « up », mais votre appli est bloquée en attente.
Fix : Fixer des timeouts agressifs (typiquement 5–50 ms selon la topologie), limiter les retries, ajouter un circuit breaker, assurer un fallback vers MySQL pour les lectures cache-aside.
2) « On a invalidé le cache et c’est toujours stale »
Symptôme : Après des mises à jour, certains utilisateurs voient des données anciennes pendant des minutes.
Cause racine : Course d’invalidation (delete puis refill avec l’ancienne valeur), ou lag de réplication (vous lisez d’un replica ancien et repopulez le cache).
Fix : Utiliser des clés versionnées ou des écritures CAS ; router read-after-write vers le primaire ou imposer la consistance de lecture ; ajouter jitter TTL et single-flight.
3) « Les écritures sont devenues plus lentes après l’ajout du caching »
Symptôme : P95 de latence d’écriture en hausse ; timeouts apparaissent sur les endpoints de mise à jour.
Cause racine : Le write-through a ajouté Redis au chemin critique ; fsync de persistance ou jitter réseau amplifie la latence.
Fix : Rendre les écritures du cache asynchrones (DB-authoritative + CDC), ou accepter cache-aside avec invalidation ; tuner la persistance Redis ou isoler un Redis pour cache uniquement.
4) « Déconnexions aléatoires, jobs dupliqués, verrous manquants »
Symptôme : Sessions qui disparaissent ; jobs background qui tournent deux fois ; verrous distribués qui échouent.
Cause racine : Utiliser une seule instance Redis pour clés de cache volatiles et données de coordination éphémères ; la politique d’éviction supprime des clés importantes.
Fix : Séparer les instances Redis ou au moins les budgets mémoire/politiques ; éviter l’éviction pour les données de coordination ; surveiller evicted_keys.
5) « Toutes les minutes la base de données fond »
Symptôme : Pics périodiques de QPS et latence MySQL alignés sur des bornes de TTL.
Cause racine : Expiration synchronisée de TTL sur des clés chaudes ; stampede.
Fix : Jitter sur les TTL, early refresh, request coalescing, servir stale pendant la revalidation, et cache partiel des composants coûteux.
6) « Le hit rate est élevé mais les performances sont toujours mauvaises »
Symptôme : Le hit rate Redis a l’air excellent ; l’appli est toujours lente.
Cause racine : Les payloads volumineux causent un overhead réseau ; coûts de sérialisation/désérialisation ; commandes Redis lentes ; CPU appli saturé.
Fix : Mesurez la taille des payloads, compressez sélectivement, stockez des projections plus petites, évitez les requêtes de range lourdes, profilez le CPU appli.
7) « On a de la corruption de données sans erreurs »
Symptôme : Les utilisateurs voient des états contradictoires selon l’endpoint.
Cause racine : Dual-write sans idempotence et sans garanties d’ordonnancement ; le design write-through assume une symétrie de succès.
Fix : Arrêtez le dual-write dans le chemin de requête. Utilisez outbox transactionnel/CDC pour mettre à jour le cache après commit ; ajoutez des checks de version ; définissez une source de vérité unique.
Checklists / plan étape par étape
Choisir le pattern : un arbre de décision pratique
- Le système peut-il fonctionner correctement sans Redis ?
- Oui → par défaut optez pour cache-aside.
- Non → vous construisez un datastore distribué. Traitez Redis comme une infra critique et demandez-vous si MySQL est toujours nécessaire sur le chemin chaud.
- Exigez-vous une consistance read-after-write pour les flux visibles utilisateur ?
- Oui → cache-aside avec lectures vers le primaire pour la session, ou mises à jour du cache pilotées par la DB avec versioning.
- Non → cache-aside avec TTL + jitter suffit généralement.
- Les écritures sont-elles fréquentes et sensibles à la latence ?
- Oui → évitez le write-through synchrone sauf si Redis est extrêmement proche et très bien opéré.
- Non → write-through peut être acceptable si cela simplifie les lectures et si vous pouvez imposer l’idempotence.
Runbook cache-aside : implémentation « correction d’abord »
- Définissez la source de vérité : MySQL est autoritaire. Redis est dérivé.
- Chemin lecture : Redis GET → en miss, lire MySQL → set Redis avec TTL + jitter.
- Chemin écriture : Écrire MySQL en transaction → après commit, invalider (DEL) ou mettre à jour Redis.
- Prévenir les stampedes : implémentez single-flight par clé (mutex avec TTL court), ou servir stale pendant la revalidation.
- Ajoutez du versioning : embeddez la version dans la valeur ; rejetez les écritures de cache plus anciennes si possible.
- Timeouts rapides : timeout Redis court ; si atteint, sautez le cache et lisez MySQL.
- Observer : suivez hit rate, évictions, latence, et QPS MySQL pendant les drills de bypass du cache.
Runbook write-through : si vous insistez, faites-le sérieusement
- Rendez les écritures idempotentes : les retries ne doivent pas créer d’état nouveau. Utilisez des request IDs, versions, ou upserts soigneusement.
- Définissez l’ordonnancement : last-write-wins nécessite un timestamp/version monotone par objet.
- Planifiez le comportement en cas de panne partielle : si l’écriture Redis échoue mais MySQL réussit, que se passe-t-il ? Si MySQL échoue mais Redis réussit, comment réparer ?
- Privilégiez les mises à jour pilotées par la DB : commit dans MySQL, puis mise à jour de Redis via worker asynchrone consommant un outbox/CDC.
- Budgetez la latence : Redis devient partie du SLO d’écriture. Mesurez, alertez, et planifiez la capacité en conséquence.
- Isolation : ne partagez pas ce Redis avec des caches best-effort et des expérimentations features aléatoires.
Checklist incident : garder le service en vie
- Réduisez le rayon d’explosion : désactivez les lectures de cache pour les endpoints les plus chauds si c’est sûr.
- Limitez la concurrence à l’ingress (load shedding vaut mieux qu’un effondrement total).
- Vérifiez les évictions et la latence Redis ; vérifiez les verrous MySQL et le lag de réplication.
- Si la correction est compromise (incohérence dual-write), figez les écritures ou routez les lectures vers MySQL pendant la réparation.
- Après stabilisation, re-remplissez le cache et exécutez des échantillonnages de cohérence.
FAQ
1) Lequel casse le moins : cache-aside ou write-through ?
Dans la plupart des applications produit : cache-aside casse moins car il peut dégrader vers MySQL quand Redis est lent ou down. Write-through rend Redis partie de votre disponibilité d’écriture.
2) Peut-on rendre write-through sûr ?
Oui, mais « sûr » signifie généralement pas un dual-write synchrone naïf. Le modèle plus sûr est commit MySQL d’abord, puis update asynchrone du cache via outbox/CDC avec versioning.
3) Pourquoi ne pas se reposer uniquement sur la persistance Redis et abandonner MySQL ?
Parfois c’est valable, mais c’est une conception différente. La persistance et le clustering Redis peuvent fonctionner, mais vous perdez les requêtes relationnelles et gagnez de nouvelles contraintes opérationnelles. Ne tombez pas dedans par inadvertance.
4) Quel TTL devrais-je utiliser ?
Choisissez un TTL selon la gravité de la staleness et le coût d’un miss. Puis ajoutez du jitter (offset aléatoire) pour éviter les expirations synchronisées. Les clés chaudes nécessitent souvent un traitement spécial au-delà du TTL.
5) Dois-je mettre à jour le cache à l’écriture ou invalider ?
Invalidation est plus simple et souvent plus sûr, mais peut augmenter les misses. Mettre à jour à l’écriture réduit les misses mais augmente la complexité de correction. Si vous mettez à jour à l’écriture, utilisez versioning ou sémantiques CAS pour éviter les courses.
6) Comment prévenir le cache stampede ?
Utilisez single-flight (lock par clé), servirez stale pendant la revalidation, rafraîchissement anticipé, et jitter sur TTL. Envisagez aussi le negative caching pour les éléments inexistants.
7) Pourquoi mon hit rate est élevé mais MySQL est toujours chaud ?
Parce que les misses restants peuvent être les plus coûteux, ou parce que les appels Redis sont lents/gros, ou parce que vous effectuez du travail MySQL supplémentaire par requête (joins, verrous, requêtes secondaires) non lié aux objets cachés.
8) Redis Cluster est-il nécessaire pour le caching ?
Non. Beaucoup de caches se débrouillent bien avec primaire+replica et Sentinel pour le failover, ou même une instance unique si vous pouvez tolérer une perte. Cluster ajoute une surcharge opérationnelle et des contraintes de hash-slot — utile quand vous avez besoin de scalabilité horizontale.
9) Comment déboguer rapidement des plaintes de « données obsolètes » ?
Choisissez un objet unique, comparez directement les valeurs Redis vs MySQL, et vérifiez le lag de réplication. Identifiez ensuite s’il s’agit d’une course d’invalidation (cache-aside) ou d’un dual-write partiel (write-through).
Conclusion : prochaines étapes que vous pouvez déployer
Si vous voulez quelque chose qui casse moins, choisissez cache-aside avec des timeouts serrés, des fallbacks sensés, et une protection contre les stampedes. Traitez Redis comme une couche de performance, pas comme une couche de vérité. Quand Redis échoue, vous devez devenir plus lent — pas incorrect.
Si vous avez vraiment besoin de la sémantique write-through, n’utilisez pas de dual-writes naïves dans le chemin de requête. Faites de MySQL l’autorité, mettez à jour Redis après commit, et ajoutez du versioning pour que les anciennes écritures de cache ne puissent pas ressusciter des états obsolètes.
Prochaines étapes concrètes :
- Auditez chaque appel Redis : timeout, politique de retry, et si la requête peut réussir sans Redis.
- Ajoutez dashboards/alertes pour la latence Redis, les évictions, les blocked clients, et le lag de réplication MySQL.
- Implémentez du TTL jitter et du single-flight sur vos 20 clés les plus QPS.
- Organisez un game day : désactivez les lectures Redis pour un endpoint et vérifiez que MySQL peut survivre assez longtemps pour une réponse incident.
- Rédigez votre frontière de correction en anglais simple et appliquez-la lors des code reviews.
Les systèmes de production ne récompensent pas la finesse. Ils récompensent les contrats qui tiennent sous stress.