Kurs NestJS · Moduł 8: Cache i wydajność
Redis Integration - szybkie magazyny tributów
W tej lekcji5
Cache w pamięci procesu poznałeś w poprzedniej lekcji i działa doskonale - dopóki masz jeden serwer. Uruchom drugą instancję aplikacji za load balancerem, a każda zbuduje własny cache. Użytkownik trafia raz na jedną, raz na drugą i dostaje różne dane. Gorzej: unieważnienie wpisu na pierwszej instancji nie usunie go z drugiej.
Legion nie trzymał zapasów w plecaku każdego żołnierza. Miał wspólny magazyn, do którego wszyscy sięgali. Redis jest takim magazynem - i to jest jego główna zaleta nad cache w pamięci: współdzielenie między wieloma instancjami aplikacji.
Nie chodzi o to, że jest zawsze szybszy - cache w pamięci procesu bywa szybszy, bo nie wymaga przejścia przez sieć. Chodzi o to, że wszystkie instancje widzą to samo.
Cztery zastosowania
Redis w NestJS przydaje się do czterech rzeczy naraz: cache, sessions, pub/sub i queue.
Cache to zastosowanie, od którego zwykle się zaczyna. Sessions - sesje użytkowników, które poznałeś przy uwierzytelnianiu; dzięki wspólnemu magazynowi zalogowanie na jednej instancji obowiązuje na wszystkich. Pub/sub - rozgłaszanie zdarzeń między serwisami. Queue - kolejki zadań, do których wracaliśmy przy posłańcach Imperium.
To dlatego Redis pojawia się niemal w każdym stosie: jedna usługa pokrywa cztery potrzeby, które inaczej wymagałyby czterech osobnych narzędzi.
Cztery kroki konfiguracji
Podłączenie Redisa jako magazynu cache przebiega zawsze tak samo:
- Zainstaluj pakiety:
cache-manager-redis-store. - Skonfiguruj
CacheModulez Redis store. - Wstrzyknij
CACHE_MANAGERw serwisie. - Używaj
cache.get()icache.set()z kluczami.
Krok drugi wygląda tak:
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 {}Zwróć uwagę na jedno pole: store. To ono zamienia domyślny cache w pamięci na Redisa - reszta konfiguracji opisuje już tylko, gdzie ten Redis stoi. Cała aplikacja korzysta z tego samego interfejsu; podmiana magazynu nie dotyka ani jednej linii w serwisach.
ttl: 300 to domyślny czas życia wpisu w sekundach - pięć minut. Po tym czasie Redis usuwa go sam.
Użycie w serwisie
Kroki trzeci i czwarty to już zwykły kod:
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) to wstrzyknięcie po tokenie - ten sam mechanizm, który poznałeś przy providerach bez typu klasy.
Sam schemat nazywa się cache-aside: najpierw pytamy cache, przy trafieniu zwracamy od razu, przy pudle idziemy do bazy i dopiero potem zapisujemy wynik. Trzeci argument set nadpisuje domyślny TTL dla tego jednego wpisu.
Klucz warto budować z prefiksem, jak legion:42. Bez tego prędzej czy później dwa różne byty zaczną używać tej samej liczby jako klucza, a błąd wyjdzie w najgorszym możliwym momencie.
Write-Through - inna strategia
Cache-aside zapisuje do cache dopiero po odczycie z bazy. Istnieje odwrotne podejście, write-through, w którym cache jest zapisywany przy każdym zapisie danych, a nie przy odczycie. Kolejność jest tu ustalona i warto ją znać:
- Aplikacja wysyła zapis.
- Cache zapisuje dane.
- Cache synchronicznie zapisuje do bazy.
- Potwierdzenie zwrócone do aplikacji.
Kluczowe jest słowo synchronicznie w kroku trzecim: aplikacja czeka, aż dane trafią i do cache, i do bazy. Dzięki temu cache nigdy nie jest nieaktualny - ale zapis trwa dłużej niż sam zapis do bazy.
Wybór między strategiami sprowadza się do tego, czego bardziej potrzebujesz. Cache-aside jest tańszy przy zapisach i wystarcza, gdy dane rzadko się zmieniają albo chwilowa nieaktualność nie szkodzi. Write-through kosztuje przy każdym zapisie, ale gwarantuje spójność - i to on ma sens tam, gdzie stary odczyt oznacza realny problem, jak przy saldzie czy stanie magazynu.
Podsumowanie
Wspólny magazyn stoi, wszystkie instancje sięgają do tego samego:
- główną zaletą Redisa nad cache w pamięci jest współdzielenie cache między wieloma instancjami aplikacji - nie to, że jest zawsze szybszy,
- Redis w NestJS służy do czterech rzeczy: cache, sessions, pub/sub i queue,
- konfiguracja w czterech krokach: zainstaluj
cache-manager-redis-store→ skonfigurujCacheModulez Redis store → wstrzyknijCACHE_MANAGER→ używajcache.get()icache.set(), - pole
storezamienia domyślny cache w pamięci na Redisa; serwisy nie zmieniają się wcale, ttlto czas życia wpisu w sekundach, a trzeci argumentsetnadpisuje go dla jednego klucza,- cache-aside: zapytaj cache, przy pudle idź do bazy i dopiero potem zapisz wynik,
- klucze buduj z prefiksem (
legion:42), żeby dwa byty nie zaczęły używać tej samej liczby, - write-through w kolejności: aplikacja wysyła zapis → cache zapisuje dane → cache synchronicznie zapisuje do bazy → potwierdzenie do aplikacji,
- cache-aside jest tańszy przy zapisach, write-through gwarantuje spójność - wybieraj według tego, czy stary odczyt może zaszkodzić.
W następnej lekcji zajmiemy się optymalizacją samej aplikacji - tym, co przyspiesza ją zanim jeszcze pomyślisz o cache. A na razie zapamiętaj: cache w pamięci należy do procesu, Redis należy do wszystkich - i dopiero to sprawia, że druga instancja widzi to samo co pierwsza.
Kod do tej lekcji: src/redis-integration.ts
1// Redis Integration - Szybkie Magazyny Tributyw
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. Konfiguracja Redis jako store 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. Konfiguracja async z 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. Zaawansowany serwis Redis
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); // Zapisz do bazy
63 await this.cache.set(key, data, ttl * 1000); // Zapisz do cache
64 }
65
66 // Cache Warming - rozgrzewanie 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 minut
74 });
75 await Promise.all(promises);
76 console.log(`Cache rozgrzany: ${keys.length} kluczy`);
77 }
78
79 // Invalidacja po wzorcu
80 async invalidateByPrefix(prefix: string) {
81 // W Redis: KEYS prefix:* -> DEL
82 console.log(`Invalidacja kluczy: ${prefix}:*`);
83 // W rzeczywistej implementacji z Redis:
84 // const keys = await redis.keys(`${prefix}:*`);
85 // await Promise.all(keys.map(k => this.cache.del(k)));
86 }
87}
88
89// Redis wspiera dodatkowe struktury:
90// - Strings (cache)
91// - Lists (kolejki)
92// - Sets (unikalne kolekcje)
93// - Sorted Sets (rankingi)
94// - Hashes (obiekty)
95// - Pub/Sub (komunikacja)
96Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Redis w NestJS można użyć do:
2. Główna zaleta Redis nad in-memory cache to:
Zadania praktyczne w grze
- Edytor kodu
Podłącz Redis jako backend cache w CacheModule
- Układanie w pionie
Uporządkuj kroki konfiguracji Redis cache w NestJS:
- Układanie w pionie
Ułóż kroki strategii Write-Through cache: