Cours NestJS · Module 8: Cache et performances
Redis Integration - les entrepôts rapides à tributs
Dans cette leçon5
Tu as rencontré le cache en mémoire du processus dans la leçon précédente et il fonctionne à merveille - tant que tu n'as qu'un serveur. Lance une seconde instance derrière un répartiteur de charge et chacune bâtira son propre cache. Un utilisateur tombe tantôt sur l'une, tantôt sur l'autre, et reçoit des données différentes. Pire : invalider une entrée sur la première instance ne la supprime pas de la seconde.
Une légion ne gardait pas ses vivres dans le sac de chaque soldat. Elle avait un entrepôt commun où tous puisaient. Redis est un tel entrepôt - et c'est là son principal avantage sur un cache en mémoire : le partage du cache entre plusieurs instances de l'application.
Il ne s'agit pas de dire qu'il est toujours plus rapide - un cache en mémoire de processus l'est souvent, car il évite un aller-retour réseau. Il s'agit de ce que toutes les instances voient la même chose.
Quatre usages
Redis dans NestJS sert à quatre choses à la fois : cache, sessions, pub/sub et queue.
Le cache est l'usage par lequel on commence d'ordinaire. Les sessions - celles des utilisateurs, rencontrées à l'authentification ; grâce à un magasin commun, une connexion sur une instance vaut sur toutes. Le pub/sub - la diffusion d'événements entre services. La queue - les files de tâches auxquelles nous sommes revenus avec les messagers de l'Empire.
C'est pourquoi Redis figure dans presque toutes les piles : un seul service couvre quatre besoins qui exigeraient autrement quatre outils distincts.
Quatre étapes de configuration
Brancher Redis comme magasin de cache se déroule toujours de la même façon :
- Installe les paquets :
cache-manager-redis-store. - Configure
CacheModuleavec le store Redis. - Injecte
CACHE_MANAGERdans le service. - Utilise
cache.get()etcache.set()avec des clés.
La deuxième étape ressemble à ceci :
1@Module({
2 imports: [
3 CacheModule.registerAsync({
4 isGlobal: true,
5 useFactory: (config: ConfigService) => ({
6 store: redisStore,
7 host: config.get('REDIS_HOST'),
8 port: config.get('REDIS_PORT'),
9 ttl: 300,
10 }),
11 inject: [ConfigService],
12 }),
13 ],
14})
15export class AppModule {}Note un champ : store. C'est lui qui transforme le cache en mémoire par défaut en Redis - le reste de la configuration ne fait que dire où se trouve ce Redis. Toute l'application utilise la même interface ; changer de magasin ne touche pas une ligne dans les services.
ttl: 300 est la durée de vie par défaut d'une entrée en secondes - cinq minutes. Passé ce délai, Redis la supprime de lui-même.
L'usage dans un service
Les étapes trois et quatre sont du code ordinaire :
1@Injectable()
2export class LegionService {
3 constructor(
4 @Inject(CACHE_MANAGER) private cache: Cache,
5 private repo: LegionRepository,
6 ) {}
7
8 async findOne(id: number) {
9 const key = `legion:${id}`;
10
11 const cached = await this.cache.get<Legion>(key);
12 if (cached) {
13 return cached;
14 }
15
16 const legion = await this.repo.findOne(id);
17 await this.cache.set(key, legion, 600);
18
19 return legion;
20 }
21}@Inject(CACHE_MANAGER) est une injection par token - le mécanisme rencontré avec les providers dépourvus de type de classe.
Le schéma lui-même s'appelle cache-aside : on interroge d'abord le cache, on renvoie aussitôt en cas de succès, et en cas d'échec on va en base et seulement ensuite on enregistre le résultat. Le troisième argument de set remplace le TTL par défaut pour cette seule entrée.
Il vaut la peine de bâtir les clés avec un préfixe, comme legion:42. Sans lui, tôt ou tard deux entités différentes se mettront à utiliser le même nombre comme clé, et le bug surgira au pire moment.
Write-through - l'autre stratégie
Le cache-aside n'écrit dans le cache qu'après une lecture en base. Il existe l'approche inverse, le write-through, où le cache est écrit à chaque écriture de données et non à la lecture. L'ordre y est fixé et mérite d'être connu :
- L'application envoie une écriture.
- Le cache enregistre les données.
- Le cache écrit de façon synchrone en base.
- Une confirmation est renvoyée à l'application.
Le mot clé est synchrone à la troisième étape : l'application attend que les données atteignent à la fois le cache et la base. Le cache n'est ainsi jamais périmé - mais une écriture dure plus longtemps qu'une simple écriture en base.
Le choix entre les stratégies dépend de ce dont tu as le plus besoin. Le cache-aside coûte moins cher à l'écriture et suffit quand les données changent rarement ou qu'une péremption brève ne nuit pas. Le write-through coûte à chaque écriture mais garantit la cohérence - et il se justifie là où une lecture périmée pose un vrai problème, comme pour un solde ou un état de stock.
Récapitulatif
L'entrepôt commun est en place et toutes les instances y puisent :
- le principal avantage de Redis sur un cache en mémoire est le partage du cache entre plusieurs instances - non le fait qu'il soit toujours plus rapide,
- Redis dans NestJS sert à quatre choses : cache, sessions, pub/sub et queue,
- configuration en quatre étapes : installer
cache-manager-redis-store→ configurerCacheModuleavec le store Redis → injecterCACHE_MANAGER→ utilisercache.get()etcache.set(), - le champ
storetransforme le cache en mémoire par défaut en Redis ; les services ne changent pas du tout, ttlest la durée de vie d'une entrée en secondes, et le troisième argument desetla remplace pour une clé,- cache-aside : interroge le cache, en cas d'échec va en base et enregistre seulement ensuite le résultat,
- bâtis les clés avec un préfixe (
legion:42) pour que deux entités n'utilisent jamais le même nombre, - write-through dans l'ordre : l'application envoie une écriture → le cache enregistre les données → le cache écrit de façon synchrone en base → une confirmation revient à l'application,
- le cache-aside coûte moins à l'écriture, le write-through garantit la cohérence - choisis selon qu'une lecture périmée peut nuire ou non.
Dans la prochaine leçon, nous nous occuperons de l'optimisation de l'application elle-même - ce qui l'accélère avant même que tu ne penses au cache. En attendant, retiens ceci : un cache en mémoire appartient à un processus, Redis appartient à tous - et c'est cela seul qui fait que la seconde instance voit ce que voit la première.
Code de cette leçon : src/redis-integration.ts
1// Redis Integration - Entrepôts Rapides de Tributs
2import { Injectable, Inject, Module } from '@nestjs/common';
3import { CACHE_MANAGER } from '@nestjs/cache-manager';
4import { CacheModule } from '@nestjs/cache-manager';
5import { Cache } from 'cache-manager';
6
7// 1. Configuration de Redis en tant que store de cache
8// npm install cache-manager-redis-store redis
9@Module({
10 imports: [
11 CacheModule.register({
12 // store: redisStore,
13 // host: process.env.REDIS_HOST || 'localhost',
14 // port: parseInt(process.env.REDIS_PORT) || 6379,
15 // password: process.env.REDIS_PASSWORD,
16 ttl: 300,
17 max: 1000,
18 isGlobal: true,
19 }),
20 ],
21})
22export class RedisCacheModule {}
23
24// 2. Configuration asynchrone avec ConfigService
25// CacheModule.registerAsync({
26// imports: [ConfigModule],
27// inject: [ConfigService],
28// useFactory: (config: ConfigService) => ({
29// store: redisStore,
30// host: config.get('REDIS_HOST'),
31// port: config.get('REDIS_PORT'),
32// ttl: config.get('CACHE_TTL'),
33// }),
34// })
35
36// 3. Advanced Redis service
37@Injectable()
38export class RedisCacheService {
39 constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
40
41 // Cache-Aside Pattern
42 async getOrSet<T>(
43 key: string,
44 fetcher: () => Promise<T>,
45 ttl: number = 300
46 ): Promise<T> {
47 let data = await this.cache.get<T>(key);
48 if (data) return data;
49
50 data = await fetcher();
51 await this.cache.set(key, data, ttl * 1000);
52 return data;
53 }
54
55 // Write-Through Pattern
56 async writeThrough<T>(
57 key: string,
58 data: T,
59 saveFn: (data: T) => Promise<void>,
60 ttl: number = 300
61 ): Promise<void> {
62 await saveFn(data); // Save to database
63 await this.cache.set(key, data, ttl * 1000); // Save to cache
64 }
65
66 // Cache Warming - préchauffage du cache
67 async warmup(
68 keys: string[],
69 fetcher: (key: string) => Promise<any>
70 ) {
71 const promises = keys.map(async (key) => {
72 const data = await fetcher(key);
73 await this.cache.set(key, data, 600000); // 10 minutes
74 });
75 await Promise.all(promises);
76 console.log(`Cache préchauffé : ${keys.length} clés`);
77 }
78
79 // Invalidation par motif
80 async invalidateByPrefix(prefix: string) {
81 // Dans Redis : KEYS prefix:* -> DEL
82 console.log(`Invalidation des clés : ${prefix}:*`);
83 // Dans une implémentation réelle avec Redis :
84 // const keys = await redis.keys(`${prefix}:*`);
85 // await Promise.all(keys.map(k => this.cache.del(k)));
86 }
87}
88
89// Redis prend en charge des structures supplémentaires :
90// - Strings (cache)
91// - Lists (files d'attente)
92// - Sets (collections uniques)
93// - Sorted Sets (classements)
94// - Hashes (objets)
95// - Pub/Sub (communication)
96Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Redis dans NestJS peut être utilisé pour :
2. Le principal avantage de Redis par rapport au cache in-memory est :
Tâches pratiques dans le jeu
- Éditeur de code
Connecte Redis comme backend de cache dans CacheModule
- Tri vertical
Ordonne les étapes de configuration du cache Redis dans NestJS :
- Tri vertical
Ordonne les étapes de la stratégie de cache Write-Through :