Caching là một kỹ thuật mạnh mẽ và đơn giản để nâng cao hiệu năng ứng dụng của bạn. Bằng cách đóng vai trò như một lớp lưu trữ tạm thời, nó cho phép truy cập nhanh hơn tới dữ liệu được dùng thường xuyên, giảm nhu cầu phải fetch hoặc tính toán lại cùng một thông tin nhiều lần. Điều này giúp thời gian phản hồi nhanh hơn và cải thiện hiệu quả tổng thể.
Cài đặt
Để bắt đầu dùng caching trong Nest, bạn cần cài package @nestjs/cache-manager cùng với package cache-manager.
$ npm install @nestjs/cache-manager cache-manager
Mặc định, mọi thứ được lưu trong memory (bộ nhớ); vì cache-manager dùng Keyv bên dưới, bạn có thể dễ dàng chuyển sang một giải pháp lưu trữ nâng cao hơn, chẳng hạn Redis, bằng cách cài package phù hợp. Chúng ta sẽ nói kỹ hơn về điều này ở phần sau.
Cache trong memory (In-memory cache)
Để bật caching trong ứng dụng của bạn, import CacheModule và cấu hình nó bằng phương thức register():
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller.js';
@Module({
imports: [CacheModule.register()],
controllers: [AppController],
})
export class AppModule {}
Cấu hình này khởi tạo caching trong memory với các thiết lập mặc định, cho phép bạn bắt đầu cache dữ liệu ngay lập tức.
Tương tác với cache store
Để tương tác với instance cache manager, inject nó vào class của bạn bằng token CACHE_MANAGER, như sau:
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
Gợi ý Class Cache và token CACHE_MANAGER đều được import từ package @nestjs/cache-manager.
Phương thức get trên instance Cache (từ package cache-manager) được dùng để lấy các item từ cache. Nếu item không tồn tại trong cache, undefined sẽ được trả về (trong cache-manager v6 trở về trước, null được trả về thay vào đó). Hãy coi cả hai đều là falsy khi migrate.
const value = await this.cacheManager.get('key');
Để thêm một item vào cache, dùng phương thức set:
await this.cacheManager.set('key', 'value');
Lưu ý Bộ lưu trữ cache trong memory chỉ có thể lưu các giá trị thuộc kiểu được hỗ trợ bởi structured clone algorithm.
Ghi chú người dịch Structured clone algorithm là cơ chế “deep clone” chuẩn của JS engine (dùng bởi postMessage, structuredClone()), nó không clone được function, class instance có method, hay Symbol. Nếu bạn quen lưu cache bằng Redis từ PHP/Go rồi serialize JSON thủ công, điểm khác biệt ở đây là store in-memory của Nest bỏ qua bước serialize/deserialize — nên nó nhanh hơn nhưng cũng kén kiểu dữ liệu hơn JSON.
Bạn có thể chỉ định thủ công một TTL (thời gian hết hạn, tính bằng mili-giây) cho key cụ thể này, như sau:
await this.cacheManager.set('key', 'value', 1000);
Trong đó 1000 là TTL tính bằng mili-giây - trong trường hợp này, cache item sẽ hết hạn sau một giây.
Để tắt việc hết hạn của cache, đặt thuộc tính cấu hình ttl thành 0:
await this.cacheManager.set('key', 'value', 0);
Để xóa một item khỏi cache, dùng phương thức del:
await this.cacheManager.del('key');
Để xóa toàn bộ cache, dùng phương thức clear:
await this.cacheManager.clear();
Tự động cache response (Auto-caching responses)
Cảnh báo Trong các ứng dụng GraphQL, interceptor được thực thi riêng biệt cho từng field resolver. Do đó, CacheModule (vốn dùng interceptor để cache response) sẽ không hoạt động đúng.
Để bật tự động cache response, chỉ cần gắn (bind) CacheInterceptor vào nơi bạn muốn cache dữ liệu.
@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
@Get()
findAll(): string[] {
return [];
}
}
warningCảnh báo Chỉ các endpoint
GETmới được cache. Ngoài ra, các route của HTTP server mà inject native response object (@Res()) không thể dùng Cache Interceptor. Xem response mapping để biết thêm chi tiết.
Để giảm lượng boilerplate cần thiết, bạn có thể gắn CacheInterceptor cho toàn bộ endpoint một cách global:
import { Module } from '@nestjs/common';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';
import { AppController } from './app.controller.js';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
imports: [CacheModule.register()],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: CacheInterceptor,
},
],
})
export class AppModule {}
Ghi chú người dịch Cách APP_INTERCEPTOR hoạt động ở đây tương đương việc bạn đăng ký một middleware global trong Express (app.use(cacheMiddleware)) hoặc một global middleware trong Laravel (app/Http/Kernel.php) — điểm khác là interceptor của Nest chạy quanh cả pipeline (trước lẫn sau khi handler xử lý), còn middleware chỉ chạy trước. Vì đăng ký qua DI token, CacheInterceptor này còn có thể inject phụ thuộc khác nếu bạn subclass nó.
Thời gian sống (Time-to-live - TTL)
Giá trị mặc định cho ttl là 0, nghĩa là cache sẽ không bao giờ hết hạn. Để chỉ định một TTL tùy chỉnh, bạn có thể cung cấp tùy chọn ttl trong phương thức register(), như minh họa dưới đây:
CacheModule.register({
ttl: 5000, // milliseconds
});
Dùng module ở phạm vi global (Use module globally)
Khi bạn muốn dùng CacheModule trong các module khác, bạn cần import nó (như thông thường với bất kỳ module Nest nào). Ngoài ra, bạn có thể khai báo nó như một global module bằng cách đặt thuộc tính isGlobal của object options thành true, như bên dưới. Trong trường hợp đó, bạn sẽ không cần import CacheModule ở các module khác một khi nó đã được load ở module gốc (ví dụ, AppModule).
CacheModule.register({
isGlobal: true,
});
Ghi đè cache global (Global cache overrides)
Trong khi cache global được bật, các cache entry được lưu dưới một CacheKey được tự động sinh ra dựa trên route path. Bạn có thể ghi đè một số thiết lập cache (@CacheKey() và @CacheTTL()) theo từng phương thức, cho phép các chiến lược caching tùy biến cho từng phương thức controller. Điều này có thể phù hợp nhất khi dùng các cache store khác nhau.
Bạn có thể áp dụng decorator @CacheTTL() theo từng controller để đặt TTL caching cho cả controller. Trong trường hợp cả thiết lập TTL cấp controller lẫn cấp phương thức đều được định nghĩa, thiết lập TTL cache ở cấp phương thức sẽ được ưu tiên hơn thiết lập ở cấp controller.
@Controller()
@CacheTTL(5000)
export class AppController {
@CacheKey('custom_key')
@CacheTTL(2000)
findAll(): string[] {
return [];
}
}
Gợi ý Decorator @CacheKey() và @CacheTTL() được import từ package @nestjs/cache-manager.
Decorator @CacheKey() có thể được dùng kèm hoặc không kèm decorator @CacheTTL() tương ứng, và ngược lại. Bạn có thể chọn chỉ ghi đè @CacheKey() hoặc chỉ @CacheTTL(). Các thiết lập không được ghi đè bằng decorator sẽ dùng giá trị mặc định đã đăng ký ở cấp global (xem Tùy biến caching).
WebSockets và Microservices
Bạn cũng có thể áp dụng CacheInterceptor cho cả WebSocket subscriber lẫn pattern của Microservice (bất kể phương thức transport nào đang được dùng).
@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
return [];
}
@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client, data) {
return [];
}
Tuy nhiên, decorator @CacheKey() bổ sung là bắt buộc để chỉ định một key dùng để lưu và lấy lại dữ liệu đã cache sau đó. Ngoài ra, xin lưu ý rằng bạn không nên cache mọi thứ. Các hành động thực hiện một số thao tác nghiệp vụ thay vì chỉ đơn thuần truy vấn dữ liệu thì không bao giờ nên được cache.
Thêm vào đó, bạn có thể chỉ định thời gian hết hạn cache (TTL) bằng decorator @CacheTTL(), decorator này sẽ ghi đè giá trị TTL mặc định ở cấp global.
@CacheTTL(1000)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
return [];
}
@CacheTTL(1000)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client, data) {
return [];
}
Gợi ý Decorator @CacheTTL() có thể được dùng kèm hoặc không kèm decorator @CacheKey() tương ứng.
Điều chỉnh cách tracking (Adjust tracking)
Mặc định, Nest dùng request URL (trong ứng dụng HTTP) hoặc cache key (trong ứng dụng websocket và microservice, đặt qua decorator @CacheKey()) để gắn kết các cache record với endpoint của bạn. Tuy nhiên, đôi khi bạn có thể muốn thiết lập việc tracking dựa trên các yếu tố khác, ví dụ dùng HTTP header (chẳng hạn Authorization để nhận diện đúng các endpoint profile).
Để làm được điều đó, tạo một subclass của CacheInterceptor và ghi đè phương thức trackBy().
@Injectable()
class HttpCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext): string | undefined {
return 'key';
}
}
Ghi chú người dịch trackBy() chính là chỗ bạn tự quyết định “khóa cache” (cache key) được sinh ra như thế nào cho mỗi request — mặc định dựa trên URL, nhưng nếu hai user khác nhau gọi cùng một URL /profile mà bạn không override trackBy() để tách theo token/Authorization header, bạn sẽ vô tình trả cache của user A cho user B. Đây là một cạm bẫy bảo mật thực tế hay gặp khi bật global cache mà quên tùy biến tracking cho các endpoint có dữ liệu theo user.
Dùng cache store thay thế (Using alternative Cache stores)
Chuyển sang một cache store khác khá đơn giản. Đầu tiên, cài package phù hợp. Ví dụ, để dùng Redis, cài package @keyv/redis:
$ npm install @keyv/redis
Sau khi có package này, bạn có thể đăng ký CacheModule với nhiều store như bên dưới:
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller.js';
import { createKeyv } from '@keyv/redis';
import { Keyv } from 'keyv';
import { KeyvCacheableMemory } from 'cacheable';
@Module({
imports: [
CacheModule.registerAsync({
useFactory: async () => {
return {
stores: [
new Keyv({
store: new KeyvCacheableMemory({ ttl: 60000, lruSize: 5000 }),
}),
createKeyv('redis://localhost:6379'),
],
};
},
}),
],
controllers: [AppController],
})
export class AppModule {}
Trong ví dụ này, chúng ta đã đăng ký hai store: CacheableMemory và một Redis store. Store CacheableMemory là một store in-memory đơn giản, được tạo qua storage adapter KeyvCacheableMemory, còn Redis store được tạo qua helper createKeyv từ @keyv/redis. Mảng stores được dùng để chỉ định các store bạn muốn dùng. Store đầu tiên trong mảng là store mặc định, các store còn lại là store dự phòng (fallback).
Xem tài liệu Keyv để biết thêm thông tin về các store có sẵn.
Cấu hình bất đồng bộ (Async configuration)
Bạn có thể muốn truyền các option của module theo cách bất đồng bộ thay vì truyền tĩnh tại compile time. Trong trường hợp này, dùng phương thức registerAsync(), phương thức này cung cấp nhiều cách để xử lý cấu hình bất đồng bộ.
Một cách tiếp cận là dùng factory function:
CacheModule.registerAsync({
useFactory: () => ({
ttl: 5000,
}),
});
Factory của chúng ta hoạt động giống như mọi factory module bất đồng bộ khác (nó có thể là async và có thể inject các phụ thuộc thông qua inject).
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
ttl: configService.get('CACHE_TTL'),
}),
inject: [ConfigService],
});
Ngoài ra, bạn có thể dùng cách useClass:
CacheModule.registerAsync({
useClass: CacheConfigService,
});
Cấu trúc trên sẽ khởi tạo CacheConfigService bên trong CacheModule và dùng nó để lấy object options. CacheConfigService phải implement interface CacheOptionsFactory để cung cấp các option cấu hình:
@Injectable()
class CacheConfigService implements CacheOptionsFactory {
createCacheOptions(): CacheModuleOptions {
return {
ttl: 5000,
};
}
}
Nếu bạn muốn dùng một provider cấu hình đã có sẵn, được import từ module khác, dùng cú pháp useExisting:
CacheModule.registerAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Cách này hoạt động giống useClass nhưng có một khác biệt quan trọng - CacheModule sẽ tìm trong các module đã import để tái sử dụng bất kỳ ConfigService nào đã được tạo, thay vì tự khởi tạo instance riêng của nó.
Gợi ý CacheModule#register, CacheModule#registerAsync và CacheOptionsFactory có một generic tùy chọn (type argument) để thu hẹp các option cấu hình đặc thù cho từng store, giúp nó an toàn về kiểu (type safe).
Bạn cũng có thể truyền cái gọi là extraProviders cho phương thức registerAsync(). Các provider này sẽ được gộp chung với các provider của module.
CacheModule.registerAsync({
imports: [ConfigModule],
useClass: ConfigService,
extraProviders: [MyAdditionalProvider],
});
Điều này hữu ích khi bạn muốn cung cấp thêm các phụ thuộc cho factory function hoặc constructor của class.
Ví dụ
Một ví dụ đầy đủ có sẵn tại đây.