Kurs NestJS · Moduł 8: Cache i wydajność

Redis Integration - szybkie magazyny tributów

5 min czytania
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:

  1. Zainstaluj pakiety: cache-manager-redis-store.
  2. Skonfiguruj CacheModule z Redis store.
  3. Wstrzyknij CACHE_MANAGER w serwisie.
  4. Używaj cache.get() i cache.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ć:

  1. Aplikacja wysyła zapis.
  2. Cache zapisuje dane.
  3. Cache synchronicznie zapisuje do bazy.
  4. 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 → skonfiguruj CacheModule z Redis store → wstrzyknij CACHE_MANAGER → używaj cache.get() i cache.set(),
  • pole store zamienia domyślny cache w pamięci na Redisa; serwisy nie zmieniają się wcale,
  • ttl to czas życia wpisu w sekundach, a trzeci argument set nadpisuje 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)
96

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Redis w NestJS można użyć do:

  2. 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:

Przydatne artykuły