layercache – Thư viện bộ nhớ đệm đa lớp cho Node.js
(github.com/flyingsquirrel0419)layercache là gì?
Đây là một thư viện bộ nhớ đệm đa lớp cho Node.js, gộp Memory → Redis → Disk vào trong một API duy nhất.
Khi cache hit, giá trị sẽ được lấy từ lớp nhanh nhất và các lớp phía trên sẽ được tự động điền lại. Khi miss, ngay cả khi có 100 yêu cầu đồng thời, fetcher cũng chỉ chạy đúng một lần.
Vì sao nó được tạo ra?
Khi vận hành dịch vụ Node.js, cách xây dựng các lớp cache thường đi theo một lộ trình khá giống nhau. Bắt đầu với in-memory cache, khi số lượng instance tăng lên thì gắn Redis vào, rồi gặp vấn đề stampede, phát sinh tình trạng cache không nhất quán giữa các instance... Từng vấn đề đều có thể giải quyết, nhưng để kết nối tất cả lại ở mức production thì tốn công hơn tưởng tượng rất nhiều.
Vì vậy, nó được tạo ra với ý tưởng là chỉ cần làm bài bản công việc đó một lần.
Những tính năng chính là gì?
Cơ chế cốt lõi
- Đọc theo lớp + tự động backfill (L1 miss → truy vấn L2 → điền lại L1)
- Stampede prevention: 100 yêu cầu đồng thời →
fetcherchạy 1 lần - Distributed single-flight: loại bỏ thực thi trùng lặp giữa các instance bằng Redis distributed lock
- L1 invalidation bus dựa trên Redis pub/sub (đồng bộ in-memory cache giữa các instance)
Invalidation / độ tươi mới
- Invalidation theo tag, invalidation bằng wildcard/tiền tố
- Stale-while-revalidate, Stale-if-error
- Sliding TTL, Adaptive TTL, Refresh-ahead
Khả năng phục hồi
- Graceful degradation: khi Redis gặp sự cố thì bỏ qua lớp đó và tự động khôi phục sau
- Circuit breaker
- Chính sách ghi Strict / best-effort
Khả năng quan sát
- Prometheus exporter, hook OpenTelemetry
- Đo độ trễ theo từng lớp, event hook
- Admin CLI (
npx layercache stats|keys|invalidate)
Tích hợp framework
Express, Fastify, Hono, tRPC, GraphQL, Next.js
Muốn xem các con số benchmark
Dựa trên VM một lõi + Docker Redis thực tế.
| Kịch bản | Độ trễ trung bình |
| L1 memory warm hit | 0.005 ms |
| L2 Redis warm hit (1 KiB) | 0.193 ms |
| Không có cache (mô phỏng DB) | 5.030 ms |
- HTTP throughput:
/layered16,211 req/s so với/nocache158 req/s - Stampede: 75 yêu cầu đồng thời → origin fetch 5 lần (nếu không có cache là 375 lần)
- Distributed single-flight: 60 yêu cầu đồng thời → origin fetch 1 lần
Toàn bộ phương pháp benchmark và kết quả raw đã được tổng hợp tại docs/benchmarking.md.
Nó khác gì so với các thư viện hiện có?
node-cache-manager, keyv, cacheable đều là những lựa chọn tốt. Tóm tắt ngắn gọn sự khác biệt như sau:
- Stampede prevention / Distributed single-flight: cả ba thư viện đều không cung cấp sẵn theo mặc định. layercache được thiết kế xoay quanh hai điểm này.
- Cross-instance L1 invalidation: tự động đồng bộ in-memory cache giữa các instance bằng Redis pub/sub. Có thể yên tâm dùng memory cache trong môi trường multi-instance.
- Auto backfill: tự động điền lại các lớp phía trên khi lớp bên dưới hit.
- Graceful degradation + Circuit breaker: ngay cả khi Redis chết, dịch vụ vẫn hoạt động.
Cài đặt và liên kết
npm install layercache
- GitHub: https://github.com/flyingsquirrel0419/layercache
- npm: https://www.npmjs.com/package/layercache
Nếu bạn có thắc mắc về các quyết định thiết kế, đặc biệt là cách điều phối single-flight hoặc cơ chế hoạt động của graceful degradation, cứ thoải mái đặt câu hỏi.
4 bình luận
Thư viện hay đấy!
Có lý do gì mà Redis được đưa vào trong thiết kế không? Có phải đang giả định tình huống nhiều instance chỉ đọc cùng chạy đồng thời không? Nếu vậy thì chẳng phải (local) Disk nên được đặt ở lớp phía trước Redis sao?
Redis được đưa vào là để giả định trường hợp có nhiều máy chủ. Vì bộ nhớ của mỗi máy chủ có thể chứa các giá trị khác nhau, nên Redis đóng vai trò là "nguồn chân lý dùng chung".
Disk được đặt sau Redis vì với giả định Redis nằm trong cùng mạng cục bộ thì nó nhanh hơn. Theo benchmark, Disk khoảng ~2ms, còn Redis là ~0.02ms. Tuy nhiên, nếu Redis ở xa hoặc mạng kém thì Disk cục bộ có thể nhanh hơn, và khi đó việc đảo lại thứ tự là hợp lý. Thư viện cũng không ép buộc thứ tự mà được thiết kế để người dùng tự quyết định.
Dù Disk nằm ở đâu đi nữa, mục đích chính của nó không phải là cạnh tranh tốc độ, mà là đóng vai trò như lớp bảo hiểm cuối cùng còn sống sót khi cả Memory và Redis đều chết.
Cảm ơn bạn đã chia sẻ ý đồ thiết kế. Ha.
Ý bạn là mọi lệnh gọi từ xa đều được lưu bằng thao tác ghi vào đĩa cục bộ, và khi lệnh gọi từ xa thất bại thì sẽ đọc từ đĩa đúng không? Có lẽ cũng nên cân nhắc xem liệu lớp cache có thực sự cần Disk hay không.
DiskLayer không phải kiểu mẫu đó mà đơn giản hoạt động như một lớp cache thông thường — vừa đọc vừa ghi, và khi miss ở lớp trên thì sẽ truy cập tuần tự xuống dưới. Tôi đã gây nhầm lẫn rồi.
Mẫu "lưu kết quả gọi từ xa xuống đĩa rồi đọc lại khi thất bại" mà bạn nói đến thực ra gần với tùy chọn stale-if-error hơn, nhưng cái đó được giữ trong bộ nhớ nên nếu tiến trình khởi động lại thì sẽ mất.
Còn về ý kiến cho rằng DiskLayer có thực sự cần thiết hay không thì, ừm. Trong thực tế, ở phần lớn môi trường đa instance, chỉ Memory → Redis là đã đủ, và ngay khi thêm Disk thành một lớp thì sẽ kéo theo chi phí tuần tự hóa và độ phức tạp trong quản lý tệp.