Trong các hệ thống có lượng truy cập lớn, Redis thường đóng vai trò là “tấm khiên” bảo vệ cơ sở dữ liệu chính (PostgreSQL, MySQL, MongoDB). Mỗi khi có request, backend sẽ kiểm tra Redis trước; nếu có (Cache Hit) thì trả về ngay lập tức, nếu không (Cache Miss) mới truy vấn DB và ghi ngược lại vào Redis.
Mọi thứ vận hành hoàn hảo cho đến một ngày hệ thống của bạn đột ngột bị sập nguồn khi Redis vẫn đang hoạt động bình thường. Thủ phạm rất có thể là: Cache Avalanche (Hiện tượng tuyết lở).
1. Hiện tượng Cache Avalanche là gì?
Hiện tượng này xảy ra khi hàng nghìn hoặc hàng triệu key trong Cache hết hạn (expire) cùng một thời điểm.
- Kịch bản điển hình: Bạn chạy một cronjob lúc 00:00 hoặc hệ thống vừa khởi động lại sau bảo trì. Bạn nạp trước (warm-up) 100.000 sản phẩm hot vào Redis và đặt TTL (Time-To-Live) đồng loạt là 1 giờ (
TTL = 3600s). - Hậu quả:
- Đúng 01:00, 100.000 key này đồng loạt biến mất khỏi Redis.
- Ngay giây tiếp theo, hàng chục nghìn request ập đến. Tất cả đều rơi vào trạng thái Cache Miss.
- Toàn bộ lượng traffic khổng lồ này xuyên thẳng qua tầng cache và đâm trực tiếp vào Database. Database lập tức cạn kiệt Connection Pool, CPU chạm ngưỡng 100%, và toàn bộ hệ thống sập theo hiệu ứng domino.
2. Giải pháp đơn giản mà hiệu quả: Thêm Random Jitter
Cách khắc phục kinh điển và dễ triển khai nhất không phải là dựng thêm cụm server, mà là làm lệch thời gian hết hạn của các key để chúng không bao giờ chết cùng một giây. Kỹ thuật này gọi là Jitter (phân tán ngẫu nhiên).
Thay vì gán một con số TTL cố định:
JavaScript
// RỦI RO CAO: Tất cả key đều hết hạn đúng sau 60 phút
const TTL = 60 * 60; // 3600s
await redis.set(`product:${id}`, JSON.stringify(data), 'EX', TTL);
Hãy cộng thêm một khoảng thời gian ngẫu nhiên nhỏ (ví dụ dao động từ 10% đến 20%):
JavaScript
// AN TOÀN: Phân tán thời gian hết hạn ngẫu nhiên
function getTTLWithJitter(baseTTL = 3600, jitterPercentage = 0.2) {
const maxJitter = baseTTL * jitterPercentage;
const randomOffset = Math.floor(Math.random() * maxJitter);
return baseTTL + randomOffset; // Sẽ dao động từ 3600s đến 4320s
}
const safeTTL = getTTLWithJitter(3600); // Mỗi item sẽ có TTL hơi lệch nhau một chút
await redis.set(`product:${id}`, JSON.stringify(data), 'EX', safeTTL);
Chỉ với phép tính ngẫu nhiên này:
- Các key sẽ rải rác hết hạn lần lượt qua từng phút thay vì chết đồng loạt.
- Database chỉ cần phục vụ một vài request Cache Miss rải rác để làm mới dữ liệu, tải trọng luôn giữ ở mức phẳng (flat line) ổn định.
3. Hai tấm khiên phụ trợ khác bạn nên biết
Ngoài Jitter, để bảo vệ hệ thống triệt để ở cấp độ lớn hơn:
- Khóa phân tán (Mutex Lock): Khi phát hiện Cache Miss, chỉ cho phép 1 request duy nhất lấy lock để query DB và ghi đè cache; các request khác tạm thời chờ vài mili-giây để đọc lại cache mới tạo thay vì cùng ùa vào DB.
- Logic Expiration (Soft TTL): Lưu thêm trường
expireAtbên trong payload JSON. Khi phát hiện sắp hết hạn, trả về dữ liệu cũ trước cho người dùng và kích hoạt một background job âm thầm làm mới cache mà không chặn request.
Tối ưu hóa hệ thống phân tán đôi khi không nằm ở các thuật toán cao siêu, mà ở sự thấu hiểu hành vi của dữ liệu theo thời gian. Một dòng Math.random() đơn giản vào biến TTL hôm nay có thể cứu hệ sinh thái của bạn khỏi một cuộc khủng hoảng sập mạng vào lúc nửa đêm!


