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.
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.
Podłączenie Redisa jako magazynu cache przebiega zawsze tak samo:
cache-manager-redis-store.CacheModule z Redis store.CACHE_MANAGER w serwisie.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:
. 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.store
ttl: 300 to domyślny czas życia wpisu w sekundach - pięć minut. Po tym czasie Redis usuwa go sam.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.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ć:
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.
Wspólny magazyn stoi, wszystkie instancje sięgają do tego samego:
cache-manager-redis-store → skonfiguruj CacheModule z Redis store → wstrzyknij CACHE_MANAGER → używaj cache.get() i cache.set(),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,legion:42), żeby dwa byty nie zaczęły używać tej samej liczby,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.