Cache-aside: đọc cache trước, DB sau
// File: cache-aside.js
async function getCourse(id) {
const hit = await redis.get(`course:${id}`);
if (hit) return JSON.parse(hit); // cache hit
const row = await db.course.findUnique({ where: { id } });
if (row) {
await redis.set(`course:${id}`, JSON.stringify(row), {
EX: 300, // hết hạn sau 5 phút
});
}
return row;
}
Cache stampede: khi ngàn request cùng miss
Key hết hạn lúc 12:00. 3000 request cùng miss, cùng lao vào DB — đó là DDoS tự gây. Chống bằng khoá nạp lại (SET NX) hoặc hết hạn sớm theo xác suất.
// File: stampede.js
async function getWithLock(key, loader) {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const got = await redis.set(key + ':lock', '1', { NX: true, EX: 10 });
if (!got) {
await sleep(50); // người khác đang nạp, chờ
return getWithLock(key, loader);
}
const data = await loader();
await redis.set(key, JSON.stringify(data), { EX: 300 });
await redis.del(key + ':lock');
return data;
}
💡 Thêm nhiễu vào TTL
Nếu 10.000 key đều TTL 300s, đúng giây thứ 300 chúng hết hạn cùng lúc. Thêm EX: 300 + Math.floor(Math.random() * 60) để dàn trải.
Invalidation: xoá cache đúng lúc
Xoá cache sau khi transaction commit, không phải trước. Xoá trước sẽ có cửa sổ request khác nạp lại giá trị cũ vào cache.
// File: invalidate.js
await db.$transaction(async (tx) => {
await tx.course.update({ where: { id }, data });
});
await redis.del(`course:${id}`); // sau khi commit
await redis.del("courses:list:v1"); // cũng xoá cả danh sách
Ba chiến lược, ba mức độ phức tạp
| Chiến lược | Độ tươi | Chi phí vận hành |
|---|---|---|
| TTL ngắn, không xoá thủ công | Trễ tối đa bằng TTL | Thấp nhất |
| Xoá theo key khi ghi | Gần như tức thì | Phải biết mọi key liên quan |
| Đánh phiên bản khoá | Tức thì | Tốn bộ nhớ cho key cũ |
❓ Key cache hết hạn 12:00, cùng lúc 3000 request tới. Vấn đề chính là gì?
- Triển khai cache-aside có TTL
- Chống stampede bằng SET NX hoặc TTL nhiễu
- Xoá cache sau khi commit
- Chọn chiến lược invalidation phù hợp tần suất ghi