Học caching strategy từ cơ bản đến nâng cao: cache-aside, write-through, write-behind, cache invalidation, eviction policies (LRU/LFU), và multi-layer caching. Hiểu trade-off giữa performance và consistency trong distributed systems.
Chia sẻ bài học
Có một câu nói nổi tiếng trong system design:
"There are only two hard things in Computer Science: cache invalidation and naming things." - Phil Karlton
Caching nghe đơn giản: lưu data vào memory để đọc nhanh hơn. Nhưng khi implement thực tế, bạn sẽ gặp hàng tá câu hỏi khó:
Đây chính là lý do caching strategy quan trọng.
Lesson này dạy bạn tư duy caching như một architect - hiểu các pattern, biết khi nào dùng pattern nào, và quan trọng nhất: hiểu trade-off giữa performance và consistency.
Trước khi học strategy, phải hiểu tại sao caching lại hiệu quả đến vậy.
Dữ liệu được truy cập gần đây có khả năng cao sẽ được truy cập lại.
Ví dụ thực tế:
Thay vì query database 1 triệu lần, cache lần đầu → serve 999,999 lần từ memory.
RAM (cache): ~100 nanoseconds
SSD (database): ~100 microseconds (1000x chậm hơn)
HDD: ~10 milliseconds (100,000x chậm hơn)
Network (remote): ~100 milliseconds (1,000,000x chậm hơn)
Cache hit = 1000x - 1,000,000x nhanh hơn.
Đây là lý do tại sao caching có impact khổng lồ lên performance.
80% requests truy cập 20% data.
Chỉ cần cache 20% data hot, bạn đã giải quyết 80% traffic.
Mental model: Caching không phải cache tất cả. Là cache đúng data.
Đây là pattern phổ biến nhất và đơn giản nhất.
sequenceDiagram
participant App
participant Cache
participant DB
App->>Cache: 1. Check cache
alt Cache Hit
Cache-->>App: 2a. Return data
else Cache Miss
Cache-->>App: 2b. Cache miss
App->>DB: 3. Query database
DB-->>App: 4. Return data
App->>Cache: 5. Write to cache
App-->>App: 6. Return data
end
Step by step:
def get_user(user_id):
# 1. Check cache
cache_key = f"user:{user_id}"
user = redis.get(cache_key)
if user is not None:
# 2. Cache hit
return json.loads(user)
# 3. Cache miss - query database
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
# 4. Write to cache (TTL = 1 hour)
redis.setex(cache_key, 3600, json.dumps(user))
# 5. Return data
return user
Đơn giản và dễ implement
Cache chỉ chứa data được request
Resilient với cache failure
Cache miss penalty cao
Data có thể stale
Phù hợp với:
Ví dụ use case:
Write-through giải quyết vấn đề consistency của cache-aside.
sequenceDiagram
participant App
participant Cache
participant DB
Note over App,DB: READ Flow
App->>Cache: 1. Read from cache
alt Cache Hit
Cache-->>App: Return data
else Cache Miss
App->>DB: 2. Read from DB
DB-->>App: 3. Return data
App->>Cache: 4. Write to cache
end
Note over App,DB: WRITE Flow
App->>Cache: 1. Write to cache
Cache->>DB: 2. Write to database
DB-->>Cache: 3. Confirm write
Cache-->>App: 4. Confirm write
Key difference: Khi write data, application write vào cache → cache write vào database.
Cache luôn sync với database.
def update_user(user_id, data):
cache_key = f"user:{user_id}"
# 1. Write to cache first
redis.set(cache_key, json.dumps(data))
# 2. Cache writes to database
db.execute("UPDATE users SET ... WHERE id = ?", user_id)
# Cache and DB are now in sync
return data
Data consistency cao
Read performance tốt
Write latency cao
Write-heavy workload không hiệu quả
Cache failure = write failure
Phù hợp với:
Ví dụ use case:
Write-behind optimize write performance bằng cách write async.
sequenceDiagram
participant App
participant Cache
participant Queue
participant DB
Note over App,DB: WRITE Flow
App->>Cache: 1. Write to cache (fast)
Cache-->>App: 2. Return immediately
Cache->>Queue: 3. Queue write async
Note over Queue,DB: Background Process
Queue->>DB: 4. Batch write to DB
DB-->>Queue: 5. Confirm
Key difference: Application chỉ write vào cache → return ngay. Cache tự sync xuống database sau (async).
Write performance cực cao
Batch writes
Throughput cao
Risk mất data
Consistency phức tạp
Implementation phức tạp
Phù hợp với:
Ví dụ use case:
"Cache invalidation is one of the hardest problems in computer science."
Vấn đề: Làm sao biết khi nào cache đã stale và cần refresh?
Set expiration time cho mỗi cache entry.
# Cache expires after 1 hour
redis.setex("user:123", 3600, user_data)
Ưu điểm:
Nhược điểm:
Best practice:
ttl = 3600 + random.randint(0, 300) # 1h ± 5 min
Invalidate cache khi data thay đổi.
def update_user(user_id, data):
# Update database
db.execute("UPDATE users SET ... WHERE id = ?", user_id)
# Invalidate cache
redis.delete(f"user:{user_id}")
Ưu điểm:
Nhược điểm:
Dùng version number trong cache key.
def get_user(user_id):
version = get_user_version(user_id) # from DB or separate cache
cache_key = f"user:{user_id}:v{version}"
user = redis.get(cache_key)
if user is None:
user = db.query(...)
redis.set(cache_key, user)
return user
def update_user(user_id, data):
db.execute("UPDATE users SET ... WHERE id = ?", user_id)
increment_user_version(user_id) # Bump version
Ưu điểm:
Nhược điểm:
Thực tế, architect dùng combination:
Không có perfect solution. Pick trade-off phù hợp.
RAM có giới hạn. Khi cache đầy, phải xóa data cũ.
Câu hỏi: Xóa cache entry nào?
Xóa entry lâu nhất không được access.
Logic:
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return None
# Move to end (most recent)
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
# Remove least recent (first item)
self.cache.popitem(last=False)
Khi nào dùng:
Ví dụ: News site - Breaking news (recent) được read nhiều.
Xóa entry có frequency thấp nhất.
Logic:
class LFUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {}
self.freq = {} # Track access frequency
def get(self, key):
if key not in self.cache:
return None
self.freq[key] += 1
return self.cache[key]
def put(self, key, value):
if len(self.cache) >= self.capacity:
# Find least frequent key
min_key = min(self.freq, key=self.freq.get)
del self.cache[min_key]
del self.freq[min_key]
self.cache[key] = value
self.freq[key] = 1
Khi nào dùng:
Ví dụ: E-commerce - Sản phẩm bestseller luôn hot, không phụ thuộc temporal.
Xóa entry cũ nhất (theo thứ tự insert).
Đơn giản nhưng ít dùng vì không consider access pattern.
| Policy | Evict | Best For | Overhead |
|---|---|---|---|
| LRU | Lâu nhất không dùng | Temporal access | Medium |
| LFU | Ít được dùng nhất | Long-term popular | High |
| FIFO | Cũ nhất | Simple cases | Low |
Reality: Redis default dùng approximation LRU (fast + efficient).
Caching không chỉ một layer. Hệ thống lớn có nhiều cache layers.
Cache ở client browser.
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000
ETag: "abc123"
Ưu điểm:
Dùng cho:
Cache ở edge locations gần user.
User (Vietnam) → CDN (Singapore) → Origin (US)
Ưu điểm:
Dùng cho:
Cache ở application layer.
# Redis cache
user = redis.get(f"user:{user_id}")
Ưu điểm:
Dùng cho:
Cache ở database layer.
MySQL query cache, PostgreSQL shared buffers, etc.
Ưu điểm:
Dùng cho:
flowchart TD
User[User Request]
Browser[Browser Cache]
CDN[CDN Cache]
AppCache[Application Cache]
DB[Database]
User --> Browser
Browser -->|Miss| CDN
CDN -->|Miss| AppCache
AppCache -->|Miss| DB
DB -->|Store| AppCache
AppCache -->|Store| CDN
CDN -->|Store| Browser
Request flow:
Mỗi layer đều giảm load cho layer sau.
| Pattern | Write Perf | Read Perf | Consistency | Complexity |
|---|---|---|---|---|
| Cache-Aside | Fast | Good | Eventual | Low |
| Write-Through | Slow | Best | Strong | Medium |
| Write-Behind | Best | Best | Eventual | High |
Không có best pattern. Chỉ có right pattern cho use case.
Core insight của caching:
Bạn đang trade consistency để lấy speed.
Không có free lunch.
Architect giỏi biết:
Example decisions:
E-commerce product price:
Social media like count:
Blog posts:
1. Caching works vì locality of reference
80% traffic hit 20% data. Cache đúng 20% đó.
2. Ba pattern chính: Cache-Aside, Write-Through, Write-Behind
Mỗi pattern trade-off khác nhau. Pick based on use case.
3. Cache invalidation is hard
Combine TTL, event-based, version-based. Không có perfect solution.
4. Cache eviction: LRU cho temporal, LFU cho long-term popular
Redis default LRU works cho most cases.
5. Multi-layer caching: Browser → CDN → App → Database
Mỗi layer giảm load cho layer sau. Defense in depth.
6. Mental model: Trade consistency for speed
Hiểu trade-off, pick đúng level cho use case.
7. Cache là optimization, không phải requirement
Cache failure không được làm system fail. Always have fallback.
Remember: Great architects don't just add cache. They understand which data to cache, where to cache it, and what consistency guarantees they're giving up.