Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Redis Integration - szybkie magazyny tributów

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, @name.

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.

Przejdź do CodeWorlds