Lista prowincji zmienia się raz na kwartał. Zapytanie o nią przychodzi tysiąc razy na minutę - i za każdym razem baza wykonuje tę samą pracę, żeby zwrócić ten sam wynik. Serwer jest obciążony nie dlatego, że robi dużo, tylko dlatego, że robi wciąż to samo.
Legion nie posyłał gońca do Rzymu po każdy bochenek chleba. Trzymał skrytkę z zapasami przy obozie: to, co potrzebne często, leżało pod ręką, a po resztę schodziło się do składu. Cache to taka skrytka - tymczasowe przechowywanie danych dla szybszego dostępu.
Zwróć uwagę na słowo „tymczasowe". Cache nie jest trwałym magazynem - od tego jest baza. Nie jest też narzędziem do kompresji ani do logowania. Dane w nim mają zniknąć, i to jest cecha, nie usterka.
konfiguruje cache wraz z jego parametrami - nie routing, nie autoryzację, nie logowanie:CacheModule.register()
1@Module({
2 imports: [
3 CacheModule.register({
4 isGlobal: true,
5 ttl: 300,
6 max: 1000,
7 }),
8 ],
9})
10export class AppModule {}
(Time To Live) oznacza maksymalny czas życia wpisu, po którym jest on automatycznie usuwany - tutaj 300 sekund. Nie jest to liczba dozwolonych odczytów ani czas odpowiedzi; to termin ważności.ttl
ogranicza liczbę wpisów. Gdy zostanie przekroczona, cache usuwa najdawniej używane - bo pamięć jest skończona, a cache bez limitu prędzej czy później ją wyczerpie.max
Cache wstrzykujemy po tokenie. Kolejność zapisu:
, @Inject(CACHE_MANAGER)
, private cache:
:Cache
1@Injectable()
2export class ProvinceService {
3 constructor(
4 @Inject(CACHE_MANAGER) private cache: Cache,
5 private repo: ProvinceRepository,
6 ) {}
7
8 async findAll() {
9 const cached = await this.cache.get<Province[]>('provinces:all');
10 if (cached) {
11 return cached;
12 }
13
14 const provinces = await this.repo.findAll();
15 await this.cache.set('provinces:all', provinces, 600);
16
17 return provinces;
18 }
19}Ten schemat nazywa się cache-aside (albo lazy loading) i oznacza, że kod ręcznie pobiera dane z cache i ręcznie je tam zapisuje. Cztery kroki, zawsze w tej kolejności:
get).set) i zwróć.Nazwa „lazy" bierze się z kroku trzeciego: cache zapełnia się leniwie, dopiero gdy ktoś o dane poprosi. Pierwsze żądanie zawsze idzie do bazy.
Dla zwykłych odczytów nie musisz pisać tych czterech kroków:
1@Controller('provinces')
2export class ProvinceController {
3 @Get()
4 @UseInterceptors(CacheInterceptor)
5 @CacheKey('provinces:all')
6 @CacheTTL(600)
7 findAll() {
8 return this.provinceService.findAll();
9 }
10}
automatycznie cache'uje odpowiedzi endpointu - nie kompresuje ich, nie loguje i nie waliduje.@UseInterceptors(CacheInterceptor)
to dekorator odpowiadający za czas życia danych z tej metody - sześćset sekund, czyli dziesięć minut, niezależnie od wartości domyślnej. Nie mylić z @CacheTTL(600)
@CacheKey, który nadaje nazwę wpisu, ani z @UseInterceptors, który dopiero włącza cache'owanie.Domyślnie klucz powstaje z adresu URL.
@CacheKey przydaje się, gdy chcesz go ustalić sam - na przykład po to, żeby móc ten sam wpis później usunąć z serwisu.Powyższe podejścia zapełniają cache przy odczycie. Można też przy zapisie, i tu istnieją dwa warianty różniące się jednym słowem:
Różnica sprowadza się więc do tego, czy aplikacja czeka na bazę. Write-through wybieraj tam, gdzie utrata zapisu jest niedopuszczalna; write-behind tam, gdzie liczy się przepustowość, a pojedynczy zgubiony wpis nie jest katastrofą - jak przy licznikach odsłon.
Dane w cache starzeją się. Sposoby radzenia sobie z tym układają się od najprostszego do najbardziej złożonego:
provinces:. Najpotężniejsze i najłatwiej nim usunąć za dużo.Sam proces unieważnienia po aktualizacji ma trzy kroki: zaktualizuj dane w bazie, zidentyfikuj i usuń odpowiednie klucze, a następne żądanie pobierze świeże dane i zapisze je na nowo.
Przy cache-aside pierwszy użytkownik po każdym restarcie płaci pełną cenę odczytu z bazy. Można temu zapobiec, zapełniając skrytkę zanim ktokolwiek poprosi:
1@Injectable()
2export class CacheWarmupService implements OnModuleInit {
3 constructor(
4 @Inject(CACHE_MANAGER) private cache: Cache,
5 private repo: ProvinceRepository,
6 ) {}
7
8 async onModuleInit() {
9 const provinces = await this.repo.findAll();
10 await this.cache.set('provinces:all', provinces, 3600);
11 }
12}
to metoda wywoływana przez NestJS przy starcie aplikacji, po zbudowaniu modułu. Proces jest trzyetapowy: zidentyfikuj dane krytyczne, pobierz je i zapisz przy starcie, dostosuj listę na podstawie tego, co naprawdę jest odpytywane najczęściej.onModuleInit
Rozgrzewaj tylko to, co jest jednocześnie często potrzebne i rzadko zmieniane, @name. Rozgrzewanie wszystkiego wydłuża start aplikacji i zapełnia pamięć danymi, po które nikt nie sięgnie.
Skrytka przy obozie działa, po resztę schodzi się do składu:
CacheModule.register() konfiguruje cache z parametrami; max ogranicza liczbę wpisów,@Inject(CACHE_MANAGER), private cache:, Cache,@UseInterceptors(CacheInterceptor) automatycznie cache'uje odpowiedzi endpointu,@CacheTTL(600) ustawia czas życia; @CacheKey nadaje nazwę wpisu, @UseInterceptors włącza mechanizm,onModuleInit: zidentyfikuj dane krytyczne → pobierz i zapisz przy starcie → dostosuj listę.W następnej lekcji przeniesiemy skrytkę poza proces - do Redisa, dzięki czemu zobaczą ją wszystkie instancje aplikacji. A na razie zapamiętaj: cache nie przyspiesza pracy, tylko pozwala jej nie wykonywać - a cała trudność polega na tym, by wiedzieć, kiedy zapas się zestarzał.