Caching trong System Design — không chỉ là chuyện 'lưu tạm'
Caching là một trong những khái niệm đầu tiên mà bất kỳ backend developer nào cũng học đến. Nhưng giữa "biết" và "dùng đúng" là cả một khoảng cách. Có những hệ thống cần cache để chạy nhanh, có những hệ thống bị chết vì cache sai chỗ. Bài này sẽ điểm qua một vài điều cơ bản về caching dưới góc nhìn system design.
Ảnh: Brett Sayles — Pexels
Nhiều bạn mới làm backend thường nghĩ cache đơn giản là lưu cái gì đó xuống Redis cho nhanh, rồi quên đi. Nhưng càng design nhiều hệ thống, càng thấy việc chọn đúng pattern cache — và quan trọng hơn, biết khi nào không nên cache — mới là thứ phân biệt senior với junior.
Cache Aside (Lazy Loading) — pattern phổ biến nhất
Đây là pattern mà đa số dev quen mặt nhất. Application tự quản lý cache:
Ưu điểm: Dễ implement, chỉ cache những gì thực sự được request. Cache không chiếm dung lượng vô ích.
Nhược điểm: Cache miss đầu tiên luôn chậm. Nếu có nhiều cache miss cùng lúc (ví dụ app vừa restart), database có thể bị sập vì request đổ dồn — còn gọi là thundering herd problem.
Read Through — cache tự lo
Cũng giống Cache Aside, nhưng cache layer (ví dụ Redis, Memcached) là người chủ động load dữ liệu từ database khi miss. Application không cần biết database tồn tại.
Pattern này giúp code sạch hơn, nhưng đẩy trách nhiệm xuống cache layer — nếu cache không hỗ trợ sẵn, bạn lại phải tự implement logic read-through.
Write Through vs Write Behind
Write Through: Mỗi lần ghi dữ liệu, ghi vào database và cache cùng lúc. Cache luôn consistent với database, nhưng latency tăng vì mỗi write phải chờ cả 2.
Write Behind (Write Back): Ghi vào database trước hoặc sau cache — hoặc gom nhiều write rồi flush một lần. Nhanh hơn nhưng rủi ro mất dữ liệu nếu cache crash trước khi flush xong.
Cả 2 pattern này đều không phổ biến bằng Cache Aside trong thực tế, nhưng rất hữu ích ở những chỗ cần write throughput cao (ví dụ: hệ thống analytics, logging).
Ảnh: panumas nikhomkhai — Pexels
Cache Invalidation — nỗi đau muôn thuở
Có 2 câu nói đùa trong giới system design: "There are only two hard things in Computer Science: cache invalidation and naming things." — và nó đùa thật đấy.
Vấn đề là: khi data trong database thay đổi, làm sao cache biết để cập nhật? Có mấy cách:
- TTL (Time To Live): Đơn giản nhất. Cache tự expire sau N giây. Dễ implement nhưng dữ liệu có thể stale trong khoảng thời gian đó.
- Write-through kết hợp eviction: Khi data ghi vào DB, chủ động update hoặc xoá cache entry. Consistent hơn nhưng tăng độ phức tạp.
- Pub/Sub: Database publish sự kiện thay đổi, cache subscriber nhận và invalidate tương ứng. Pattern mạnh nhưng cần infrastructure hỗ trợ.
Không có cách nào hoàn hảo — tuỳ use case mà chọn. Với dữ liệu ít thay đổi (user profile, config), TTL vài phút là ổn. Với dữ liệu nhạy cảm (số dư tài khoản), cần invalidate ngay lập tức.
Khi nào không nên cache?
Nghe hơi ngược, nhưng cache không phải lúc nào cũng tốt:
- Dữ liệu thay đổi quá nhanh: Cache không kịp expire trước khi data đã thay đổi → liên tục miss → cache vô dụng.
- Cache lớn hơn dữ liệu gốc: Nếu DB query đã nhanh (dưới 5ms) và tần suất đọc thấp, thêm cache chỉ tăng độ trễ và độ phức tạp.
- Cache miss gây hậu quả nặng: Như hệ thống real-time trading — stale cache có thể gây tổn thất tài chính.
Nguyên tắc chung: chỉ cache khi read nhiều hơn write, và dữ liệu không yêu cầu consistency tuyệt đối.
Cache là công cụ, không phải mục đích. Hiểu pattern và trade-off của từng loại sẽ giúp bạn thiết kế hệ thống vừa nhanh vừa đáng tin cậy. Có những hệ thống dev vô tội vạ cache lung tung — cuối cùng performance không cải thiện, mà còn thêm vấn đề về data consistency.
Caching và Rate Limiting là 2 chủ đề mà backend developer nào cũng nên nằm lòng. Trên blog đã có bài về Rate Limiting, hôm nay là về Cache — kỳ tới chắc sẽ viết về Load Balancer hoặc Database Sharding nếu có dịp.
📋 Phụ lục thuật ngữ
- Cache Aside (Lazy Loading) — pattern cache phổ biến nhất: application tự kiểm tra cache trước, nếu miss thì query DB và ghi vào cache
- Thundering herd problem — hiện tượng nhiều request cache miss đồng thời làm quá tải database
- Cache Invalidation — quá trình xoá hoặc cập nhật dữ liệu trong cache khi dữ liệu gốc thay đổi