Read in English · Leer en español · Auf Deutsch lesen
Section 7.6 sur 7.8 · Intermédiaire · 24 min de lecture
Stratégies de mise en cache et leurs pièges
Leçon vidéo
5 min · narré en anglais, avec sous-titresÉcouter seulement (audio)
Lire la transcription
Les caches rendent les systèmes rapides, et ils peuvent aussi les faire tomber. Dans cette leçon, vous apprendrez comment fonctionne un cache, comment garder ses copies suffisamment fraîches, et comment l’empêcher de lâcher au pire moment.
Imaginez un café très fréquenté. Au lieu de moudre des grains pour chaque commande, le barista garde sur le comptoir un petit bocal de café moulu. La plupart des commandes sont servies à partir du bocal en quelques secondes. Ce n’est que lorsqu’il est vide que quelqu’un va à la réserve. Le bocal est un cache. Il rend le service rapide, et il ménage la réserve. Mais il soulève des questions. Combien de temps le café moulu peut-il attendre avant d’avoir un goût de rassis ? Et que se passe-t-il quand le bocal se vide pendant le coup de feu du matin ? Les caches logiciels soulèvent exactement les mêmes questions.
Un cache conserve une copie des données dans un endroit plus rapide que leur emplacement d’origine. Il réduit la latence, et il protège la base de données, car chaque succès est une requête que la base de données ne voit jamais. Le prix se paie en exactitude. Un cache est une seconde copie des données, et deux copies peuvent diverger. La première question n’est donc pas de savoir quel cache utiliser. C’est de savoir ce qui peut être périmé, dans quelle mesure, et pour qui. Une carte vieille de cinq minutes, ce n’est pas grave. Un solde bancaire vieux de cinq minutes peut l’être.
Le patron sûr par défaut s’appelle cache-aside. Ici, un service Carte l’utilise. Le service interroge d’abord le cache. En cas de succès, la valeur revient directement. En cas de défaut, il lit la base de données, le système de référence, et stocke le résultat dans le cache avec une durée de vie, après laquelle l’entrée expire. Les écritures empruntent un autre chemin. Mettez d’abord à jour la base de données, puis supprimez l’entrée du cache. N’écrivez pas la nouvelle valeur dans le cache. La lecture suivante rate simplement le cache et recharge la valeur à jour. Et si le cache disparaît complètement, le service lit simplement la base de données, plus lentement. C’est pourquoi cache-aside échoue sans danger.
Garder un cache à jour est notoirement difficile. Une boutade attribuée à Phil Karlton dit qu’il n’y a que deux choses difficiles en informatique : l’invalidation de cache et le nommage des choses. Voici pourquoi. Même la suppression à l’écriture laisse une étroite situation de concurrence. Un lecteur rate le cache et lit un prix de neuf livres dans la base de données. Puis il se met en pause, peut-être à cause d’un réseau lent. Pendant ce temps, un écrivain met à jour le prix à onze livres et supprime l’entrée du cache. Jusque-là, tout va bien. Puis le lecteur reprend et stocke neuf livres dans le cache. Le prix périmé reste jusqu’à l’expiration de sa durée de vie. C’est pourquoi les bonnes conceptions combinent les deux : invalider lors des changements, et garder une durée de vie comme filet de sécurité.
La part des consultations auxquelles le cache répond est son taux de succès. Supposons qu’un service Carte reçoive vingt mille lectures par seconde. Le cache répond en une demi-milliseconde, la base de données en huit, et la base de données supporte environ trois mille requêtes par seconde. Sans cache, chaque lecture va à la base de données : presque sept fois ce qu’elle peut soutenir. À un taux de succès de quatre-vingts pour cent, quatre mille requêtes par seconde atteignent encore la base de données. C’est au-dessus de la capacité, donc les files d’attente s’allongent et la latence augmente. À quatre-vingt-quinze pour cent, c’est mille requêtes par seconde. Confortable. À quatre-vingt-dix-neuf pour cent, la latence moyenne change à peine, mais la charge de la base de données est divisée par cinq. Chaque « neuf » de succès supplémentaire divise la charge de la base de données par environ dix. Maintenant, le cache redémarre vide. Les vingt mille requêtes par seconde frappent toutes une base de données dimensionnée pour trois mille, et elle s’effondre. Un cache dont on ne peut pas se passer est une dépendance critique.
Une ruée se produit quand une entrée populaire expire, et que des centaines de requêtes ratent le cache au même instant et vont toutes à la base de données. Les parades se combinent bien. Le regroupement des requêtes ne laisse qu’une requête par clé aller chercher la valeur, pendant que les autres attendent son résultat. L’expiration avec gigue aléatoire ajoute une durée aléatoire à chaque durée de vie, pour que des entrées écrites ensemble n’expirent pas ensemble. Stale-while-revalidate sert la valeur expirée à tout le monde pendant qu’une requête en arrière-plan en récupère une nouvelle. Et préchauffer un nouveau cache à partir d’une liste de clés très demandées, avant qu’il ne reçoive du trafic, évite la ruée après un redémarrage.
Enfin, quatre erreurs courantes. Ajouter un cache avant de mesurer : souvent, la vraie solution est un index ou une meilleure requête. N’avoir aucune durée de vie, si bien qu’un seul chemin de code oublié laisse des données périmées pour toujours. Dimensionner la base de données uniquement pour la charge mise en cache, si bien qu’un redémarrage du cache fait tomber toute la plateforme. Et mettre en cache des données personnelles ou financières dans des caches partagés. Une page portant le nom d’un client, mise en cache sous sa seule URL, peut être servie au visiteur suivant.
La section complète compare cinq patrons de mise en cache, et couvre les niveaux de cache, les en-têtes HTTP et l’éviction. Ensuite, dans la section sept point sept, nous verrons l’évolution des schémas. À très vite.
Ce qu’il faut savoir
- Un cache échange une seconde copie des données, potentiellement périmée, contre une latence plus faible, un débit plus élevé et la protection de la source ; décidez d’abord ce qui peut être périmé, dans quelle mesure et pour qui.
- Cache-aside est le choix sûr par défaut ; read-through déplace le chargement dans la couche de cache ; write-through garde le cache à jour au prix d’écritures plus lentes ; write-behind risque de perdre des données et ne doit jamais contenir d’argent ; refresh-ahead convient à quelques clés très demandées.
- Combinez l’invalidation lors des changements avec un TTL comme filet de sécurité ; lors des écritures, mettez à jour la base de données, puis supprimez l’entrée du cache au lieu de l’écraser.
- Prévenez les ruées sur le cache avec le regroupement des requêtes, l’expiration avec gigue aléatoire (jitter), stale-while-revalidate, le rafraîchissement anticipé probabiliste et le préchauffage du cache.
- Le taux de succès pèse bien plus sur la charge de la base de données que sur la latence moyenne : chaque « neuf » de succès supplémentaire divise les requêtes à la base de données par environ dix, et un cache froid peut submerger une base de données dimensionnée pour le trafic mis en cache.
- Les caches existent dans les navigateurs, les CDN, les reverse proxies, les applications et les bases de données ; HTTP les contrôle avec Cache-Control, ETag et les requêtes conditionnelles (RFC 9111, RFC 9110).
- LRU et LFU sont les politiques d’éviction courantes ; les clés doivent inclure tout ce qui change la réponse, et les données personnelles ou financières doivent rester hors des caches partagés.
Pourquoi mettre en cache, et ce que cela coûte
Un cache conserve une copie des données dans un endroit plus rapide ou plus proche que leur emplacement d’origine, afin que les requêtes répétées ne paient pas le prix fort à chaque fois. La section 0.2 a présenté l’idée comme un échange d’espace contre du temps. À l’échelle de l’architecture, l’échange est plus intéressant, car un cache modifie trois qualités à la fois. Il réduit la latence (une consultation de Redis dans le même centre de données prend généralement bien moins d’une milliseconde ; une requête qui joint cinq tables peut prendre des dizaines de millisecondes). Il augmente le débit et protège la base de données, car chaque succès (hit) est une requête que la base de données ne voit jamais. Et il peut améliorer la disponibilité, car un cache capable de servir des données légèrement anciennes continue de fonctionner pendant que la source est lente ou indisponible.
Le prix se paie en exactitude et en complexité. Un cache est une seconde copie des données, et deux copies peuvent diverger. Chaque cache incarne donc une décision sur le degré de péremption autorisé pour une réponse, et pour qui. Une carte de restaurant vieille de cinq minutes est acceptable ; un solde bancaire vieux de cinq minutes, montré à un client qui se demande s’il peut se permettre un paiement, ne l’est pas. Les caches ajoutent aussi un composant qui peut tomber en panne, consomment de la mémoire qui coûte de l’argent et créent de nouveaux modes de défaillance, comme les ruées et les démarrages à froid, qui n’existeraient pas sans eux. La première question n’est donc pas quel cache ?
mais qu’est-ce qui peut être périmé, dans quelle mesure, et que se passe-t-il si c’est le cas ?
L’efficacité d’un cache se mesure par son taux de succès : le nombre de succès divisé par le nombre total de consultations. Le taux de succès dépend de l’asymétrie du profil d’accès. Dans la plupart des systèmes grand public, le trafic de lecture est très asymétrique (une petite fraction des restaurants, des produits ou des comptes reçoit l’essentiel des lectures), si bien qu’un cache contenant quelques pour cent des données peut souvent répondre à la plupart des lectures. Un accès aléatoire uniforme, en revanche, se met mal en cache quoi que vous fassiez.
Les cinq patrons classiques
Les patrons de mise en cache diffèrent par l’acteur qui charge le cache, par le moment où les écritures atteignent la source, et donc par le degré de péremption et de durabilité des données. Le tableau les compare ; les paragraphes qui le suivent expliquent où chacun trouve sa place.
Comparaison des patrons de mise en cache
| Patron | Fonctionnement des lectures | Fonctionnement des écritures | Points forts | Risques |
|---|---|---|---|---|
| Cache-aside (chargement paresseux) | L’application consulte le cache ; en cas de défaut (miss), elle lit la base de données et stocke le résultat | L’application écrit dans la base de données, puis supprime (ou met à jour) l’entrée du cache | Simple, ne met en cache que ce qui est utilisé, une panne du cache se traduit seulement par des lectures plus lentes | La première lecture après expiration est lente ; des situations de concurrence peuvent laisser des entrées périmées ; ruées |
| Read-through | L’application interroge le cache ; la bibliothèque de cache charge elle-même depuis la base de données en cas de défaut | Généralement combiné avec des écritures write-through ou cache-aside | La logique de chargement est à un seul endroit | Mêmes problèmes de péremption et de ruée ; la couche de cache doit connaître la source de données |
| Write-through | Comme read-through ou cache-aside | L’écriture va dans le cache et dans la base de données de façon synchrone avant l’accusé de réception | Le cache n’est jamais plus ancien que la base de données pour les données écrites à travers lui | Chaque écriture paie les deux latences ; met en cache des données qui ne seront peut-être jamais lues |
| Write-behind (write-back) | Servies depuis le cache | L’écriture est confirmée dès qu’elle est dans le cache, puis vidée vers la base de données plus tard, souvent par lots | Écritures très rapides ; absorbe les pics ; regroupe le travail de la base de données par lots | Perte de données si le cache tombe en panne avant le vidage ; retard de la base de données ; ordre et conflits |
| Refresh-ahead | Servies depuis le cache | Ne concerne pas les écritures : les entrées très demandées sont rechargées peu avant leur expiration | Les éléments populaires n’expirent jamais sous les yeux des utilisateurs | Rechargements inutiles d’éléments qui n’auraient pas été lus ; nécessite des statistiques d’accès |
Cache-aside est le choix par défaut pour de bonnes raisons : il est facile à comprendre et échoue sans danger, car si le cache disparaît, l’application lit simplement la base de données (plus lentement). Read-through déplace la même logique dans une bibliothèque ou un produit de cache, ce qui est plus net, mais ne change rien au comportement. Write-through convient aux données lues peu après leur écriture, comme la session d’un utilisateur ou son panier. Write-behind est le plus risqué : il fait du cache le système de référence temporaire, si bien qu’il ne convient que lorsque perdre les dernières secondes d’écritures est acceptable (compteurs de vues, événements analytiques, scores de jeu) ou lorsque le cache lui-même est répliqué et durable. N’utilisez jamais write-behind pour de l’argent. Refresh-ahead est une optimisation pour un petit ensemble de clés très demandées, comme la liste des restaurants de la page d’accueil dans une grande ville animée.
Cache-aside en pratique
import json, random
TTL = 300 # seconds
def get_menu(restaurant_id):
key = f"menu:{restaurant_id}:v3" # version the key with the data shape
cached = cache.get(key)
if cached is not None:
return json.loads(cached) # hit
with cache.lock(f"lock:{key}", timeout=5): # coalesce: one loader per key
cached = cache.get(key) # someone may have filled it meanwhile
if cached is not None:
return json.loads(cached)
menu = db.load_menu(restaurant_id) # miss: go to the system of record
jitter = random.randint(0, TTL // 10) # spread expiry times out
cache.set(key, json.dumps(menu), ex=TTL + jitter)
return menu
def update_price(restaurant_id, item_id, price):
db.update_price(item_id, price) # 1. change the system of record
cache.delete(f"menu:{restaurant_id}:v3") # 2. invalidate, do not overwriteDurée de vie et invalidation
Il y a deux façons d’empêcher un cache de servir des données qui ont changé. La première est une durée de vie (TTL) : chaque entrée expire après une période fixe, ce qui borne la péremption sans aucune coordination. Un TTL de cinq minutes sur les cartes signifie qu’un changement de prix est visible partout en moins de cinq minutes. La seconde est l’invalidation explicite : quand la source change, quelque chose supprime ou met à jour la copie en cache. L’invalidation donne des données plus fraîches, mais exige que chaque chemin de code qui modifie les données connaisse chaque cache qui les contient, et c’est exactement ce qui se dérègle à mesure que les systèmes grandissent. Dans les systèmes distribués, l’invalidation est souvent pilotée par des événements : le service Carte publie MenuChanged, et chaque service qui en détient une copie supprime son entrée. La plupart des conceptions professionnelles combinent les deux : invalider lors des changements pour la fraîcheur, et garder un TTL comme filet de sécurité pour l’invalidation qui aura été manquée.
La difficulté est résumée par une boutade attribuée à Phil Karlton, de Netscape : Il n’y a que deux choses difficiles en informatique : l’invalidation de cache et le nommage des choses.
C’est difficile parce que l’invalidation est un problème de systèmes distribués déguisé. L’écriture en base de données et la suppression dans le cache sont deux opérations sur deux systèmes sans transaction commune ; l’une peut donc réussir pendant que l’autre échoue, et des opérations issues de requêtes différentes peuvent s’entrelacer.
Deux règles évitent la plupart des problèmes. D’abord, lors d’une écriture, mettez à jour la base de données, puis supprimez l’entrée du cache ; n’écrivez pas la nouvelle valeur dans le cache. Deux écrivains concurrents qui mettent chacun le cache à jour peuvent terminer dans l’ordre inverse de leurs commits en base de données et laisser l’ancienne valeur en cache indéfiniment, alors qu’une suppression ne peut causer qu’un défaut supplémentaire. Ensuite, acceptez que même la suppression à l’écriture laisse une étroite situation de concurrence, illustrée ci-dessous, et bornez-la avec un TTL. L’article de Facebook de 2013 sur le passage à l’échelle de memcache décrit des baux (leases) pour ce cas : le cache remet un jeton au client qui a subi le défaut, et refuse l’écriture ultérieure de ce client si la clé a été supprimée entre-temps.
La situation de concurrence du SET périmé
Ruées et effet de troupeau
Une ruée sur le cache (cache stampede) se produit quand une entrée populaire expire, ou qu’un nœud de cache redémarre vide, et que des centaines de requêtes concurrentes ratent toutes le cache au même instant. Chacune va à la base de données pour calculer la même valeur, et la base de données, dimensionnée pour le taux de défaut normal du cache, est submergée. Des réponses plus lentes font s’accumuler davantage de requêtes, et la surcharge peut se propager en cascade. Le phénomène voisin de l’effet de troupeau (thundering herd) désigne de nombreux clients en attente réveillés en même temps pour se disputer une seule ressource. Facebook a indiqué que, pour un ensemble de clés particulièrement sujettes à ce problème, les baux ont fait passer le pic de requêtes à la base de données de 17 000 à 1300 par seconde.
Les parades se combinent bien. Le regroupement des requêtes (request coalescing, aussi appelé single flight) ne laisse qu’une requête par clé aller chercher la valeur à la source, pendant que les autres attendent son résultat ; au sein d’un processus, c’est une table de futures en cours ; entre processus, un verrou court dans le cache, comme dans le code ci-dessus. L’expiration avec gigue aléatoire (jitter) ajoute une durée aléatoire à chaque TTL afin que des entrées écrites ensemble (après un déploiement ou un vidage du cache) n’expirent pas ensemble. Stale-while-revalidate sert la valeur expirée à tout le monde pendant qu’une requête en arrière-plan en récupère une nouvelle, ce qui supprime entièrement le pic de latence quand une légère péremption est acceptable. L’expiration anticipée probabiliste (Vattani, Chierichetti et Lowenstein, VLDB 2015) fait décider à chaque lecteur, au hasard et avec une probabilité qui augmente à l’approche de l’expiration, de rafraîchir l’entrée en avance, de sorte qu’une requête la rafraîchit généralement avant qu’elle n’expire réellement. Enfin, le préchauffage d’un nouveau cache à partir d’une liste de clés très demandées, avant qu’il ne reçoive du trafic, évite la ruée du démarrage à froid après un redémarrage.
Exemple commenté
Exemple commenté : ce que le taux de succès fait à la latence et à la charge de la base de données
Le service Carte de la plateforme de livraison de repas reçoit 20 000 lectures de carte par seconde au pic du soir. Une consultation du cache prend 0,5 ms ; une requête à la base de données prend 8 ms. En cas de défaut, le service paie les deux (8,5 ms) et stocke le résultat de façon asynchrone. La base de données peut soutenir environ 3000 requêtes de ce type par seconde avant que la latence ne grimpe en flèche.
| Formule | Latence moyenne = 0,5 + (1 − h) × 8 ms. Charge de la base de données = (1 − h) × 20 000 requêtes par seconde. |
|---|---|
| Aucun cache | 8 ms en moyenne ; 20 000 requêtes par seconde, presque sept fois ce que la base de données peut soutenir. |
| Taux de succès 80 % | 0,5 + 0,2 × 8 = 2,1 ms ; 4000 requêtes par seconde : encore au-dessus de la capacité, donc les requêtes s’accumulent dans la file d’attente de la base de données et la latence augmente. |
| Taux de succès 95 % | 0,5 + 0,05 × 8 = 0,9 ms ; 1000 requêtes par seconde : confortable. |
| Taux de succès 99 % | 0,5 + 0,01 × 8 = 0,58 ms ; 200 requêtes par seconde. |
| De 95 % à 99 % | La latence moyenne ne s’améliore que de 0,32 ms, mais la charge de la base de données est divisée par cinq, de 1000 à 200 requêtes par seconde. |
| Le cluster de cache redémarre vide | Le taux de succès tombe près de 0 % pendant un moment : 20 000 requêtes par seconde frappent une base de données dimensionnée pour 3000. Sans regroupement, préchauffage ni limitation de débit, la base de données s’effondre. |
| Percentiles | À 95 % de succès, 5 % des requêtes prennent 8,5 ms, donc le p95 est à la limite et le p99 vaut 8,5 ms, pas 0,9 ms. |
Le taux de succès compte bien plus pour la base de données que pour la latence moyenne : chaque « neuf » de succès supplémentaire divise la charge de la base de données par environ dix.
Un cache sans lequel le système ne peut pas fonctionner n’est plus une optimisation ; c’est une dépendance critique, et sa défaillance doit être prévue comme la section 2.5 le prévoit pour toute autre.
Les moyennes flattent les caches ; comme l’a montré la section 2.4, ce sont les défauts qui définissent la queue de distribution.
Les niveaux de cache
Les caches existent à chaque niveau entre l’utilisateur et le disque, et une requête reçoit sa réponse du premier niveau qui dispose d’une copie utilisable. Le navigateur garde un cache privé contrôlé par des en-têtes HTTP. Un réseau de diffusion de contenu (CDN) garde des copies partagées dans des emplacements en périphérie (edge), près des utilisateurs, idéal pour les images, les scripts et les réponses d’API publiques comme les cartes. Un reverse proxy à l’origine (Nginx, Varnish) peut mettre en cache des réponses entières pendant quelques secondes, ce qu’on appelle le microcaching, qui peut absorber un pic soudain. L’application met en cache des objets calculés, soit dans le processus (le plus rapide, mais chaque instance a sa propre copie et elles divergent), soit dans un cache partagé comme Redis ou Memcached. La base de données met en cache des pages en mémoire (les shared buffers de PostgreSQL, le buffer pool d’InnoDB) et peut précalculer des résultats coûteux dans des vues matérialisées.
Plus on est loin des données, plus l’économie par succès est grande et plus l’invalidation est difficile. Vous pouvez supprimer une clé de Redis en une milliseconde ; vous pouvez purger un CDN, plus lentement ; vous ne pouvez pas du tout atteindre le navigateur d’un client, si bien que le max-age que vous envoyez est une promesse avec laquelle vous devrez vivre. Les longues durées de vie dans le navigateur sont donc réservées aux fichiers dont l’URL change quand leur contenu change (app.3f9a1c.js), et les courtes, ou la revalidation, à tout le reste.
Où vivent les caches
En-têtes de cache HTTP
HTTP dispose d’un modèle de cache complet, défini dans la RFC 9111 (juin 2022), et bien l’utiliser peut supprimer de la charge avant même qu’elle n’atteigne vos serveurs. Cache-Control: max-age=60 permet à n’importe quel cache de réutiliser une réponse pendant 60 secondes ; s-maxage remplace cette valeur pour les caches partagés comme les CDN ; private interdit aux caches partagés de la stocker (utilisez-le pour tout ce qui est personnel) ; no-cache signifie, de façon déroutante, stocke-la, mais vérifie auprès du serveur avant chaque réutilisation
; et no-store signifie n’en garde jamais de copie
, le bon réglage pour les soldes et les relevés bancaires. Les extensions stale-while-revalidate et stale-if-error (RFC 5861, 2010) appliquent au niveau HTTP les idées de ruée et de disponibilité vues plus haut.
Quand une copie en cache est périmée, le client n’a pas besoin de la télécharger à nouveau. Un validateur lui permet de demander si quelque chose a changé : le serveur envoie un ETag (un identifiant de version opaque) ou une date Last-Modified avec la réponse, et le client envoie plus tard If-None-Match ou If-Modified-Since. Si rien n’a changé, le serveur répond 304 Not Modified sans corps, ce qui économise de la bande passante et souvent l’essentiel du travail du serveur. Ces requêtes conditionnelles sont définies dans la RFC 9110. L’en-tête Vary indique aux caches quels en-têtes de requête (comme Accept-Language) produisent des réponses différentes, afin qu’une carte en français ne soit jamais servie à une requête en anglais.
GET /restaurants/17/menu HTTP/1.1
Host: api.example-food.test
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"
... 18 KB of menu JSON ...
# 100 seconds later the copy is past max-age plus the stale-while-revalidate
# window, so the browser must revalidate before using it:
GET /restaurants/17/menu HTTP/1.1
If-None-Match: "menu-17-v42"
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"
# A bank balance, by contrast, must never sit in any cache:
Cache-Control: no-storeÉviction, dimensionnement et risques de cohérence
Un cache est plus petit que les données qu’il couvre ; quand il est plein, il doit donc évincer quelque chose. La politique moins récemment utilisé (LRU) évince l’entrée restée inutilisée le plus longtemps et fonctionne bien quand l’usage récent prédit l’usage futur. La politique moins fréquemment utilisé (LFU) évince l’entrée la moins souvent utilisée et résiste aux parcours ponctuels (un rapport nocturne qui touche chaque restaurant une fois viderait un cache LRU de ses entrées réellement très demandées). Les caches de production approximent les deux : Redis, par exemple, échantillonne quelques clés et évince le meilleur candidat parmi elles, et propose des politiques comme allkeys-lru, allkeys-lfu et volatile-ttl. Sa politique par défaut, noeviction, rejette les écritures quand la mémoire est pleine, ce qui convient à Redis utilisé comme stockage, mais pas comme cache ; choisissez donc la politique délibérément.
Au-delà de la péremption, les caches introduisent des risques de cohérence plus subtils. Un utilisateur qui change son adresse de livraison puis voit l’ancienne, parce que la lecture provient d’un cache qu’une autre instance n’avait pas encore invalidé, subit une rupture de la garantie de lecture de ses propres écritures (section 7.4) ; une solution courante consiste à contourner le cache pour les écritures récentes de l’utilisateur lui-même. Les caches internes au processus de nombreuses instances divergent entre eux, si bien que des requêtes consécutives acheminées vers des instances différentes peuvent montrer une valeur qui revient en arrière. Mettre en cache une erreur ou un résultat vide sans TTL court transforme une brève panne en une longue. Mettre en cache des données qui mêlent plusieurs utilisateurs (une page portant le nom du client, mise en cache sous la seule URL) divulgue des données personnelles au visiteur suivant, une catégorie bien connue d’incidents de sécurité. Enfin, les clés de cache doivent inclure tout ce qui change la réponse : locataire (tenant), langue, devise, permissions et version du format des données.
Vérifiez votre compréhension
Pourquoi une écriture doit-elle supprimer l’entrée du cache plutôt que d’y placer la nouvelle valeur ?
Parce que deux écrivains concurrents peuvent terminer leurs mises à jour du cache dans l’ordre inverse de leurs commits en base de données, laissant l’ancienne valeur en cache ; une suppression peut au pire causer un défaut supplémentaire, qui recharge la valeur actuelle.
Une consultation du cache coûte 1 ms et une lecture en base de données 10 ms. Quelle est approximativement la latence moyenne à un taux de succès de 90 % avec cache-aside ?
1 + 0,1 × 10 = 2 ms (chaque requête paie la consultation ; une sur dix paie aussi la lecture en base de données).
Quelle est la différence entre Cache-Control: no-cache et no-store ?
no-cache permet à un cache de stocker la réponse, mais l’oblige à la revalider auprès du serveur avant chaque réutilisation ; no-store interdit d’en garder la moindre copie.
Pourquoi faire passer le taux de succès de 99 % à 99,9 % peut-il compter, alors que la latence moyenne change à peine ?
Parce que la charge de la base de données est proportionnelle au taux de défaut, qui passe de 1 % à 0,1 %, soit dix fois moins de requêtes atteignant la base de données.
Sources et lectures complémentaires
Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.
Demander : « Notre API de recherche de restaurants est lente aux heures de pointe. Comment décider s’il faut ajouter un cache, où le placer et quel TTL utiliser ? »