Provider scope trong NestJS: singleton, request và transient Trung cấp
Thời gian đọc ước tính: 6 phút
Vấn đề
Phần tiêu đề “Vấn đề”CheckoutService cần tenant của request hiện tại để chọn cấu hình thanh toán. Hai cực đều có giá:
- Cách ngây thơ: middleware gán
this.currentTenantvào service. Nest mặc định dùng một instance cho cả application, nên hai request chạy xen kẽ ghi đè field của nhau. - Cách ngược lại: gắn
Scope.REQUESTlên mọi service liên quan, và Nest phải tạo cả cây object mới cho mỗi request.
Hai thái cực đều sai. Cần phân biệt dữ liệu theo request với vòng đời của cả service.
Số instance theo scope
Phần tiêu đề “Số instance theo scope”Demo count-demo.ts resolve CheckoutService trong 100 context khác nhau (giả lập 100 request, không có HTTP). Số lần constructor chạy, đo thật trên Nest 11:
| Provider | Scope | Số instance sau 100 request | Vì sao |
|---|---|---|---|
Config |
DEFAULT |
1 | Dùng chung trong application context |
Formatter |
TRANSIENT |
2 | Mỗi consumer inject (ReportService, InvoiceService) nhận một bản riêng; hai consumer này là default nên chỉ tạo một lần |
ReportService, InvoiceService |
DEFAULT |
1 mỗi loại | Singleton, giữ Formatter của mình qua mọi request |
RequestContext |
REQUEST |
100 | Một bản cho mỗi request context |
CheckoutService |
DEFAULT (không khai báo) |
100 | Inject RequestContext nên bị kéo thành request-scoped |
Hai dòng cuối là điểm hay bỏ sót: scope “bubble up” theo chuỗi inject, kể cả khi provider phía trên không khai báo gì. Nếu CheckoutService được inject vào controller, controller cũng trở thành request-scoped.
Đang tải diagram…
Khai báo scope:
@Injectable({ scope: Scope.REQUEST })class RequestContext {}
@Injectable({ scope: Scope.TRANSIENT })class Formatter {}Với scoped provider trong standalone context, dùng app.resolve(token, contextId) thay cho app.get(). Trong ứng dụng HTTP, Nest tự tạo context ID cho từng request.
Chọn REQUEST, ALS hay tham số
Phần tiêu đề “Chọn REQUEST, ALS hay tham số”Đang tải diagram…
| Cách | Instance | Chi phí | Hợp với |
|---|---|---|---|
| Tham số | Singleton | Sửa chữ ký hàm | Dữ liệu nghiệp vụ |
| ALS | Singleton | Dependency ngầm vào entry point | Correlation ID, logger |
Scope.REQUEST |
Mỗi request một cây | Cấp phát theo request, lan scope | State thật sự sống đúng một request (cache trong request, unit of work) |
Tài liệu Nest nói request scope trong một ứng dụng thiết kế tốt không nên làm latency tăng quá khoảng 5%. Đó là con số của tài liệu, không phải cam kết cho hệ thống của bạn.
Đo với graph thật, đặc biệt khi một provider trung tâm như logger hay datasource bị kéo thành request-scoped, vì nó kéo theo mọi thứ phía trên.
Request scope cũng buộc code gắn với request của transport:
| Transport | Request scope |
|---|---|
| Cron | Không có request (tài liệu Nest nêu Cron controller phải là singleton, không dùng request-scoped provider) |
| Microservice như Kafka | Vẫn có context theo message, nhưng cách lấy request khác HTTP |
| WebSocket gateway | Scope gắn với kết nối, không phải từng message |
Kiểm tra transport trước khi dựa vào nó.
Durable provider cho multi-tenant
Phần tiêu đề “Durable provider cho multi-tenant”Khi request scope chỉ cần khác nhau theo tenant (vài chục tenant, hàng nghìn request), tạo lại cả cây cho mỗi request là phí. Durable provider cho phép Nest gom request theo một khoá (ví dụ tenant) và tái sử dụng cây dependency cho cả nhóm. Theo tài liệu Nest:
@Injectable({ scope: Scope.REQUEST, durable: true })export class TenantConfig {}Cần thêm một ContextIdStrategy quyết định request thuộc nhóm nào, đăng ký một lần ở bootstrap:
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());Chiến lược đọc tenant từ request (ví dụ header x-tenant-id), trả payload chứa tenant ID để provider durable nhận qua @Inject(REQUEST). Hai điều kiện:
- Số nhóm phải hữu hạn (cache theo tenant không giới hạn sẽ giữ vô hạn instance).
- Đo heap với số tenant thật.
Repo này chưa chạy durable provider; code trên bám theo tài liệu Nest, hãy đọc mục “Durable providers” cho đúng phiên bản bạn dùng.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Checklist production
Phần tiêu đề “Checklist production”- Mặc định
DEFAULT; chỉ đổi scope khi có state thật sự sống theo request. - Không lưu
currentUserhaycurrentTenanttrong field của providerDEFAULT. - Trước khi gắn
Scope.REQUEST, liệt kê mọi provider phía trên sẽ bị kéo theo (đặc biệt logger, datasource). - Metadata (requestId, tenant cho log) dùng ALS; dữ liệu nghiệp vụ truyền tham số.
- Test song song với nhiều tenant; một field mutable có thể pass test tuần tự nhưng race khi tải đồng thời.
- Đếm số instance tạo ra và đo p99 trước và sau khi đổi scope, dùng graph thật.
- Multi-tenant với số tenant hữu hạn thì cân nhắc durable provider và đo heap.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Tóm tắt
Phần tiêu đề “Tóm tắt”Tự kiểm tra: (1) Vì sao currentTenant trên service mặc định có thể sai khi chạy song song? (2) Transient Formatter inject vào consumer default có được tạo lại mỗi request không? (3) CheckoutService không khai báo scope nhưng inject provider REQUEST: nó có bao nhiêu instance sau 100 request?
Chạy demo:
npx --prefix examples ts-node examples/src/nestjs-provider-scopes/count-demo.tsnpx --prefix examples ts-node examples/src/nestjs-provider-scopes/scopes-demo.ts