Cache-aside (lazy loading):
1. Đọc: GET key từ Redis. Hit → trả luôn. Miss → query DB, SET vào cache kèm TTL, rồi trả.
2. Ghi: cập nhật DB xong thì xóa (invalidate) key liên quan — delete chứ không update cache, để lần đọc sau tự nạp bản mới từ DB.
Các quyết định thiết kế:
- Đặt key theo quy tắc (product:{id}, product:{id}:reviews): khi sản phẩm đổi, biết chính xác phải xóa những key nào — invalidate sai/sót là bug khó thấy nhất của cache ("một trong hai thứ khó nhất trong CS").
- TTL luôn có (vd 5 phút): lưới an toàn khi lỡ quên invalidate; con số chọn theo độ tươi trang sản phẩm chấp nhận được, không phải càng dài càng tốt.
- Chỉ cache dữ liệu dùng chung: dữ liệu theo user (giỏ hàng, giá riêng) phải ở key riêng theo user hoặc không cache — trộn vào key chung là lộ dữ liệu chéo user.
Chống stampede cho key nóng — các phương án (single-flight, jitter, early/stale refresh) đã ở câu cache stampede; ở đây chốt combo cho trang sản phẩm: single-flight lock per key (một request rebuild, số còn lại chờ) + jitter cộng vào TTL (các key không hết hạn đồng loạt). Trang cực nóng (homepage, campaign) thì thêm refresh nền trước hạn.
let v = await redis.get(key)
if (v) return JSON.parse(v) // hit
return lock(key, async () => { // single-flight: one caller rebuilds
const again = await redis.get(key)
if (again) return JSON.parse(again)
const data = await db.load(id) // miss → load from DB
const ttl = 300 + Math.random() * 60 // jitter avoids synchronized expiry
await redis.set(key, JSON.stringify(data), "EX", ttl)
return data
})Lỗi hay gặp: cache dữ liệu người-dùng-riêng vào key chung; không có TTL; invalidate sót key gây hiển thị dữ liệu cũ.