Khi ứng dụng bắt đầu có nhiều người dùng, cơ sở dữ liệu thường là nơi "nghẽn cổ chai" đầu tiên. Cùng một trang sản phẩm được truy cập hàng nghìn lần mỗi phút, nhưng mỗi lần lại phải chạy lại những truy vấn giống hệt nhau. Caching, tức lưu tạm kết quả ở nơi truy xuất nhanh hơn, là cách đơn giản và hiệu quả để giảm tải. Redis, với tốc độ đọc ghi trong bộ nhớ và nhiều cấu trúc dữ liệu tiện lợi, là lựa chọn quen thuộc cho nhiệm vụ này. Tuy vậy, cache không chỉ là "lưu vào Redis rồi đọc ra". Chọn sai chiến lược có thể khiến người dùng nhìn thấy dữ liệu cũ hoặc làm hệ thống sập khi cache hết hạn đồng loạt.
Vì sao cần chiến lược caching?
Bất kỳ hệ thống cache nào cũng phải trả lời ba câu hỏi:
- Khi nào ghi dữ liệu vào cache? Lúc đọc hay lúc ghi?
- Làm sao giữ cache đồng bộ với nguồn dữ liệu gốc?
- Khi nào dữ liệu trong cache bị loại bỏ?
Các chiến lược dưới đây là những câu trả lời khác nhau cho ba câu hỏi đó.
Cache-aside (lazy loading)
Đây là chiến lược phổ biến nhất. Ứng dụng chủ động quản lý cache:
- Khi cần dữ liệu, ứng dụng đọc từ Redis trước.
- Nếu có (cache hit), trả về ngay.
- Nếu không có (cache miss), đọc từ database, ghi kết quả vào Redis kèm thời gian hết hạn, rồi trả về.
async function getProduct(id) {
const key = `product:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const product = await db.products.findById(id);
if (product) {
await redis.set(key, JSON.stringify(product), { EX: 300 });
}
return product;
}Ưu điểm: chỉ những dữ liệu thực sự được đọc mới nằm trong cache; nếu Redis gặp sự cố, ứng dụng vẫn có thể đọc thẳng từ database.
Nhược điểm: lần đọc đầu tiên luôn chậm, và dữ liệu có thể cũ cho tới khi hết hạn. Để giảm rủi ro dữ liệu cũ, khi cập nhật bản ghi bạn nên xoá key tương ứng (DEL product:42) thay vì cố ghi đè giá trị mới, vì cách xoá đơn giản hơn và ít gặp lỗi tranh chấp hơn.
Write-through
Với write-through, mỗi lần ghi dữ liệu, ứng dụng ghi đồng thời vào database và cache. Cache vì thế luôn có dữ liệu mới nhất.
- Ưu điểm: đọc gần như luôn trúng cache, dữ liệu ít bị lỗi thời.
- Nhược điểm: thao tác ghi chậm hơn vì phải ghi hai nơi; cache có thể chứa nhiều dữ liệu không bao giờ được đọc, gây lãng phí bộ nhớ.
Trên thực tế, write-through thường được kết hợp với cache-aside và TTL: write-through giữ dữ liệu "nóng" luôn mới, còn TTL dọn dẹp dữ liệu không dùng đến.
Write-behind (write-back)
Ứng dụng chỉ ghi vào cache, sau đó một tiến trình nền gom các thay đổi và ghi xuống database theo lô.
- Ưu điểm: tốc độ ghi rất cao, giảm tải mạnh cho database, phù hợp với dữ liệu như bộ đếm lượt xem.
- Nhược điểm: có nguy cơ mất dữ liệu nếu cache gặp sự cố trước khi kịp đồng bộ; logic phức tạp hơn đáng kể.
Chỉ nên dùng write-behind cho dữ liệu chấp nhận được việc mất mát nhỏ hoặc khi bạn có cơ chế bền vững bổ sung.
Read-through
Read-through giống cache-aside về hành vi, nhưng việc nạp dữ liệu từ database được giao cho chính lớp cache (thông qua thư viện hoặc dịch vụ trung gian) thay vì mã ứng dụng. Redis bản thân không tự đọc từ database của bạn, nên mô hình này thường đòi hỏi thêm một lớp thư viện bao quanh.
TTL: thời gian sống của dữ liệu
TTL (time to live) là "lưới an toàn" cho mọi chiến lược. Redis cho phép đặt thời gian hết hạn cho từng key bằng tuỳ chọn EX trong lệnh SET hoặc lệnh EXPIRE. Khi hết hạn, key tự động bị xoá.
SET session:abc123 "{...}" EX 1800
EXPIRE product:42 300
TTL product:42Chọn TTL bao lâu phụ thuộc vào mức độ chấp nhận dữ liệu cũ của từng loại dữ liệu: danh mục sản phẩm có thể để vài giờ, giá cổ phiếu có lẽ chỉ vài giây. Một mẹo hữu ích là thêm độ lệch ngẫu nhiên vào TTL (ví dụ 300 giây cộng thêm 0 đến 60 giây) để tránh việc hàng loạt key hết hạn cùng lúc.
Chính sách loại bỏ khi đầy bộ nhớ
Khi Redis chạm giới hạn maxmemory, nó cần biết loại bỏ key nào. Một số chính sách thường gặp:
allkeys-lru: loại bỏ key ít được dùng gần đây nhất trong toàn bộ key, phù hợp khi Redis chủ yếu dùng làm cache.volatile-lru: chỉ loại bỏ trong số key có đặt TTL.allkeys-lfu: ưu tiên loại bỏ key ít được truy cập nhất.noeviction: từ chối ghi mới khi đầy, phù hợp khi Redis lưu dữ liệu không được phép mất.
Những vấn đề kinh điển cần phòng tránh
- Cache stampede: một key "hot" hết hạn, hàng trăm request cùng lúc đổ xuống database. Giải pháp: dùng khoá phân tán để chỉ một request nạp lại dữ liệu, hoặc làm mới cache trước khi hết hạn.
- Cache penetration: request liên tục truy vấn key không tồn tại (có thể do tấn công), luôn bị miss. Giải pháp: cache cả kết quả rỗng với TTL ngắn, hoặc dùng Bloom filter.
- Dữ liệu không nhất quán: luôn có độ trễ giữa database và cache. Hãy xác định rõ dữ liệu nào cần chính xác tuyệt đối (số dư tài khoản) để không cache, hoặc cache rất ngắn.
- Key đặt tên lộn xộn: dùng quy ước thống nhất như
entity:id:fieldđể dễ quản lý và xoá theo nhóm.
Kết luận
Không có chiến lược caching nào tốt nhất cho mọi tình huống. Cache-aside kết hợp TTL là điểm khởi đầu an toàn cho phần lớn ứng dụng web. Write-through phù hợp khi bạn muốn dữ liệu đọc luôn mới, còn write-behind dành cho bài toán ghi cực nhiều và chấp nhận rủi ro. Quan trọng nhất là hiểu đặc thù dữ liệu của mình, đo tỷ lệ cache hit, và luôn chuẩn bị cho trường hợp cache trống hoặc Redis không khả dụng.






