Port przyjmuje tyle statków, ile zdoła rozładować. Gdy przypłynie więcej, są dwa wyjścia: rozładowywać szybciej albo wpuszczać mniej. Aplikacja pod obciążeniem ma dokładnie te same dwa - cache zmniejsza pracę przypadającą na żądanie, a rate limiting ogranicza liczbę żądań, które w ogóle przyjmujesz.
Ta lekcja domyka obie strony: najpierw strażnik przy bramie, potem magazyn, z którego wydaje się towar bez schodzenia do składu.
NestJS ma wbudowany mechanizm ograniczania ruchu, więc nie pisz własnego licznika:
1@Module({
2 imports: [
3 ThrottlerModule.forRoot([
4 {
5 ttl: 60000,
6 limit: 100,
7 },
8 ]),
9 ],
10 providers: [
11 { provide: APP_GUARD, useClass: ThrottlerGuard },
12 ],
13})
14export class AppModule {}Dwie liczby opisują całą regułę.
to długość okna w milisekundach - tutaj minuta. ttl
to liczba żądań dozwolonych w tym oknie. Razem: sto żądań na minutę z jednego adresu.limit
Rejestracja przez
APP_GUARD czyni z ThrottlerGuard guarda globalnego - działa na wszystkich trasach bez dopisywania @UseGuards w każdym kontrolerze. Po przekroczeniu limitu klient dostaje 429 Too Many Requests, a metoda kontrolera nie wykona się wcale.Pojedyncze trasy można poluzować albo zaostrzyć dekoratorem:
1@Post('login')
2@Throttle({ default: { ttl: 60000, limit: 5 } })
3login(@Body() dto: LoginDto) { }Pięć prób logowania na minutę zamiast stu. To jest właśnie miejsce, gdzie rate limiting przestaje być kwestią wydajności, a staje się zabezpieczeniem: bez niego nic nie powstrzyma zgadywania haseł metodą siłową.
Drugą stronę obsługuje cache. Opakowanie go we własny serwis daje jedno miejsce na klucze i czasy życia:
1@Injectable()
2export class CacheService {
3 constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
4
5 async get<T>(key: string): Promise<T | undefined> {
6 return this.cache.get<T>(key);
7 }
8
9 async set<T>(key: string, value: T, ttl = 300): Promise<void> {
10 await this.cache.set(key, value, ttl);
11 }
12
13 async del(key: string): Promise<void> {
14 await this.cache.del(key);
15 }
16}Trzy metody wystarczają na wszystko.
czyta, get
zapisuje z czasem życia, set
usuwa. Domyślne del
ttl = 300 można nadpisać przy każdym wywołaniu - krótkie dla danych zmiennych, długie dla słowników.Zwróć uwagę na
del - to metoda, o której najłatwiej zapomnieć przy pisaniu, a najtrudniej bez niej żyć. Bez możliwości usunięcia klucza jedynym sposobem na pozbycie się nieaktualnych danych jest czekanie, aż wygasną same.Dla zwykłych odczytów nie musisz pisać nawet tego.
CacheInterceptor robi to za Ciebie:1@Controller('provinces')
2export class ProvincesController {
3 @Get()
4 @UseInterceptors(CacheInterceptor)
5 @CacheTTL(600)
6 findAll() {
7 return this.provincesService.findAll();
8 }
9}Przy każdym żądaniu przechodzi przez pięć kroków, zawsze w tej kolejności:
Krok trzeci jest sednem: przy trafieniu metoda kontrolera w ogóle się nie wykonuje, więc nie ma zapytania do bazy ani żadnej pracy. Klucz interceptor buduje domyślnie z adresu URL, więc
/provinces?page=2 i /provinces?page=3 to dwa osobne wpisy.Stąd też jego ograniczenie: interceptor obsługuje wyłącznie żądania
GET. Nie ma sensu cache'ować POST, bo ten ma zmieniać stan, a nie go odczytywać.Cache jest łatwy, dopóki dane się nie zmieniają. Gdy się zmieniają, trzeba stare wpisy usunąć - a proces ma cztery kroki:
del).1async update(id: number, dto: UpdateProvinceDto) {
2 const province = await this.repo.save({ id, ...dto });
3
4 await this.cacheService.del(`province:${id}`);
5 await this.cacheService.del('provinces:all');
6
7 return province;
8}Krok drugi jest tym, który sprawia najwięcej kłopotu, i widać to w powyższym kodzie. Zmiana jednej prowincji unieważnia dwa klucze: wpis tej prowincji i listę wszystkich, bo lista też zawiera jej dane. Przy trzeciej stronie wyników i piątym filtrze tych kluczy robi się kilkanaście - i wtedy zaczyna się pominięcia.
Dlatego, @name, przy projektowaniu kluczy zadaj sobie od razu pytanie odwrotne: nie „jak to zapisać", tylko „co będę musiał usunąć, gdy to się zmieni". Cache, którego nie umiesz unieważnić, jest gorszy od braku cache - bo pokazuje dane nieaktualne, a ty o tym nie wiesz.
Port kontroluje ruch i wydaje towar z magazynu:
ThrottlerModule.forRoot opisują dwie liczby: ttl (okno w milisekundach) i limit (żądania w oknie),APP_GUARD czyni ThrottlerGuard globalnym; po przekroczeniu limitu klient dostaje 429,@Throttle zaostrza limit dla pojedynczej trasy - przy logowaniu to zabezpieczenie, nie optymalizacja,get, set z konfigurowalnym TTL i del,CacheInterceptor działa w pięciu krokach: przechwycenie żądania → sprawdzenie klucza → wykonanie handlera (jeśli MISS) → zapisanie wyniku → zwrócenie odpowiedzi,GET,del) → pobranie i zapisanie nowych,W następnej lekcji zajmiemy się kompresją i optymalizacją odpowiedzi - tym, co zmniejsza ruch już po stronie wyjścia. A na razie zapamiętaj: cache przyspiesza obsługę, limit chroni przed nadmiarem - a najtrudniejszy w cache nie jest zapis, tylko wiedza, kiedy go usunąć.