Chiến lược Caching Hiệu quả Trong Ứng Dụng Thực Tế
Bài viết phân tích các chiến lược caching phổ biến như cache bộ nhớ trong tiến trình, Redis, stale-while-revalidate cùng các kỹ thuật invalidate cache để giúp tối ưu hiệu suất ứng dụng và giảm tải cho cơ sở dữ liệu.

Chiến lược Caching Hiệu quả Trong Ứng Dụng Thực Tế
Caching là một công cụ quan trọng giúp cải thiện hiệu năng của ứng dụng bằng cách lưu trữ tạm thời dữ liệu truy xuất nhiều lần. Tuy nhiên không phải loại cache nào cũng phù hợp và chọn nhầm phương pháp cache có thể dẫn đến hiệu quả ngược lại, thậm chí gây lỗi khó hiểu.
Bài viết này sẽ giới thiệu các chiến lược caching phổ biến được ứng dụng trong thực tế, từ cache tích hợp trong tiến trình Node.js đến Redis cache, và mô hình stale-while-revalidate, kèm theo các cách invalidation để giữ cache luôn chính xác.
Phân loại các kiểu Cache
Không phải cache nào cũng giống nhau, việc lựa chọn sai loại cache có thể còn tệ hơn không dùng cache.
1. In-Process Memory Cache (Cache bộ nhớ trong tiến trình)
Đây là cache nhanh nhất vì chạy ngay trong tiến trình ứng dụng (ví dụ Node.js), không có truy vấn mạng. Thích hợp cho dữ liệu cấu hình hoặc tính toán theo từng instance.
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 300, checkperiod: 60, maxKeys: 1000 });
async function getUser(userId: string): Promise<User> {
const cacheKey = `user:${userId}`;
const cached = cache.get<User>(cacheKey);
if (cached) return cached;
const user = await db.users.findUnique({ where: { id: userId } });
cache.set(cacheKey, user);
return user;
}
- Ưu điểm: Rất nhanh, không mất chi phí mạng.
- Hạn chế: Chỉ dùng cho ứng dụng chạy đơn instance, không đồng bộ cache được giữa nhiều instance. Không phù hợp với bộ dữ liệu lớn.
2. Redis Cache
Redis là bộ nhớ cache chia sẻ giữa các instance của ứng dụng, tồn tại độc lập và thường không bị xóa khi deploy.
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
async function getUser(userId: string): Promise<User> {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.users.findUnique({ where: { id: userId } });
await redis.setEx(cacheKey, 300, JSON.stringify(user));
return user;
}
async function updateUser(userId: string, data: Partial<User>) {
const updated = await db.users.update({ where: { id: userId }, data });
await redis.del(`user:${userId}`); // xóa cache liên quan khi dữ liệu thay đổi
return updated;
}
Redis phù hợp cho hệ thống quy mô lớn, đa instance, cần chia sẻ cache và khả năng tồn tại lâu dài.
3. Stale-While-Revalidate (SWR)
Mô hình này giúp phục vụ dữ liệu cũ (stale) nhanh chóng để giảm thời gian chờ, đồng thời cập nhật dữ liệu mới trong nền.
async function getWithSWR<T>(
key: string,
fetcher: () => Promise<T>,
staleTTL: number,
freshTTL: number
): Promise<T> {
const cached = await redis.hGetAll(key);
if (cached.data) {
const age = Date.now() - parseInt(cached.timestamp);
const data = JSON.parse(cached.data) as T;
if (age < freshTTL * 1000) {
return data; // dữ liệu tươi, trả ngay
}
if (age < staleTTL * 1000) {
// trả dữ liệu stale, đồng thời refresh ngầm
setImmediate(async () => {
const fresh = await fetcher();
await redis.hSet(key, {
data: JSON.stringify(fresh),
timestamp: Date.now().toString(),
});
await redis.expire(key, staleTTL);
});
return data;
}
}
// cache miss hoặc quá cũ, lấy dữ liệu mới và cache lại
const fresh = await fetcher();
await redis.hSet(key, {
data: JSON.stringify(fresh),
timestamp: Date.now().toString(),
});
await redis.expire(key, staleTTL);
return fresh;
}
SWR rất hữu ích với các dữ liệu ít thay đổi nhưng cần phục vụ nhanh, như bảng xếp hạng, danh sách sản phẩm phổ biến.
4. Cache-Aside và Read-Through
- Cache-Aside: Ứng dụng chủ động đọc ghi cache. Ưu điểm linh hoạt, chỉ cache khi cần.
- Read-Through: Cache tự động lấy dữ liệu từ nguồn khi cache miss, giảm boilerplate code.
// Cache-Aside
let data = await cache.get(key);
if (!data) {
data = await db.query(...);
await cache.set(key, data, ttl);
}
return data;
// Read-Through
const readthrough = new ReadThroughCache({
async fetch(key: string) {
return db.query(extractId(key));
},
ttl: 300,
});
const data = await readthrough.get(key);
Cache-aside thường được dùng khi phát triển theo yêu cầu đặc thù, trong khi read-through giúp giảm tải cho dev.
5. Chiến lược Invalid Cache (Xóa cache)
- TTL (Time To Live): Cache tự động hết hạn sau một thời gian.
- Event-based: Xóa cache ngay khi dữ liệu gốc bị thay đổi.
- Version-based: Đưa version vào key để dễ dàng invalidate theo phiên bản.
6. Thiết kế Cache Key
Xây dựng key cache chi tiết, granular sẽ giúp tránh cache dữ liệu quá rộng, ảnh hưởng đến hiệu quả.
Ví dụ:
- Tốt:
user:123,user:123:orders - Xấu:
users(chứa tất cả user)
Phân phối key hợp lý trong Redis Cluster bằng cách dùng hash tags cũng quan trọng để tránh hotspot.
Những sự thật cần nhớ về caching
Caching không phải là phép màu hay fix lỗi hiệu năng đơn giản. Nó cũng có thể tạo ra nhiều lỗi nan giải:
- Dữ liệu lỗi thời, gây khó hiểu cho người dùng.
- Việc cập nhật dữ liệu bị trì hoãn do delay invalidate cache.
Vài nguyên tắc cần tuân thủ:
- Chỉ cache đọc, không cache ghi trực tiếp.
- Luôn đặt TTL, không cache dữ liệu vô thời hạn.
- Invalidate cache thật mạnh tay ngay khi dữ liệu thay đổi.
- Theo dõi tỷ lệ cache hit, nếu < 80% thường do thiết kế key không tốt.
- Nên cache ở tầng gần data layer, tránh can thiệp vào business logic.
Cache là công cụ giúp tối ưu hiệu năng, không phải là bản sửa lỗi. Cần sử dụng một cách có chủ đích và hiểu rõ.
Nguồn mở rộng: Bộ starter kit AI SaaS tích hợp Redis caching, SWR pattern và logic invalidate cache: Whoff Agents AI SaaS Starter Kit
Bài viết liên quan

Phần mềm
Ra mắt Rail: Ngôn ngữ lập trình tự hosting tích hợp HTTPS thuần túy
18 tháng 4, 2026

Phần mềm
Tương lai "Headless" cho AI cá nhân: Khi giao diện dòng lệnh lên ngôi
18 tháng 4, 2026

Công nghệ
Cursor đàm phán huy động hơn 2 tỷ USD với định giá 50 tỷ USD khi tăng trưởng doanh nghiệp bùng nổ
17 tháng 4, 2026
