Kurs NestJS · Moduł 8: Cache i wydajność
API Rate Limiting i warstwa cache - kontrola ruchu portowego
W tej lekcji5
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.
Strażnik - ThrottlerModule
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łę. ttl to długość okna w milisekundach - tutaj minuta. limit to liczba żądań dozwolonych w tym oknie. Razem: sto żądań na minutę z jednego adresu.
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ą.
Magazyn - serwis cache
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. get czyta, set zapisuje z czasem życia, del usuwa. Domyślne 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.
Automatyczny cache dla endpointów
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:
- Przechwycenie żądania HTTP.
- Sprawdzenie klucza w cache.
- Wykonanie handlera - tylko jeśli był MISS.
- Zapisanie wyniku do cache.
- Zwrócenie odpowiedzi klientowi.
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ć.
Unieważnianie - najtrudniejsza część
Cache jest łatwy, dopóki dane się nie zmieniają. Gdy się zmieniają, trzeba stare wpisy usunąć - a proces ma cztery kroki:
- Wykrycie zmiany danych w źródle (DB).
- Identyfikacja kluczy cache do invalidacji.
- Usunięcie starych danych z cache (
del). - Pobranie i zapisanie nowych danych do cache.
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 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.
Podsumowanie
Port kontroluje ruch i wydaje towar z magazynu:
- pod obciążeniem masz dwie drogi: cache zmniejsza pracę na żądanie, rate limiting ogranicza liczbę żądań,
ThrottlerModule.forRootopisują dwie liczby:ttl(okno w milisekundach) ilimit(żądania w oknie),- rejestracja przez
APP_GUARDczyniThrottlerGuardglobalnym; po przekroczeniu limitu klient dostaje 429, @Throttlezaostrza limit dla pojedynczej trasy - przy logowaniu to zabezpieczenie, nie optymalizacja,- serwis cache opakowuje trzy metody:
get,setz konfigurowalnym TTL idel, CacheInterceptordziała w pięciu krokach: przechwycenie żądania → sprawdzenie klucza → wykonanie handlera (jeśli MISS) → zapisanie wyniku → zwrócenie odpowiedzi,- przy trafieniu metoda kontrolera nie wykonuje się wcale; klucz powstaje z adresu URL, więc różne parametry to różne wpisy,
- interceptor obsługuje tylko
GET, - invalidacja w czterech krokach: wykrycie zmiany w bazie → identyfikacja kluczy → usunięcie starych (
del) → pobranie i zapisanie nowych, - jedna zmiana unieważnia zwykle kilka kluczy - projektuj klucze pytając, co będziesz musiał usunąć.
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ąć.
Kod do tej lekcji: src/rate-limiting.ts
1// API Rate Limiting - Kontrola Ruchu Portowego
2import {
3 Injectable, NestMiddleware, HttpException, HttpStatus,
4} from '@nestjs/common';
5import { Request, Response, NextFunction } from 'express';
6
7// 1. Prosty Rate Limiter
8@Injectable()
9export class RateLimitMiddleware implements NestMiddleware {
10 private requests: Map<string, { count: number; resetTime: number }> = new Map();
11 private readonly MAX_REQUESTS = 100; // Max requestow
12 private readonly WINDOW_MS = 60 * 1000; // Okno czasowe: 1 minuta
13
14 use(req: Request, res: Response, next: NextFunction) {
15 const clientIp = req.ip || 'unknown';
16 const now = Date.now();
17
18 let record = this.requests.get(clientIp);
19
20 // Resetuj jesli okno wygaslo
21 if (!record || now > record.resetTime) {
22 record = { count: 0, resetTime: now + this.WINDOW_MS };
23 this.requests.set(clientIp, record);
24 }
25
26 record.count++;
27
28 // Ustaw headery informacyjne
29 res.setHeader('X-RateLimit-Limit', this.MAX_REQUESTS);
30 res.setHeader('X-RateLimit-Remaining',
31 Math.max(0, this.MAX_REQUESTS - record.count));
32 res.setHeader('X-RateLimit-Reset',
33 new Date(record.resetTime).toISOString());
34
35 if (record.count > this.MAX_REQUESTS) {
36 throw new HttpException(
37 'Too Many Requests - port jest przeciazony!',
38 HttpStatus.TOO_MANY_REQUESTS
39 );
40 }
41
42 next();
43 }
44}
45
46// 2. NestJS Throttler (zalecane rozwiazanie)
47// npm install @nestjs/throttler
48//
49// @Module({
50// imports: [
51// ThrottlerModule.forRoot({
52// ttl: 60, // Okno czasowe (sekundy)
53// limit: 100, // Max requestow w oknie
54// }),
55// ],
56// })
57//
58// // Uzycie na kontrolerze lub endpoincie:
59// @UseGuards(ThrottlerGuard)
60// @Throttle(10, 60) // 10 requestow na 60 sekund
61// @Get('tribute')
62// getTribute() { ... }
63
64// 3. Rozne strategie limitowania
65const strategies = {
66 // Per IP - kazdy klient ma swoj limit
67 perIp: { key: 'ip', limit: 100, window: 60 },
68
69 // Per User - zalogowani uzytkownicy maja wyzszy limit
70 perUser: { key: 'userId', limit: 500, window: 60 },
71
72 // Per Endpoint - rozne limity na rozne endpointy
73 perEndpoint: {
74 'GET /tributes': { limit: 200, window: 60 },
75 'POST /tributes': { limit: 20, window: 60 },
76 'DELETE /tributes': { limit: 5, window: 60 },
77 },
78
79 // Sliding Window - bardziej precyzyjne liczenie
80 slidingWindow: { limit: 100, window: 60, precision: 1 },
81};
82
83// 4. Redis-based rate limiting (dla wielu instancji)
84// Kazda instancja API wspoldzieli stan w Redis
85// Klucz: ratelimit:{ip}:{endpoint}
86// Wartosc: licznik requestow
87// TTL: czas okna
88Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Edytor kodu
Stwórz serwis cache z metodami get/set/del używając Redis store z konfigurowalnymi TTL
- Układanie w pionie
Uporządkuj kroki procesu invalidacji cache od wykrycia zmiany do aktualizacji:
- Klikanie w kolejności
Ułóż kolejność działania CacheInterceptor przy obsłudze żądania HTTP: