Trong kiến trúc phần mềm hiện đại, Caching là vũ khí tối thượng để giảm độ trễ và tăng khả năng mở rộng. Tuy nhiên, việc giữ cho dữ liệu giữa Cache và Database luôn đồng bộ là một thách thức lớn. Có hai chiến lược phổ biến nhất để giải quyết vấn đề này là Cache-aside và Write-through.
Bài viết này sẽ đi sâu vào cơ chế, ưu nhược điểm và mã nguồn minh họa cho từng loại.

1. Cache-aside (Lazy Loading) - "Lười biếng" nhưng hiệu quả
Cache-aside là chiến lược mà ứng dụng đóng vai trò điều phối. Dữ liệu chỉ được đưa vào Cache khi có yêu cầu truy vấn thực tế.
Quy trình hoạt động:
-
Đọc: Ứng dụng tìm trong Cache. Nếu thấy (Hit), trả về ngay. Nếu không (Miss), đọc từ DB -> Lưu vào Cache -> Trả về.
-
Ghi: Ứng dụng cập nhật Database trực tiếp, sau đó xóa (Invalidate) key đó trong Cache để buộc lần đọc sau phải lấy dữ liệu mới từ DB.
Code minh họa (Node.js & Redis):
async function getUser(userId) {
// 1. Kiểm tra trong Cache
let user = await redis.get(`user:${userId}`);
if (user) {
console.log("Cache Hit!");
return JSON.parse(user);
}
// 2. Cache Miss - Đọc từ Database
console.log("Cache Miss! Reading from DB...");
user = await db.users.findUnique({ where: { id: userId } });
// 3. Ghi vào Cache cho lần sau (kèm TTL)
if (user) {
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
}
return user;
}
-
Ưu điểm: Kháng lỗi tốt (nếu Cache sập, DB vẫn gánh được), tiết kiệm bộ nhớ.
-
Nhược điểm: Dữ liệu có thể bị cũ nếu logic xóa cache khi cập nhật DB bị thất bại.
2. Write-through - "Đồng nhất" tuyệt đối
Với Write-through, lớp Cache đóng vai trò là "nguồn sự thật" duy nhất cho ứng dụng. Mọi thao tác ghi đều phải đi qua Cache, và Cache có trách nhiệm đồng bộ sang Database.
Quy trình hoạt động:
-
Ghi: Ứng dụng ghi dữ liệu vào Cache. Lớp Cache/Proxy sẽ ghi đồng bộ vào Database. Chỉ khi cả hai thành công, thao tác mới hoàn tất.
-
Đọc: Dữ liệu hầu như luôn có sẵn trong Cache vì mọi thay đổi đều đã được cập nhật tại đó.
Code minh họa (Logic lớp trung gian):
class UserService {
async updateUser(userId, data) {
// Ghi đồng thời vào cả 2 nơi (hoặc qua một lớp Provider)
const updatedUser = await db.transaction(async (tx) => {
const user = await tx.users.update({ where: { id: userId }, data });
await redis.set(`user:${userId}`, JSON.stringify(user));
return user;
});
return updatedUser;
}
}
-
Ưu điểm: Dữ liệu luôn mới, cực kỳ phù hợp cho các hệ thống cần tính chính xác cao như giao dịch tài chính.
-
Nhược điểm: Ghi dữ liệu chậm hơn do phải chờ cả hai hệ thống phản hồi.
3. So sánh chi tiết: Chọn "Lười" hay chọn "Kỹ"?
| Đặc điểm | Cache-aside | Write-through |
| Tính nhất quán | Có thể bị lệch (Eventual Consistency) | Rất cao (Strong Consistency) |
| Hiệu năng ghi | Nhanh (Ghi DB xong là xong) | Chậm (Phải đợi cả DB và Cache) |
| Hiệu năng đọc | Chậm ở lần đầu (Cache Miss) | Luôn nhanh (Luôn có sẵn trong Cache) |
| Độ phức tạp | Thấp, dễ triển khai | Cao, cần đảm bảo tính nguyên tử (Atomicity) |
4. Khi nào nên dùng loại nào?
-
Chọn Cache-aside khi: * Hệ thống có lượng truy vấn đọc gấp nhiều lần ghi.
-
Dữ liệu không yêu cầu tính chính xác tuyệt đối ngay lập tức (ví dụ: số lượt like, thông tin profile cá nhân).
-
Bạn muốn hệ thống vẫn hoạt động ổn định dù Redis bị sập.
-
-
Chọn Write-through khi:
-
Tính chính xác của dữ liệu là ưu tiên hàng đầu (ví dụ: số dư tài khoản, trạng thái đơn hàng).
-
Dữ liệu được cập nhật liên tục và tần suất đọc cũng rất cao.
-
5. Những lưu ý "sống còn" khi dùng Cache
-
Thiết lập TTL (Time-to-live): Luôn có thời hạn hết hạn cho Cache để tránh dữ liệu rác tồn tại vĩnh viễn.
-
Xử lý Cache Invalidation: Trong Cache-aside, hãy ưu tiên việc Xóa Cache thay vì Cập nhật Cache khi ghi dữ liệu để tránh Race Condition.
-
Tránh Cache Stampede: Sử dụng cơ chế khóa (locking) nếu có quá nhiều request cùng đổ xô vào DB khi Cache bị hết hạn cùng lúc.
Kết luận
Không có chiến lược nào là hoàn hảo, chỉ có chiến lược phù hợp nhất với bài toán kinh doanh. Hi vọng bài viết này giúp bạn có cái nhìn rõ nét hơn để đưa ra quyết định đúng đắn cho hệ thống của mình.







