Giao diện
🎯 Mục tiêu
🎯 Sau bài này bạn sẽ nắm được:
- Hệ thống phân tầng bộ nhớ và nguyên lý locality of reference
- 5 caching patterns: Cache-Aside, Read-Through, Write-Through, Write-Behind, Refresh-Ahead
- Eviction policies: LRU, LFU, TTL — khi nào dùng cái nào
- Redis vs Memcached — đánh đổi cụ thể
- Xử lý cache stampede, stale data, cache avalanche trong production
Caching — Nghệ thuật đánh đổi bộ nhớ lấy tốc độ
Tháng 3 năm ngoái, một đội product của chúng tôi triển khai tính năng "Sản phẩm nổi bật" trên trang chủ. Query SQL phía sau kéo JOIN ba bảng, trả về trong 120ms ở môi trường staging — chấp nhận được. Ngày launch, lượng traffic tăng gấp 5 lần bình thường. Database connection pool cạn sạch trong vòng 4 phút, latency p99 nhảy từ 120ms lên 8 giây, rồi cascading failure kéo sập cả hai service phụ thuộc. Mọi thứ ổn định lại chỉ sau khi đội on-call thêm một dòng Redis SETEX với TTL 60 giây vào endpoint đó. Latency về 2ms, database load giảm 97%.
Đây không phải chuyện hiếm. Caching là một trong những đòn bẩy hiệu năng mạnh nhất trong kỹ thuật phần mềm — nhưng cũng là nguồn gốc của những bug tinh vi nhất. Bài viết này trang bị cho bạn từ nguyên lý nền tảng đến những kịch bản thực chiến mà bạn sẽ gặp khi vận hành hệ thống ở quy mô thực.
Bức tranh tư duy
Hãy tưởng tượng bạn làm việc trong một văn phòng lớn. Bàn làm việc của bạn là cache — nhỏ, đắt tiền (không gian bàn có hạn), nhưng mọi thứ nằm trong tầm tay chỉ cần với tay là lấy được. Tủ hồ sơ phòng bên là RAM server — lớn hơn, mất vài bước chân để lấy. Kho lưu trữ tầng hầm là database — chứa mọi thứ, nhưng phải đi thang máy xuống, tìm đúng thùng, rồi mang lên.
Khi bạn nhận được một yêu cầu (request), bản năng đầu tiên là nhìn xuống bàn. Nếu tài liệu nằm sẵn đó — cache hit — bạn trả lời ngay lập tức. Nếu không — cache miss — bạn phải xuống kho, lấy tài liệu lên, trả lời xong rồi để một bản sao trên bàn cho lần sau. Vấn đề xuất hiện khi bàn đầy: bạn phải quyết định vứt tài liệu nào (eviction policy), và khi tài liệu gốc ở kho bị cập nhật thì bản sao trên bàn bạn đã cũ (stale data).
Toàn bộ lý thuyết caching xoay quanh ba câu hỏi: Lưu gì lên bàn? Lưu bao lâu? Và khi nào bản sao trên bàn không còn đúng nữa?
Cốt lõi kỹ thuật
Hệ thống phân tầng bộ nhớ
Mọi chiến lược caching đều dựa trên nguyên lý locality of reference: dữ liệu vừa được truy cập có xác suất cao sẽ được truy cập lại (temporal locality), và dữ liệu gần vùng vừa truy cập cũng có xác suất cao được dùng tiếp (spatial locality).
Bảng dưới đây dựa trên dữ liệu từ bài phân tích nổi tiếng "Latency Numbers Every Programmer Should Know" của Jeff Dean (Google):
| Tầng | Công nghệ | Latency | Dung lượng điển hình |
|---|---|---|---|
| L1 Cache | SRAM | ~1 ns | 64 KB |
| L2 Cache | SRAM | ~4 ns | 256 KB |
| L3 Cache | SRAM | ~10 ns | 8–32 MB |
| RAM | DRAM | ~100 ns | 16–128 GB |
| SSD | NAND Flash | ~100 μs | 1–4 TB |
| HDD | Magnetic disk | ~10 ms | 1–16 TB |
| Network (cùng datacenter) | TCP/IP | ~500 μs | — |
| Network (cross-region) | TCP/IP | ~50–150 ms | — |
Quy luật ngón tay cái: mỗi tầng chậm hơn tầng trên ~10–100×, nhưng dung lượng lớn hơn ~100–1000×. Caching là nghệ thuật đặt đúng dữ liệu vào đúng tầng.
Caching patterns
1. Cache-Aside (Lazy Loading)
Application tự quản lý cache. Khi đọc, kiểm tra cache trước — miss thì query database rồi ghi kết quả vào cache. Khi ghi, ghi thẳng vào database rồi xoá cache entry (invalidate, không phải update).
python
def get_user_profile(user_id: str) -> dict:
cache_key = f"user:{user_id}"
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
profile = db.execute(
"SELECT id, name, email, avatar FROM users WHERE id = %s",
(user_id,)
)
if profile:
redis.setex(cache_key, 3600, json.dumps(profile))
return profile
def update_user_profile(user_id: str, data: dict) -> None:
db.execute("UPDATE users SET name=%s WHERE id=%s", (data["name"], user_id))
redis.delete(f"user:{user_id}") # Invalidate, KHÔNG updateƯu điểm: đơn giản, chỉ cache dữ liệu thực sự được dùng, cache chết thì app vẫn chạy (fallback về DB). Nhược điểm: lần đầu luôn chậm (cold miss), app phải tự viết logic cache.
2. Read-Through
Giống Cache-Aside nhưng cache layer tự fetch dữ liệu khi miss — app chỉ cần gọi cache, không cần biết database. Thường được triển khai bằng cache provider có hỗ trợ loader function (ví dụ: Caffeine trong Java, hoặc cấu hình NCache).
3. Write-Through
Mỗi lần ghi, dữ liệu đi qua cache rồi cache đồng bộ ghi xuống database. Đảm bảo cache luôn nhất quán, nhưng write latency = cache write + DB write.
Dùng khi: dữ liệu cần consistency cao và read-heavy (session store, cấu hình hệ thống).
4. Write-Behind (Write-Back)
App ghi vào cache, cache bất đồng bộ flush xuống database theo batch. Write latency cực thấp (chỉ ghi cache), nhưng rủi ro mất dữ liệu nếu cache node chết trước khi flush.
Dùng khi: chấp nhận eventual consistency — page view counters, analytics events, activity logs. Tuyệt đối không dùng cho giao dịch tài chính.
5. Refresh-Ahead
Cache tự động refresh entry trước khi TTL hết hạn, dựa trên tần suất truy cập. Giảm cache miss cho hot keys, nhưng phức tạp hơn và tốn tài nguyên nếu dự đoán sai.
So sánh nhanh
| Pattern | Write latency | Read latency (hit) | Consistency | Độ phức tạp |
|---|---|---|---|---|
| Cache-Aside | Thấp (chỉ DB) | Rất thấp | Eventual | Thấp |
| Read-Through | Thấp | Rất thấp | Eventual | Trung bình |
| Write-Through | Cao (Cache + DB) | Rất thấp | Strong | Trung bình |
| Write-Behind | Rất thấp (chỉ cache) | Rất thấp | Eventual | Cao |
| Refresh-Ahead | Tuỳ backend | Rất thấp | Eventual | Cao |
Eviction policies
Khi cache đầy, hệ thống phải chọn entry nào để xoá:
| Policy | Cách hoạt động | Khi nào dùng |
|---|---|---|
| LRU (Least Recently Used) | Xoá entry lâu nhất chưa được truy cập | Mặc định tốt nhất cho đa số trường hợp |
| LFU (Least Frequently Used) | Xoá entry có tần suất truy cập thấp nhất | Khi phân biệt "hot" vs "warm" data quan trọng |
| TTL (Time-To-Live) | Tự hết hạn sau thời gian cố định | Dữ liệu có tính thời sự (giá cả, session) |
| FIFO | Xoá entry cũ nhất (theo thứ tự chèn) | Streaming data, message queue buffer |
| Random | Xoá ngẫu nhiên | Khi overhead tracking LRU quá cao |
Redis mặc định dùng approximated LRU (lấy mẫu ngẫu nhiên rồi chọn entry cũ nhất trong mẫu, tiết kiệm bộ nhớ hơn LRU chính xác). Từ Redis 4.0, có thêm LFU mode.
Redis vs Memcached
| Tiêu chí | Redis | Memcached |
|---|---|---|
| Cấu trúc dữ liệu | String, Hash, List, Set, Sorted Set, Stream, ... | Chỉ key-value (string) |
| Persistence | RDB snapshots + AOF | Không (thuần in-memory) |
| Replication | Master-replica built-in | Không native |
| Clustering | Redis Cluster (tự động sharding) | Client-side consistent hashing |
| Pub/Sub | Có | Không |
| Lua scripting | Có | Không |
| Multi-thread | I/O threads từ Redis 6.0, xử lý vẫn single-thread | Multi-thread native |
| Bộ nhớ mỗi key | Overhead cao hơn (~90 bytes/key) | Thấp hơn (~48 bytes/key) |
Chọn Redis khi cần cấu trúc dữ liệu phong phú, persistence, pub/sub, hoặc scripting. Chọn Memcached khi chỉ cần cache key-value đơn thuần, cần tối ưu bộ nhớ tuyệt đối, hoặc workload đa luồng rất cao.
Thực chiến
Kịch bản 1: Caching user session với Redis
Hệ thống có 500K daily active users, mỗi session chứa ~2 KB dữ liệu (user ID, roles, preferences). Không có cache, mỗi request phải query database để xác thực session — khoảng 15ms/query, tổng cộng ~50M session lookups/ngày.
Tính toán dung lượng:
500,000 concurrent sessions × 2 KB = ~1 GB RAM
Với replication factor 2 → cần ~2 GB RedisCấu hình Redis cho session store:
yaml
# redis.conf (production tuning)
maxmemory 2gb
maxmemory-policy allkeys-lru
save "" # Tắt RDB persistence cho session
appendonly no # Session mất thì user login lại
tcp-keepalive 300
timeout 0Code triển khai (Python + redis-py):
python
import redis
import json
import secrets
pool = redis.ConnectionPool(
host="redis-cluster.internal",
port=6379,
max_connections=50,
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=1,
retry_on_timeout=True,
)
r = redis.Redis(connection_pool=pool)
SESSION_TTL = 86400 # 24 giờ
def create_session(user_id: str, roles: list[str]) -> str:
session_id = secrets.token_urlsafe(32)
session_data = json.dumps({
"user_id": user_id,
"roles": roles,
"created_at": int(time.time()),
})
r.setex(f"session:{session_id}", SESSION_TTL, session_data)
return session_id
def get_session(session_id: str) -> dict | None:
data = r.get(f"session:{session_id}")
if not data:
return None
# Gia hạn TTL mỗi lần truy cập (sliding expiration)
r.expire(f"session:{session_id}", SESSION_TTL)
return json.loads(data)
def destroy_session(session_id: str) -> None:
r.delete(f"session:{session_id}")Kết quả: Latency session lookup giảm từ 15ms (DB) xuống ~0.5ms (Redis). Database connection pool giải phóng hoàn toàn khỏi session queries.
Kịch bản 2: CDN caching cho static assets
Hệ thống phục vụ 200 GB static assets (ảnh, CSS, JS bundles) cho users ở Việt Nam, Singapore, và Nhật Bản. Origin server đặt tại Singapore.
Cấu hình Cache-Control headers:
nginx
# nginx.conf tại origin server
location ~* \.(js|css|woff2|png|jpg|webp|avif)$ {
# Assets có content hash trong filename → cache lâu
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
}
location ~* \.(html)$ {
# HTML luôn revalidate để nhận version mới
add_header Cache-Control "no-cache, must-revalidate";
add_header ETag $upstream_http_etag;
}
location /api/ {
# API responses — cache ngắn hoặc không cache
add_header Cache-Control "private, max-age=0, no-store";
}Tính toán tiết kiệm bandwidth:
200 GB assets × 10M requests/ngày × cache hit ratio 95%
= 9.5M requests served từ CDN edge (không về origin)
= Tiết kiệm ~190 GB bandwidth/ngày từ origin
Latency user VN: 150ms (origin SG) → 5ms (edge HCM)Sai lầm điển hình
⚠️ Cạm bẫy
1. Cache Stampede (Thundering Herd) Hot key hết hạn → hàng nghìn request đồng thời miss → tất cả đổ về database cùng lúc → database quá tải.
Giải pháp: Dùng distributed lock (chỉ 1 request rebuild cache, còn lại chờ) hoặc probabilistic early refresh (làm mới sớm với xác suất tăng dần khi gần hết TTL).
python
# Distributed lock pattern với Redis
def get_with_lock(key: str) -> dict | None:
data = redis.get(key)
if data:
return json.loads(data)
lock_key = f"lock:{key}"
if redis.set(lock_key, "1", nx=True, ex=5):
try:
result = db.query(key)
redis.setex(key, 3600, json.dumps(result))
return result
finally:
redis.delete(lock_key)
else:
time.sleep(0.05)
return get_with_lock(key)⚠️ Cạm bẫy
2. Phục vụ stale data mà không biết Cache entry đã cũ nhưng TTL chưa hết → user nhìn thấy dữ liệu sai (giá cũ, tồn kho sai, thông tin profile cũ). Đặc biệt nguy hiểm với dữ liệu nhạy cảm như quyền truy cập hay số dư tài khoản.
Giải pháp: Kết hợp TTL ngắn + event-driven invalidation. Khi dữ liệu thay đổi, publish event để xoá cache ngay lập tức thay vì chờ hết TTL.
⚠️ Cạm bẫy
3. Cache quá mức (Over-caching) Cache mọi thứ "phòng hờ" → RAM cạn kiệt, eviction liên tục xoá cả hot data, hit ratio giảm thảm hại. Cache 1 triệu key nhưng chỉ 10,000 key được truy cập thường xuyên = lãng phí 99% bộ nhớ cache.
Giải pháp: Chỉ cache dữ liệu read-heavy (read:write ratio > 10:1). Đo lường hit ratio — nếu dưới 80%, xem lại chiến lược cache.
⚠️ Cạm bẫy
4. Không đặt TTL Key tồn tại vĩnh viễn → bộ nhớ đầy dần theo thời gian, dữ liệu ngày càng stale. Redis chỉ evict khi maxmemory đầy, lúc đó hiệu suất đã giảm rồi.
Giải pháp: Mọi cache key phải có TTL. Không ngoại lệ. Dùng TTL ngắn (phút) cho dữ liệu thay đổi thường xuyên, TTL dài (giờ) cho dữ liệu ổn định.
⚠️ Cạm bẫy
5. Cache key collision Hai loại dữ liệu khác nhau dùng chung key pattern → ghi đè lẫn nhau. Ví dụ: cache.set("user:123", profile_data) và cache.set("user:123", session_data) ở hai service khác nhau.
Giải pháp: Đặt prefix theo namespace rõ ràng: profile:user:123, session:user:123. Trong kiến trúc microservices, thêm tên service: auth:session:user:123.
Under the Hood
Cache hit ratio — con số quan trọng nhất
Hit ratio là tỷ lệ request được phục vụ từ cache so với tổng request:
Hit Ratio = cache_hits / (cache_hits + cache_misses)Benchmark thực tế: Hầu hết hệ thống production nhắm đến hit ratio > 95% cho hot data. Facebook từng công bố Memcached cluster của họ đạt hit ratio ~99% cho social graph queries (nguồn: paper "Scaling Memcache at Facebook", NSDI 2013). Nếu hit ratio dưới 80%, bạn đang lãng phí tài nguyên cache và cần xem lại chiến lược.
Ước tính hiệu quả:
Giả sử:
- Database query latency: 15ms
- Cache lookup latency: 0.5ms
- Hit ratio: 95%
Latency trung bình = 0.95 × 0.5ms + 0.05 × 15ms = 0.475 + 0.75 = 1.225ms
So với không cache: 15ms → cải thiện 12× latency trung bìnhZipf distribution — tại sao caching hiệu quả
Trong hầu hết hệ thống thực tế, truy cập dữ liệu tuân theo phân phối Zipf (hay còn gọi power law): một phần nhỏ dữ liệu chiếm phần lớn lượng truy cập. Quy luật 80/20 là gần đúng phổ biến — 20% keys chiếm 80% traffic.
Điều này giải thích tại sao chỉ cần cache một lượng nhỏ bộ nhớ (vài GB) cũng đủ phục vụ phần lớn request. Nếu dữ liệu truy cập phân bố đều (uniform distribution), caching sẽ kém hiệu quả hơn nhiều vì không có "hot keys" để giữ trong cache.
Memory vs Latency — bài toán đánh đổi
Ví dụ capacity planning:
- 10M users, mỗi profile ~1 KB
- Cache tất cả: 10 GB RAM → hit ratio ~100%, latency ~0.5ms
- Cache top 10%: 1 GB RAM → hit ratio ~90% (Zipf), latency trung bình ~2ms
- Cache top 1%: 100 MB RAM → hit ratio ~60%, latency trung bình ~6ms
Chi phí Redis RAM (AWS ElastiCache): ~$0.068/GB/giờ
- 10 GB = ~$490/tháng
- 1 GB = ~$49/tháng
→ Với hit ratio 90% ở 1/10 chi phí, phương án 1 GB thường là sweet spotConsistency models trong caching
Strong consistency: Mọi read sau write luôn trả về giá trị mới nhất. Đạt được bằng Write-Through pattern hoặc invalidation đồng bộ. Đánh đổi: write latency cao hơn.
Eventual consistency: Sau khi write, cache có thể trả về giá trị cũ trong một khoảng thời gian (bounded bởi TTL). Đa số hệ thống web chấp nhận eventual consistency với TTL vài giây đến vài phút.
Cache invalidation — "bài toán khó thứ hai trong khoa học máy tính"
Phil Karlton nổi tiếng nói: "Chỉ có hai thứ khó trong khoa học máy tính: cache invalidation và đặt tên."
Ba chiến lược invalidation:
| Chiến lược | Cách hoạt động | Trade-off |
|---|---|---|
| TTL-based | Tự hết hạn sau thời gian cố định | Đơn giản, nhưng có cửa sổ stale data |
| Event-driven | Publish event khi data thay đổi → subscriber xoá cache | Real-time hơn, nhưng thêm độ phức tạp hạ tầng |
| Version-based | Gắn version vào cache key (user:123:v5) | Không bao giờ stale, nhưng cần tracking version |
Trong thực tế, hầu hết hệ thống kết hợp TTL + event-driven: TTL làm safety net (dù event bị mất, cache vẫn tự hết hạn), event-driven đảm bảo invalidation nhanh cho hot path.
✅ Checklist triển khai
Checklist ghi nhớ
Thiết kế
- [ ] Xác định read:write ratio trước khi chọn caching pattern — cache chỉ hiệu quả khi read >> write
- [ ] Chọn đúng tầng cache: in-process (Caffeine, Guava), distributed (Redis, Memcached), CDN (CloudFront, Cloudflare)
- [ ] Mỗi cache key phải có TTL — không ngoại lệ
- [ ] Đặt cache key với namespace rõ ràng:
{service}:{entity}:{id}— tránh collision
Vận hành
- [ ] Monitor cache hit ratio liên tục — alert khi dưới 80%
- [ ] Đặt
maxmemoryvàmaxmemory-policycho Redis — không để Redis dùng hết RAM server - [ ] Cấu hình connection pooling — tránh tạo/huỷ connection liên tục
- [ ] Thêm jitter vào TTL khi cache nhiều key cùng lúc — phòng cache avalanche
Phòng thủ
- [ ] Triển khai distributed lock hoặc request coalescing cho hot keys — phòng thundering herd
- [ ] Cache negative results (NULL marker với TTL ngắn) — chặn cache penetration
- [ ] Chuẩn bị fallback khi cache node chết — app phải hoạt động được (chậm hơn) mà không có cache
- [ ] Ghi log cache miss ratio theo endpoint — phát hiện endpoint nào cần tối ưu
Invalidation
- [ ] Kết hợp TTL + event-driven invalidation — TTL là safety net, event là fast path
- [ ] Invalidate (xoá) cache khi write, không update — tránh race condition giữa concurrent writes
- [ ] Test kịch bản cache node restart — đảm bảo hệ thống warm-up lại êm
Bài tập luyện tập
Bài 1: Thiết kế cache layer cho hệ thống e-commerce
Đề bài: Một sàn e-commerce có 1 triệu sản phẩm, 100K concurrent users, mỗi trang sản phẩm trung bình 5 KB. Top 5% sản phẩm chiếm 80% traffic. Thiết kế cache layer gồm:
- Chọn caching pattern phù hợp và giải thích tại sao
- Tính dung lượng Redis cần thiết
- Chọn eviction policy
- Xử lý invalidation khi seller cập nhật giá sản phẩm
Lời giải tham khảo
1. Pattern: Cache-Aside — phù hợp vì read-heavy (hàng triệu lượt xem / vài trăm lần update giá mỗi ngày), app đã có sẵn database logic, cache failure không làm sập hệ thống.
2. Dung lượng:
Top 5% sản phẩm = 50,000 sản phẩm × 5 KB = 250 MB
Thêm overhead Redis (~100 bytes/key): 50,000 × 100 = 5 MB
Tổng: ~255 MB → cấp phát 512 MB Redis (dư cho growth)3. Eviction policy: LFU — vì muốn giữ sản phẩm hot (truy cập nhiều), loại sản phẩm ít ai xem. LRU cũng chấp nhận được nhưng LFU chính xác hơn cho pattern này.
4. Invalidation:
- Khi seller cập nhật giá → publish event
product:price_updated:{product_id} - Consumer nhận event →
redis.delete(f"product:{product_id}") - TTL = 300 giây làm safety net (nếu event bị mất, tối đa 5 phút dữ liệu cũ)
- Với sản phẩm flash sale, giảm TTL xuống 30 giây
Bài 2: Tính hit ratio
🧠 Quiz
Hệ thống ghi nhận trong 1 giờ: 950,000 cache hits và 50,000 cache misses. Mỗi cache miss tốn 20ms (DB query), mỗi hit tốn 1ms. Hỏi:
a) Hit ratio là bao nhiêu?
- [ ] 90%
- [x] 95%
- [ ] 99%
b) Nếu không có cache, latency trung bình sẽ là bao nhiêu?
- [x] 20ms (mọi request đều query DB)
- [ ] 10ms
- [ ] 1ms
c) Với cache, latency trung bình là bao nhiêu?
- [ ] 1ms
- [x] 1.95ms
- [ ] 5ms
Giải thích chi tiết
a) Hit ratio = 950,000 / (950,000 + 50,000) = 950,000 / 1,000,000 = 95%
b) Không có cache → mọi request đều đi DB → 20ms
c) Latency trung bình = (0.95 × 1ms) + (0.05 × 20ms) = 0.95 + 1.0 = 1.95ms
→ Cache giảm latency trung bình 10.3× (từ 20ms xuống 1.95ms)
Bài 3: Debug cache stampede
Đề bài: Production alert lúc 2 giờ sáng — database CPU nhảy lên 100% mỗi đúng 1 giờ. Sau khi kiểm tra, bạn phát hiện cache key homepage:featured_products có TTL đúng 3600 giây, và endpoint homepage có ~5,000 rps.
- Giải thích tại sao DB CPU spike đúng mỗi 1 giờ
- Viết code fix bằng hai phương pháp khác nhau
Lời giải tham khảo
1. Nguyên nhân: TTL = 3600s → key hết hạn mỗi đúng 1 giờ. Với 5,000 rps, trong khoảnh khắc key expire, hàng nghìn request đồng thời miss cache và đổ về DB cùng lúc (thundering herd).
2. Fix phương pháp 1 — Probabilistic early refresh:
python
import random, time
def get_featured_products() -> list:
key = "homepage:featured_products"
data = redis.get(key)
remaining_ttl = redis.ttl(key)
if data and remaining_ttl > 0:
# Khi còn < 10% TTL, refresh sớm với xác suất tăng dần
if remaining_ttl < 360:
probability = 1.0 - (remaining_ttl / 360)
if random.random() < probability:
_refresh_featured_async(key)
return json.loads(data)
return _fetch_and_cache_featured(key)Fix phương pháp 2 — Request coalescing (singleflight):
python
import threading
_locks: dict[str, threading.Event] = {}
_results: dict[str, str] = {}
def get_featured_coalesced() -> list:
key = "homepage:featured_products"
data = redis.get(key)
if data:
return json.loads(data)
if key in _locks:
_locks[key].wait(timeout=5)
return json.loads(redis.get(key) or "[]")
_locks[key] = threading.Event()
try:
result = db.query("SELECT ... FROM products WHERE featured = true")
redis.setex(key, 3600, json.dumps(result))
return result
finally:
_locks[key].set()
del _locks[key]📺 Caselet: Netflix — Caching video metadata cho 260 triệu subscribers
Netflix phục vụ hơn 260 triệu subscribers trên toàn cầu. Mỗi lần một người dùng mở app, họ duyệt qua hàng chục title — mỗi title kéo theo metadata gồm tên phim, mô tả, thumbnail, đánh giá, danh sách diễn viên, và trailer link. Với tỷ lệ read:write vào khoảng 1000:1 (metadata phim ít khi thay đổi, nhưng được đọc hàng triệu lần mỗi giây), caching không phải "nice to have" mà là xương sống của toàn bộ hệ thống serving.
Kiến trúc cache của Netflix xoay quanh EVCache — một hệ thống caching phân tán do Netflix tự phát triển dựa trên Memcached. EVCache lưu trữ movie metadata, user preferences, và viewing history trên nhiều AWS region đồng thời. Chiến lược TTL được phân tầng rõ ràng: metadata phim (title, description, cast) có TTL 24 giờ vì dữ liệu này hiếm khi thay đổi, trong khi ratings và trending data có TTL chỉ 5 phút để phản ánh sự thay đổi liên tục. Khi nội dung mới ra mắt — ví dụ một mùa mới của Squid Game — Netflix thực hiện cache warming: dữ liệu được pre-populate vào cache ở tất cả các region trước khi traffic thực sự đổ về, đảm bảo không có cold start nào khi hàng triệu người đồng loạt truy cập.
Tuy nhiên, chính những lần ra mắt lớn này tạo ra bài toán thundering herd kinh điển. Hình dung: mùa mới drop lúc 0:00 UTC → hàng triệu request đồng thời truy vấn cùng một title → tất cả đều cache miss (vì nội dung mới, chưa có trong cache) → toàn bộ đổ về database cùng lúc → database bị crush. Netflix giải quyết bằng hai kỹ thuật: Probabilistic early recomputation (cache được refresh sớm hơn TTL thực tế, với jitter ngẫu nhiên để tránh đồng loạt expire) và request coalescing (khi nhiều request cùng truy vấn một key đang miss, chỉ một request duy nhất query database, các request còn lại đợi và nhận kết quả từ request đầu tiên).
Một câu hỏi phỏng vấn phổ biến: Tại sao Netflix chọn Memcached (EVCache) thay vì Redis? Câu trả lời nằm ở use case. EVCache (Memcached-based) được chọn cho simple key-value caching vì nó đơn giản hơn, nhanh hơn cho pure caching workload, và dễ horizontal scale. Tuy nhiên, Netflix vẫn sử dụng Redis cho những nơi cần data structures phức tạp: sorted sets cho recommendation engine (xếp hạng phim theo relevance score), pub/sub cho real-time notification, và streams cho event processing. Đây không phải quyết định either/or — mà là right tool for the right job.
Trade-off cốt lõi mà Netflix phải cân nhắc mỗi ngày là cache consistency vs freshness. Khi rating của một bộ phim thay đổi, liệu tất cả 260 triệu users có cần thấy con số mới ngay lập tức? Câu trả lời: Không. Eventual consistency với TTL 5 phút là hoàn toàn chấp nhận được cho ratings — không ai thiệt hại gì khi thấy 4.7★ thay vì 4.8★ trong vài phút. Nhưng câu hỏi "bộ phim này có available ở quốc gia của bạn không?" thì bắt buộc phải consistent — vì liên quan đến licensing và pháp lý. Hiển thị một title không có bản quyền ở một region có thể dẫn đến vi phạm hợp đồng hàng triệu đô.
Bài học cho architect
Cache strategy phải phân biệt được "dữ liệu nào chấp nhận stale" và "dữ liệu nào phải luôn fresh". Đây là câu hỏi business, không phải kỹ thuật. Luôn hỏi product team trước khi quyết định TTL.
🏗️ Thực hành: Đặt đúng cache vào đúng tầng
Một ứng dụng e-commerce có kiến trúc microservices. Hãy đặt loại cache phù hợp vào từng vị trí trong hệ thống.
Các loại cache có sẵn:
- 🌐 CDN Cache (CloudFront/Cloudflare)
- 💻 Browser Cache (Service Worker + Cache API)
- ⚡ Application Cache — Redis (data structures, pub/sub)
- ⚡ Application Cache — Memcached (simple key-value, high throughput)
- 🗄️ Database Query Cache (MySQL query cache / PG materialized views)
- 📦 Object Cache (serialized objects in memory)
Các vị trí cần cache:
| # | Vị trí | Dữ liệu | Yêu cầu | Cache nào? |
|---|---|---|---|---|
| 1 | Product images & CSS/JS | Static assets 500MB+ | Global, low latency | ??? |
| 2 | Product listing page HTML | Rendered HTML, thay đổi 1x/giờ | Fast first paint | ??? |
| 3 | User session data | Session tokens, cart items | Fast read, structured data | ??? |
| 4 | Product catalog | 50K products, read-heavy | Simple key-value, high throughput | ??? |
| 5 | Search results | Complex queries, expensive computation | Reusable across users | ??? |
| 6 | User recommendations | Sorted by relevance score | Need sorted sets | ??? |
| 7 | Price data | Real-time pricing, frequent updates | Low TTL, consistent | ??? |
| 8 | Product reviews | User-generated, moderate write | Materialized, periodic refresh | ??? |
📋 Đáp án tham khảo
| # | Vị trí | Cache phù hợp | Giải thích |
|---|---|---|---|
| 1 | Static assets | CDN Cache | Assets không thay đổi thường xuyên, cần serve globally với low latency. Cache-Control: max-age=31536000, immutable |
| 2 | Product listing HTML | Browser Cache + CDN | Service Worker cache HTML shell, CDN cache full page. stale-while-revalidate pattern |
| 3 | User session | Redis | Cần data structures (hash cho session data), TTL tự động, distributed across servers |
| 4 | Product catalog | Memcached | Pure key-value lookup, read-heavy, không cần data structures phức tạp. Cache-aside pattern |
| 5 | Search results | Redis hoặc Memcached | Cache key = hash(query params). Redis nếu cần sort/filter cached results; Memcached nếu chỉ cache raw results |
| 6 | Recommendations | Redis | Cần sorted sets (ZRANGEBYSCORE) để lấy top-N recommendations theo score. Memcached không support |
| 7 | Price data | Redis (low TTL) | TTL 30-60 giây. Dùng pub/sub để invalidate khi price thay đổi. Consistency quan trọng hơn performance |
| 8 | Reviews | Database Query Cache | Materialized views refresh mỗi 15 phút. Write frequency trung bình → cache ở DB layer hiệu quả hơn |
Nguyên tắc chung:
- CDN: Static, globally distributed, rarely changes
- Browser: User-specific, reduce server round-trips
- Memcached: Simple key-value, maximum throughput, stateless
- Redis: Need data structures, pub/sub, or complex operations
- DB Cache: Heavy queries, moderate write frequency