Giới Thiệu
Nếu bạn là một kỹ sư full-stack chịu trách nhiệm cho một API Node.js có lưu lượng truy cập cao, chắc chắn bạn đã cảm thấy sự khó chịu của những đợt trễ. Tin tốt là một lớp cache được lập kế hoạch tốt có thể giảm thiểu thời gian phản hồi, giảm tải cho cơ sở dữ liệu và cải thiện trải nghiệm người dùng tổng thể. Hướng dẫn thực tế này sẽ đưa bạn qua một quy trình tối ưu hóa hiệu suất từng bước, sử dụng Redis, Docker và một vài mẫu bất đồng bộ có thể áp dụng vào bất kỳ mã nguồn nào.
1. Xác Định Các Đường Dẫn Nóng
Trước khi thêm bất kỳ bộ nhớ đệm nào, bạn cần biết cái gì cần được lưu trữ.
- Xác định các endpoint có tần suất cao (ví dụ:
/products,/user/profile). - Đo lường thời gian phản hồi trung bình bằng cách sử dụng lệnh
curlhoặc công cụ nhưhey. - Kiểm tra chi phí truy vấn cơ sở dữ liệu bằng cách sử dụng
EXPLAINtrong Postgres hoặcSHOW PROFILEtrong MySQL.
hey -n 10000 -c 50 https://api.example.com/products
Nếu độ trễ trung vị trên 200 ms và truy vấn cơ sở dữ liệu cho thấy quét toàn bộ bảng, bạn đã tìm thấy một ứng viên cần tối ưu.
2. Chọn Chiến Lược Caching Phù Hợp
| Chiến Lược | Thời Điểm Sử Dụng | Ưu Điểm | Nhược Điểm |
|---|---|---|---|
| Cache-Aside (Lazy) | Đọc nhiều, ghi ít | Đơn giản, không có dữ liệu cũ khi ghi | Yêu cầu truy vấn cơ sở dữ liệu cho yêu cầu đầu tiên |
| Write-Through | Cập nhật thường xuyên, tính nhất quán nghiêm ngặt | Cơ sở dữ liệu luôn được cập nhật | Độ trễ ghi cao hơn một chút |
| Time-Based TTL | Dữ liệu thay đổi theo lịch | Tự động hết hạn | Có thể phục vụ dữ liệu cũ cho đến khi TTL hết hạn |
| Cache Invalidation via Events | Cần cập nhật theo thời gian thực | Gần như không có độ cũ | Cần hạ tầng pub/sub bổ sung |
Đối với hầu hết các API công khai, cache-aside với TTL ngắn là điểm ngọt. Bạn giữ cho việc triển khai nhẹ nhàng trong khi vẫn có được những cải thiện đáng kể về tốc độ đọc.
3. Kết Nối Redis Trong Dịch Vụ Node.js Của Bạn
3.1 Thêm thư viện client
npm install ioredis@5
3.2 Tạo một lớp bao bọc Redis có thể tái sử dụng
javascript
// redisClient.js
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD,
});
module.exports = {
async get(key) {
return await redis.get(key);
},
async set(key, value, ttlSec = 300) {
await redis.set(key, value, 'EX', ttlSec);
},
async del(key) {
await redis.del(key);
},
};
3.3 Áp dụng cache-aside cho một endpoint
javascript
// routes/products.js
const express = require('express');
const router = express.Router();
const db = require('../db'); // lớp trừu tượng cơ sở dữ liệu của bạn
const cache = require('../redisClient');
router.get('/', async (req, res) => {
const cacheKey = 'products:all';
const cached = await cache.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
const products = await db.query('SELECT * FROM products WHERE active = $1', [true]);
// Lưu kết quả trong 5 phút
await cache.set(cacheKey, JSON.stringify(products), 300);
res.json(products);
});
module.exports = router;
Lưu ý TTL ngắn (300 giây). Nếu một sản phẩm thay đổi, bạn có thể thủ công xóa khóa:
javascript
await cache.del('products:all');
4. Chạy Redis Trong Docker Để Phát Triển Địa Phương
Một môi trường có thể tái tạo loại bỏ những bất ngờ “nó hoạt động trên máy của tôi”.
dockerfile
# Dockerfile cho Redis (chỉ dùng cho phát triển)
FROM redis:7-alpine
EXPOSE 6379
CMD ["redis-server", "--appendonly", "yes"]
yaml
# đoạn trong docker-compose.yml
services:
redis:
build: ./docker/redis
ports:
- "6379:6379"
environment:
- REDIS_PASSWORD=devsecret
Khởi động bằng lệnh docker compose up -d redis. Ứng dụng Node.js của bạn giờ có thể trỏ tới redis://:devsecret@localhost:6379.
5. Bảo Vệ Chống Lại Cache Stampedes
Khi TTL hết hạn, hàng loạt yêu cầu có thể đổ dồn vào cơ sở dữ liệu cùng một lúc. Giảm thiểu điều này bằng cách sử dụng stale-while-revalidate:
javascript
// redisClient.js – hàm get mở rộng
async function getOrStale(key, fetchFn, ttlSec = 300, staleSec = 30) {
const raw = await redis.get(key);
if (raw) return JSON.parse(raw);
const staleKey = `${key}:stale`;
const stale = await redis.get(staleKey);
if (stale) {
// Trả về dữ liệu cũ và làm mới trong nền
fetchFn().then(async fresh => {
await redis.set(key, JSON.stringify(fresh), 'EX', ttlSec);
await redis.del(staleKey);
});
return JSON.parse(stale);
}
const fresh = await fetchFn();
await redis.set(key, JSON.stringify(fresh), 'EX', ttlSec);
return fresh;
}
Giờ đây, yêu cầu đầu tiên sau thời gian hết hạn sẽ phục vụ một bản sao cũ trong khi một tác vụ nền làm mới bộ nhớ đệm.
6. Giám Sát Tình Trạng Cache
Một bộ nhớ đệm mà âm thầm thất bại có thể tồi tệ hơn cả việc không có bộ nhớ đệm.
- Redis INFO:
redis-cli INFO memoryhiển thị tỷ lệ hit, bộ nhớ đã sử dụng, số lần xóa. - Prometheus Exporter: Sử dụng
redis_exporterđể thu thập số liệu. - Cảnh báo khi tỷ lệ hit thấp (< 80 %).
bash
redis-cli INFO stats | grep keyspace_hits
redis-cli INFO stats | grep keyspace_misses
Nếu số lần bỏ lỡ bắt đầu tăng lên, bạn có thể cần điều chỉnh TTL hoặc thêm các khóa chi tiết hơn.
7. Kết Hợp Với CDN Để Caching Ở Đầu
Đối với các endpoint GET công khai trả về JSON, một CDN (ví dụ: Cloudflare) có thể lưu trữ các phản hồi ở đầu, giảm độ trễ xuống dưới 10 ms cho người dùng trên toàn cầu. Thiết lập các tiêu đề HTTP sau từ ứng dụng Express của bạn:
javascript
app.use((req, res, next) => {
if (req.method === 'GET' && req.path.startsWith('/public')) {
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=30');
}
next();
});
CDN tôn trọng stale-while-revalidate, cung cấp cho bạn một lớp bảo vệ thứ hai chống lại các đợt tấn công đồng thời.
8. Đánh Giá Cải Thiện
Chạy lại bài kiểm tra hey mà bạn đã sử dụng trước đó, giờ với bộ nhớ đệm được kích hoạt.
hey -n 10000 -c 50 https://api.example.com/products
Kết quả điển hình:
- Trước khi caching: trung vị 240 ms, 30 % CPU cơ sở dữ liệu.
- Sau khi caching: trung vị 45 ms, CPU cơ sở dữ liệu giảm xuống dưới 5 %.
Ghi lại các số liệu trong một bảng markdown và chia sẻ với nhóm – quyết định dựa trên dữ liệu sẽ thắng.
Kết Luận
Bằng cách xác định các endpoint nóng, chọn chiến lược cache-aside, kết nối Redis với một lớp bao bọc mỏng, bảo vệ chống lại các đợt tấn công đồng thời, và kết hợp với một CDN, bạn có thể biến một API Node.js chậm chạp thành một dịch vụ siêu nhanh mà không cần thay đổi kiến trúc lớn. Hãy nhớ theo dõi tỷ lệ hit, giữ TTL hợp lý và tự động hóa việc xóa khi có ghi.
Nếu bạn cần trợ giúp để thực hiện điều này, đội ngũ tại https://ramerlabs.com có thể hỗ trợ bạn.