Pourquoi utiliser le cache ?
Un cache est une copie de données conservée dans un endroit plus rapide d’accès que la source. Chaque décision d’implémenter un cache est en réalité un pari : celui que la même valeur sera demandée à nouveau avant d’être modifiée, et que servir une copie légèrement obsolète est acceptable. Lorsque ces deux conditions sont réunies, le gain est énorme.
Un seul “hit” (accès réussi au cache) produit trois effets :
- La latence. Une recherche dans Redis répond en bien moins d’une milliseconde ; la même ligne dans PostgreSQL coûte plusieurs millisecondes de réseau, de planification et d’E/S. Sur une page qui lit vingt éléments, cette différence définit toute l’expérience utilisateur.
- La charge. Si 95 % des lectures sont servies par le cache, la base de données ne voit qu’une seule requête là où elle en voyait vingt. Vous pouvez ainsi survivre à un pic de trafic, ou utiliser une instance plus petite, sans modifier une seule ligne de logique de requête.
- Le coût. Moins de lectures en base de données signifient des instances plus petites, moins de réplicas de lecture et moins de trafic inter-régions. À grande échelle, le caching est l’une des rares optimisations qui se rentabilise financièrement.
La contrepartie est la complexité. Un cache est une seconde copie de la vérité, et chaque copie peut diverger. L’essentiel de ce guide consiste à faire en sorte que cette divergence reste minime et temporaire.
Où peut se situer un cache
Le caching n’est pas un système unique, mais une pile de couches, chacune étant plus proche du lecteur et possédant sa propre logique d’invalidation.
- Le navigateur. Les réponses HTTP marquées
Cache-Control: max-agesont réutilisées sans nouvelle requête. C’est gratuit, privé et largement sous le contrôle du client. - Le CDN ou l’edge. Un cache partagé placé devant votre origine absorbe le trafic pour les réponses publiques. C’est rapide et global, mais une erreur à ce niveau est visible par tout le monde.
- L’application. Un cache in-process (un
Map, un LRU) est le niveau le plus rapide car il évite totalement le réseau. Il est également propre à chaque instance, il doit donc être léger et jetable. - Un store partagé. Redis ou Memcached se situe à côté de l’application et sert chaque instance à partir d’une copie cohérente. C’est là que se trouve la majeure partie du caching applicatif.
- La base de données. Les buffer pools, les vues matérialisées, les plans préparés et les caches de résultats de requêtes permettent à la source de rester rapide par elle-même. C’est la dernière ligne de défense avant le disque.
Une requête peut recevoir une réponse à n’importe laquelle de ces couches. Plus la réponse est proche, plus elle est rapide, mais plus elle est difficile à invalider : c’est là que réside toute la tension centrale de ce sujet.
Taux de succès du cache (hit rate) : la métrique déterminante
La santé d’un cache se mesure à son hit rate : le nombre de succès (hits) divisé par la somme des succès et des échecs (misses). C’est le seul chiffre qui vous indique si le cache est réellement utile.
hit rate = hits / (hits + misses)
L’impact arithmétique est radical. Avec un hit rate de 90 %, la base de données ne reçoit qu’une requête sur dix, soit une réduction par 10. À 50 %, elle en reçoit une sur deux, soit seulement une réduction par 2, alors que le cache a ajouté un saut réseau à la moitié du trafic. Un cache avec un hit rate faible peut s’avérer plus lent que l’absence totale de cache.
Mesurez-le directement depuis le store. Redis rapporte keyspace_hits et keyspace_misses via INFO stats, et les bibliothèques clientes exposent ces mêmes compteurs. Surveillez le hit rate après chaque modification d’une clé, d’un TTL ou d’une requête ; un léger changement de clé peut faire chuter silencieusement un cache de 95 % à 40 %.
Le corollaire est que vous devez mettre en cache des éléments qui sont réellement répétés. Une clé unique par requête a, par construction, un hit rate de 0 % et ne fait que gaspiller de la mémoire.
Les quatre patterns fondamentaux
Presque toutes les implémentations de cache suivent l’un de ces quatre modèles. Ils diffèrent selon qui remplit le cache et quand celui-ci est mis à jour.
Cache-aside (chargement paresseux)
L’application gère la logique. Lors d’une lecture, elle vérifie le cache et, en cas d’absence (miss), elle charge la donnée depuis la source puis écrit le résultat dans le cache. C’est le pattern par défaut car il est simple, fonctionne avec n’importe quel store et ne met en cache que les données réellement demandées.
async function getUser(id: string) {
const key = `user:${id}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const user = await db.user.findUniqueOrThrow({ where: { id } });
await redis.set(key, JSON.stringify(user), "EX", 600);
return user;
}
L’inconvénient est que le premier lecteur paie toujours le prix fort (en termes de latence), et le cache peut contenir des données obsolètes jusqu’à l’expiration de son TTL ou jusqu’à ce qu’une écriture les supprime.
Read-through
Le read-through déplace la logique de repli directement dans la couche de cache : l’application interroge toujours le cache, et celui-ci est configuré avec un chargeur (loader) qui s’exécute en cas de miss. Certaines bibliothèques et proxys implémentent cela, de sorte que le code de l’application n’a pas de chemin explicite pour gérer le miss. Le comportement est identique au cache-aside ; seule l’emplacement de la logique change.
Write-through
Le write-through met à jour le cache et la source lors d’une seule et même opération. Les lectures sont toujours rapides (hot) et ne voient jamais de données plus anciennes que la dernière écriture réussie.
async function updateUser(id: string, patch: Partial<User>) {
const user = await db.user.update({ where: { id }, data: patch });
await redis.set(`user:${id}`, JSON.stringify(user), "EX", 600);
return user;
}
Cela augmente la latence d’écriture et met en cache des données qui ne seront peut-être jamais lues, mais cela maintient le cache cohérent avec le flux d’écriture, ce qui est précisément ce dont ont besoin les mises à jour visibles par l’utilisateur.
Write-behind (write-back)
Le write-behind écrit immédiatement dans le cache et synchronise les données vers la source de manière asynchrone. Les écritures sont très rapides et peuvent être regroupées (batching), ce qui est précieux pour les compteurs et la télémétrie. Le risque est cependant important : si le cache tombe avant la synchronisation, l’écriture est perdue. Utilisez-le uniquement lorsque la perte des dernières écritures est acceptable, et jamais pour des transactions financières ou des registres officiels.
TTL et fraîcheur des données
Chaque valeur mise en cache doit avoir un time to live (TTL). Le TTL est un budget de péremption : c’est le temps maximum pendant lequel un utilisateur peut voir une valeur qui n’est plus à jour à la source. Le choix de ce délai est autant une décision produit qu’une décision technique.
- Prix et stocks : quelques secondes. Un prix erroné se transforme rapidement en ticket de support.
- Profils, flux et listes : quelques minutes. Un léger décalage est invisible pour l’utilisateur.
- Données de référence, feature flags et configuration : de quelques minutes à quelques heures.
- Assets statiques et contenus changeant rarement : plusieurs jours, avec un versionnage par nom de fichier.
Ajoutez du jitter (variation aléatoire) à votre TTL. Si mille clés sont écrites au même moment avec le même TTL, elles expireront simultanément, provoquant un pic de charge. Randomiser ce délai de quelques pourcents permet d’étaler les rechargements.
const ttl = 600 + Math.floor(Math.random() * 60);
await redis.set(key, JSON.stringify(value), "EX", ttl);
Même en cas d’invalidation explicite, conservez un TTL comme filet de sécurité. Un bug qui oublierait de supprimer une clé ne devrait vous coûter que quelques minutes de données obsolètes, et non une erreur permanente.
L’invalidation : la partie difficile
Il y a une raison pour laquelle l’invalidation du cache est la chute de la plus vieille blague de l’informatique. Stocker une valeur est trivial, mais il est réellement difficile de la supprimer au bon moment, à chaque couche, et sans conditions de concurrence (race conditions).
Trois techniques couvrent presque tous les cas de figure.
Le versionnage des clés. Au lieu de supprimer, changez la clé. Préfixez vos clés avec une version que vous incrémentez lorsque les données sous-jacentes changent ; ainsi, les anciennes entrées deviennent inaccessibles et expirent d’elles-mêmes.
const version = (await redis.get(`user:${id}:v`)) ?? "1";
const key = `user:${id}:v${version}`;
Cette méthode évite les race conditions et fonctionne entre plusieurs instances, c’est pourquoi c’est l’approche privilégiée pour tout ce qui est partagé.
L’invalidation explicite (explicit bust). Supprimez la clé lors de l’écriture. C’est l’approche la plus évidente et la plus facile à rater, car chaque chemin d’écriture doit impérativement s’en souvenir.
await db.user.update({ where: { id }, data: patch });
await redis.del(`user:${id}`);
L’invalidation pilotée par les événements. Publiez un événement de modification et laissez chaque instance et chaque couche réagir. C’est ainsi qu’un purge de CDN ou un cache multi-services reste cohérent, et cela s’adapte aux systèmes que vous ne contrôlez pas.
Quel que soit votre choix, soyez cohérent sur l’ordre des opérations, et préférez la suppression à l’écrasement lors d’une mise à jour. La suppression est idempotente ; l’écrasement peut ressusciter une valeur qui a déjà été remplacée par un autre écrivain concurrent.
Cache stampede et thundering herd
Lorsqu’une clé populaire expire, toutes les requêtes en cours subissent un “miss” au même instant. Si mille requêtes arrivent par seconde et que la clé prend 200 ms à être reconstruite, des centaines de requêtes identiques frappent la base de données simultanément. C’est ce qu’on appelle un cache stampede, ou thundering herd, et cela peut mettre à terre un service qui était pourtant stable un instant auparavant.
Voici quatre solutions d’atténuation, par ordre de préférence approximatif :
- Stale-while-revalidate. Servez immédiatement la valeur obsolète (stale) et rafraîchissez-la en arrière-plan. Les lecteurs n’attendent jamais et la source ne reçoit qu’un seul rafraîchissement. HTTP possède un header dédié précisément à cela.
- Locking ou single-flight. Seul le premier appelant recalcule la valeur ; tous les autres attendent brièvement ou utilisent la donnée obsolète. Redis
SET key value NX EXest un verrou distribué simple. - TTLs avec jitter. Étalez les dates d’expiration pour éviter que toutes les clés n’expirent en même temps.
- Expiration précoce probabiliste. Chaque lecture a une faible probabilité, croissante, de déclencher un rafraîchissement avant que la clé n’expire réellement, répartissant ainsi la charge sur plusieurs requêtes.
Le verrou doit toujours avoir une expiration, sinon un worker qui plante laissera la clé verrouillée indéfiniment.
Quoi mettre en cache et quoi ne jamais mettre en cache
Mettez en cache les données dont la production est coûteuse, qui sont lues beaucoup plus souvent qu’elles ne changent, qui tolèrent une légère obsolescence et qui sont partagées entre de nombreux lecteurs. Les listes de produits, les profils publics, la configuration, les fragments rendus et les agrégats coûteux sont tous d’excellents candidats.
Ne mettez jamais en cache :
- Les décisions d’autorisation dans un cache partagé. Un changement de rôle ou une déconnexion doit prendre effet immédiatement, et un CDN ne doit jamais servir la réponse privée d’un utilisateur à un autre.
- Les données sensibles par utilisateur sous une clé partagée. Chaque valeur spécifique à un utilisateur nécessite une clé incluant l’utilisateur, ainsi qu’une directive de cache
privatesi elle atteint un edge. - Les secrets et les identifiants. Un cache est un endroit supplémentaire d’où ils peuvent fuiter.
- Les données que vous ne pouvez pas invalider. Si une valeur n’a ni clé naturelle ni événement associé, le cache finira par servir une donnée erronée sans moyen de la corriger.
Un test utile : si vous ne pouvez pas décrire comment une valeur quitte le cache, ne l’y placez pas.
Mise en cache HTTP à l’edge
Le cache le moins coûteux est celui qui n’atteint jamais votre serveur. HTTP vous offre un contrôle précis à ce sujet.
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Vary: Accept-Encoding
max-age définit la durée pendant laquelle le navigateur peut réutiliser la réponse. s-maxage outrepasse ce réglage pour les caches partagés tels qu’un CDN. stale-while-revalidate permet à l’edge de servir une copie obsolète pendant qu’il récupère une version fraîche en arrière-plan, ce qui évite les effets de stampede pour le contenu public. private et no-store empêchent totalement les réponses sensibles d’entrer dans les caches partagés.
Un ETag permet les requêtes conditionnelles. Le client envoie If-None-Match, et si le tag correspond toujours, vous répondez 304 Not Modified sans corps de message, économisant ainsi la bande passante tout en garantissant la fraîcheur des données.
app.get("/posts", async (req, res) => {
const body = JSON.stringify(await listPosts());
const tag = `"${createHash("sha256").update(body).digest("hex")}"`;
res.set("Cache-Control", "public, max-age=30, stale-while-revalidate=60");
res.set("ETag", tag);
if (req.headers["if-none-match"] === tag) return res.status(304).end();
res.type("application/json").send(body);
});
Vary est facile à oublier mais important : si une réponse dépend de Accept-Encoding ou Accept-Language, précisez-le, sinon un cache pourrait envoyer la mauvaise variante au mauvais client.
In-memory vs Redis vs CDN
Ces trois couches de partage ne sont pas concurrentes ; elles forment une hiérarchie.
Un cache in-process offre la recherche la plus rapide possible car elle ne traverse jamais le réseau. C’est la solution idéale pour des données volumétriques faibles, très sollicitées et principalement en lecture, comme une configuration ou une table de permissions. Ses limites sont les suivantes : chaque instance possède sa propre copie (ce qui multiplie la consommation mémoire et peut entraîner des divergences de valeurs) et le cache disparaît lors d’un déploiement.
Redis est le cache partagé de l’application. Une seule copie logique sert toutes les instances, elle survit aux redémarrages et supporte les TTL, les opérations atomiques ainsi que des structures de données allant au-delà des simples chaînes de caractères. Son coût est un aller-retour réseau, généralement bien inférieur à une milliseconde sur un même réseau.
Un CDN est la couche la plus externe. Il met en cache les réponses publiques au plus près des utilisateurs à travers le monde et peut absorber un trafic énorme avant qu’il n’atteigne vos serveurs. Il ne fonctionne que pour les réponses identiques pour de nombreux utilisateurs, et son vidage (purge) est une opération délibérée et parfois lente.
Une architecture de production courante consiste à placer un minuscule cache in-process devant Redis pour les clés les plus sollicitées, avec un CDN devant l’ensemble de l’API pour les requêtes GET publiques.
Mise en cache négative
On décrit généralement les caches comme des systèmes stockant des valeurs, mais stocker l’ absence d’une valeur est tout aussi utile. Si une requête pour un enregistrement manquant est coûteuse et répétée, mettez en cache cet échec (le “miss”).
const hit = await redis.get(key);
if (hit === "__miss__") return null;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
await redis.set(key, "__miss__", "EX", 60);
return null;
}
Gardez des TTL négatifs courts, car un enregistrement qui n’existait pas il y a une minute peut exister maintenant. Ce modèle permet de se protéger contre la cache penetration, où un flux de requêtes pour des clés inexistantes contourne le cache et sollicite la base de données à chaque fois. Pour empêcher un attaquant de générer une infinité de clés manquantes uniques, validez les entrées et mettez en place un rate limiting avant le cache.
Cohérence et les deux défis majeurs
Un cache rend un système éventuellement cohérent (eventually consistent) : pendant un court laps de temps, différents lecteurs peuvent voir des valeurs différentes. C’est généralement acceptable, mais pas dans toutes les situations.
- Read-your-writes. Un utilisateur qui vient de mettre à jour son profil s’attend à voir le changement. Invalidez le cache lors de l’écriture, ou lisez depuis la source pendant une courte période après une écriture effectuée par le même utilisateur.
- Replication lag. Si les lectures sont dirigées vers une réplique, un cache alimenté par cette réplique peut être en retard par rapport au primaire. Invalidez le cache depuis le chemin d’écriture du primaire.
- Caches multi-régions. Une purge dans une région n’atteint pas instantanément les autres. Utilisez des clés versionnées ou acceptez le délai de propagation.
Pour être honnête, la mise en cache consiste à sacrifier la cohérence au profit de la vitesse. La seule façon de le faire en toute sécurité est de décider explicitement quel niveau d’obsolescence est acceptable et d’intégrer l’invalidation directement dans le chemin d’écriture, plutôt que de l’ajouter après coup.
Les clés de cache sont une API
Une clé de cache ressemble à un détail d’implémentation, mais elle se comporte comme une interface publique. C’est l’élément sur lequel chaque lecteur et écrivain doit s’accorder, et elle est visible dans Redis, dans les slow logs et sur les tableaux de bord. Concevez vos clés de manière délibérée.
Trois règles permettent de garder le contrôle :
- Utilisez des namespaces par version et environnement.
v1:user:42vous permet de modifier la structure plus tard et évite que l’environnement de staging ne partage des clés avec la production. - Soyez déterministe. La même recherche logique doit toujours produire la même chaîne de caractères. Normalisez la casse, nettoyez les entrées (trim) et n’incluez jamais de valeur qui change à chaque appel.
- Ne placez jamais de secrets ou de données personnelles dans une clé. Les clés sont journalisées, exportées et visibles par les opérateurs.
export const cacheKeys = {
user: (id: string) => `v1:user:${id}`,
userPosts: (id: string, cursor: string) => `v1:user:${id}:posts:${cursor}`,
productBySku: (sku: string) => `v1:product:sku:${sku.trim().toLowerCase()}`,
};
Centraliser la construction des clés dans un seul module est ce qui rend l’invalidation possible. Lorsqu’une écriture doit supprimer une clé, elle appelle la même fonction que celle utilisée pour la lecture ; lorsque la structure change, il n’y a qu’un seul endroit où incrémenter la version.
Politiques d’éviction
Un cache autorisé à croître sans limite n’est rien d’autre qu’une fuite de mémoire avec des étapes supplémentaires. Redis comme les caches in-process ont besoin d’un plafond et d’une politique pour déterminer quoi supprimer une fois ce plafond atteint.
Redis propose maxmemory et maxmemory-policy. Le choix courant pour un cache pur est allkeys-lru, qui évince la clé la moins récemment utilisée, ou allkeys-lfu, qui privilégie les clés fréquemment utilisées lorsque les schémas d’accès sont asymétriques.
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
N’utilisez jamais noeviction pour un cache : une fois la mémoire pleine, les écritures échouent et votre application commence à lever des erreurs au lieu de simplement subir un cache miss. Pour les caches in-process, utilisez une bibliothèque LRU bornée plutôt qu’un simple Map, et définissez à la fois un nombre maximum d’entrées et une taille maximale.
Les évictions ne sont pas des erreurs, mais une augmentation du nombre d’évictions est un signal. Cela signifie que le working set ne tient plus en mémoire et que le hit rate est sur le point de chuter. Soit vous allouez plus de mémoire au cache, soit vous réduisez la quantité de données mises en cache.
Préchauffage du cache (Cache warming)
Un cache est au plus froid au moment où il est le plus vulnérable : juste après un déploiement, un redémarrage ou une extension d’échelle (scale-out). Chaque instance démarre vide, le trafic arrive à plein volume, et la source subit la charge totale jusqu’à ce que le cache se remplisse. C’est un effet de stampede causé par votre propre déploiement.
Deux approches fonctionnent. Le préchauffage paresseux (lazy warming) accepte cette fenêtre de froid et s’appuie sur le verrouillage (locking) et le stale-while-revalidate pour y survivre. C’est simple et ne nécessite aucun mécanisme supplémentaire. Le préchauffage proactif (proactive warming) exécute une tâche qui charge les clés connues comme étant “chaudes” avant que la nouvelle version ne reçoive le trafic.
async function warmCache() {
const popular = await db.product.findMany({
orderBy: { views: "desc" },
take: 500,
select: { id: true },
});
for (const { id } of popular) {
await cached(`product:${id}`, 300, () =>
db.product.findUniqueOrThrow({ where: { id } }),
);
}
}
Ne préchauffez que ce que vous savez être fréquemment utilisé. Vouloir tout préchauffer revient simplement à créer une copie lente et coûteuse de la base de données, dont la majeure partie sera évincée sans jamais avoir été utilisée.
Observabilité des caches
Un cache que vous ne mesurez pas est un cache auquel vous ne pouvez pas faire confiance. Quatre indicateurs suffisent à faire le tour de la question.
- Hit rate (taux de succès) — les lectures sont-elles réellement servies par le cache ?
- Latency (latence) — un hit est-il significativement moins coûteux que la source, en incluant le saut réseau ?
- Evictions — l’ensemble de travail (working set) dépasse-t-il la mémoire disponible ?
- Erreurs et timeouts — le cache devient-il une source de pannes ?
redis-cli INFO stats | grep -E 'keyspace_(hits|misses)|evicted_keys'
redis-cli INFO memory | grep used_memory_human
Émettez également un compteur pour les hits et les misses depuis l’application, tagué par nom de cache. C’est ce qui vous permet de corréler une chute du hit-rate avec un déploiement, et c’est la première chose à vérifier lorsque la charge de la base de données augmente sans raison apparente.
Dégradation progressive en cas de panne du cache
Si la perte du cache entraîne l’arrêt de l’API, le cache est devenu un point de défaillance unique (single point of failure) pour un système dont l’objectif même est d’être optionnel. La source de vérité existe toujours ; l’application doit être capable de l’atteindre.
Encapsulez chaque opération de cache afin qu’un échec soit traité comme un miss plutôt que comme une erreur, maintenez des timeouts courts, et envisagez l’utilisation d’un circuit breaker pour arrêter d’appeler un cache défaillant pendant une période de refroidissement.
async function safeGet(key: string) {
try {
return await redis.get(key);
} catch (err) {
logger.warn({ err, key }, "cache_read_failed");
return null;
}
}
La même logique s’applique aux écritures dans le cache : un set échoué ne doit jamais faire échouer la requête. La seule exception est le write-behind, où le cache fait partie du chemin d’écriture ; c’est une raison supplémentaire pour le réserver aux données que vous pouvez vous permettre de perdre.
Mise en cache des requêtes et des agrégats coûteux
Tout ce qui mérite d’être mis en cache n’est pas forcément une ligne unique. Les jointures coûteuses, les agrégats de tableaux de bord et les résultats de recherche sont souvent les meilleurs candidats, car ils sont lents à calculer et évoluent peu fréquemment.
Une vue matérialisée est un cache qui réside dans la base de données : elle stocke le résultat d’une requête et est rafraîchie selon un planning ou à la demande. Elle vous offre la fraîcheur d’un traitement batch et la vitesse de lecture d’une table.
CREATE MATERIALIZED VIEW daily_revenue AS
SELECT date_trunc('day', created_at) AS day,
sum(total_cents) AS revenue_cents
FROM orders
WHERE status = 'paid'
GROUP BY 1;
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue;
Pour les résultats trop dynamiques pour une vue, mettez en cache la réponse sérialisée dans Redis sous une clé incluant chaque entrée qui l’influence — filtres, ordre de tri et page. Deux requêtes pour des pages différentes correspondent à des valeurs différentes, elles doivent donc avoir des clés distinctes.
Tester un cache
Les bugs de cache sont le genre de problèmes qui passent les tests mais échouent en production. Testez donc explicitement les comportements critiques : le chemin en cas de miss, le chemin en cas de hit, l’invalidation et les pannes.
test("loads once and serves the second read from cache", async () => {
let calls = 0;
const load = async () => {
calls += 1;
return { id: "1" };
};
await cached("user:1", 60, load);
await cached("user:1", 60, load);
expect(calls).toBe(1);
});
test("falls back to the source when the cache is unavailable", async () => {
redis.get = async () => {
throw new Error("connection_refused");
};
await expect(cached("user:1", 60, load)).resolves.toEqual({ id: "1" });
});
Utilisez un vrai Redis dans un container pour vos tests d’intégration, ou un faux en mémoire qui implémente get, set et del. Testez également les cas négatifs : une mise à jour qui devrait invalider le cache, un TTL qui devrait expirer, et une panne du cache qui devrait entraîner une dégradation du service plutôt qu’un crash complet.
Bonnes pratiques
- Ne mettez en cache qu’après avoir mesuré ; ajoutez un cache pour une lecture lente et répétée, et non par défaut.
- Attribuez un TTL à chaque clé, même lorsque vous effectuez également une invalidation explicite.
- Utilisez des clés déterministes et namespacées telles que
user:42:profile, et versionnez-les pour les données partagées. - Préférez la suppression ou le versionnage des clés plutôt que leur écrasement lors d’une mise à jour.
- Ajoutez du jitter aux TTL et utilisez le stale-while-revalidate pour survivre aux stampedes.
- Gardez les données par utilisateur et d’autorisation hors des caches partagés ; marquez les réponses
private. - Traitez le cache comme optionnel dans le code afin qu’une panne entraîne une dégradation du service plutôt qu’un échec complet.
- Surveillez le hit rate et le nombre d’evictions, pas seulement la latence.
- Mettez en cache les misses ainsi que les hits lorsque la recherche de données absentes est coûteuse.
Erreurs courantes
- Mettre en cache sans TTL et compter sur une purge manuelle qui n’a jamais lieu.
- Partager une seule clé entre plusieurs utilisateurs et ainsi divulguer les données d’une personne à une autre.
- Invalider le cache dans certains flux d’écriture, mais pas dans d’autres.
- Définir le même TTL pour toutes les clés, provoquant leur expiration simultanée.
- Mettre en cache une décision d’autorisation qui aurait dû être révoquée suite à un changement de rôle.
- Utiliser un cache comme base de données et perdre des écritures lors d’un redémarrage.
- Oublier
Varyet servir un encodage de contenu erroné depuis un CDN. - Ne rien mesurer, faisant ainsi chuter le taux de hit de manière invisible après un changement de clé.
- Mettre en cache un élément unique par requête, ce qui garantit un taux de hit de 0 %.
Et après ?
Le caching est un outil pour contrôler la charge ; ses compagnons naturels sont les autres mécanismes qui limitent le travail effectué. Redis approfondit le fonctionnement du store sur lequel la plupart des caches sont bâtis, et la Pagination est la version au niveau de la requête de cette même idée : ne jamais effectuer plus de travail que nécessaire. Si la base de données derrière le cache est le goulot d’étranglement, le Connection Pooling réduit le coût de chaque connexion, et PostgreSQL couvre la source de vérité que vous protégez.