Cache Strategies với Redis: Từ Lý Thuyết Đến Thực Hành
Bạn có biết?
Một website thương mại điện tử trung bình xử lý hàng nghìn request mỗi giây. Khi mỗi request đều query database, server sẽ quá tải và response time tăng vọt. Cache là giải pháp — lưu kết quả tạm thời trong Redis, giúp giảm 80-90% load lên database và tăng tốc độ phản hồi gấp 10 lần.
Trong bài viết này, chúng ta sẽ đi sâu vào các chiến lược cache phổ biến nhất, cách triển khai chúng bằng Redis, và cách xử lý các vấn đề như Cache Avalanche và Cache Stampede.
Tổng quan về Cache trong hệ thống phân tán
Cache là bộ nhớ tạm thời nằm giữa ứng dụng và nguồn dữ liệu gốc (thường là database). Thay vì truy vấn database mỗi lần, ứng dụng kiểm tra cache trước — nếu có dữ liệu (cache hit), trả về ngay; nếu không (cache miss), query database rồi lưu vào cache cho lần sau.
Redis là lựa chọn hàng đầu cho caching nhờ tốc độ đọc/ghi cực nhanh (sub-millisecond), hỗ trợ nhiều cấu trúc dữ liệu, và có built-in TTL (Time To Live) để tự động xóa dữ liệu hết hạn.
Các Cache Patterns phổ biến
1. Cache-Aside (Lazy Loading)
Đây là pattern phổ biến nhất. Ứng dụng chịu trách nhiệm quản lý cache hoàn toàn. Khi đọc dữ liệu, ứng dụng kiểm tra cache trước; nếu miss, query database rồi lưu vào cache.
Ưu điểm: Đơn giản, linh hoạt, cache chỉ chứa dữ liệu thực sự được truy cập.
Nhược điểm: Cache miss lần đầu gây tăng latency, có nguy cơ dữ liệu cache và database không đồng bộ.
# Cache-Aside Pattern - Python Implementation
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0)
def get_user(user_id):
cache_key = f"user:{user_id}"
# Bước 1: Kiểm tra cache
cached = r.get(cache_key)
if cached:
print(f"[HIT] Lấy user {user_id} từ cache")
return json.loads(cached)
# Bước 2: Cache miss - truy vấn database
print(f"[MISS] Truy vấn database cho user {user_id}")
user_data = query_database(f"SELECT * FROM users WHERE id = {user_id}")
# Bước 3: Lưu vào cache với TTL 300 giây
r.setex(cache_key, 300, json.dumps(user_data))
return user_data
def update_user(user_id, data):
# Bước 1: Cập nhật database
execute_query(f"UPDATE users SET ... WHERE id = {user_id}")
# Bước 2: Xóa cache (invalidate)
r.delete(f"user:{user_id}")
print(f"[INVALIDATE] Đã xóa cache user:{user_id}")
2. Write-Through
Với Write-Through, mỗi khi ghi dữ liệu, ứng dụng ghi đồng thời vào cả cache và database. Điều này đảm bảo cache luôn nhất quán với database.
Ưu điểm: Dữ liệu cache luôn nhất quán, đọc cực nhanh sau lần đầu.
Nhược điểm: Ghi chậm hơn vì phải ghi vào hai nơi, có thể lưu trữ dữ liệu không bao giờ được đọc (wasted cache).
# Write-Through Pattern - Python Implementation
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0)
def write_through(key, data, ttl=300):
# Ghi vào database
save_to_database(key, data)
# Ghi đồng thời vào cache
r.setex(key, ttl, json.dumps(data))
print(f"[WRITE-THROUGH] Đã ghi {key} vào cả DB và cache")
def get_data(key):
# Luôn đọc từ cache (vì cache luôn có dữ liệu mới nhất)
cached = r.get(key)
if cached:
return json.loads(cached)
# Fallback nếu cache bị xóa
return get_from_database(key)
3. Write-Behind (Write-Back)
Ứng dụng ghi vào cache trước, sau đó cache sẽ ghi xuống database bất đồng bộ (asynchronous). Tốc độ ghi cực nhanh vì không phải chờ database.
Ưu điểm: Ghi cực nhanh, giảm load lên database, có thể gom nhiều thao tác ghi thành batch.
Nhược điểm: Nguy cơ mất dữ liệu nếu cache crash trước khi ghi xuống database, phức tạp hơn khi triển khai.
# Write-Behind Pattern - Python Implementation
import redis
import json
import threading
import time
r = redis.Redis(host='localhost', port=6379, db=0)
def write_behind(key, data, ttl=300):
# Ghi vào cache ngay lập tức
r.setex(key, ttl, json.dumps(data))
# Đánh dấu cần sync xuống database
r.sadd("dirty_keys", key)
print(f"[WRITE-BEHIND] Đã ghi {key} vào cache, chờ sync")
def background_sync():
"""Chạy trong background thread để sync xuống database"""
while True:
dirty_keys = r.smembers("dirty_keys")
for key in dirty_keys:
data = r.get(key)
if data:
save_to_database(key, json.loads(data))
r.srem("dirty_keys", key)
print(f"[SYNC] Đã sync {key} xuống database")
time.sleep(5) # Sync mỗi 5 giây
4. Read-Through
Tương tự Cache-Aside nhưng cache tự chịu trách nhiệm load dữ liệu từ database khi miss. Ứng dụng chỉ cần gọi cache, không cần biết database ở đâu.
Ưu điểm: Ứng dụng đơn giản hơn, cache quản lý toàn bộ logic load dữ liệu.
Nhược điểm: Cache phức tạp hơn, cần implement callback function để load từ database.
# Read-Through Pattern - Python Implementation
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0)
def read_through(key, loader_func, ttl=300):
"""
loader_func: function để load dữ liệu từ database khi cache miss
"""
# Kiểm tra cache
cached = r.get(key)
if cached:
return json.loads(cached)
# Cache miss — gọi loader để lấy từ database
data = loader_func(key)
if data:
r.setex(key, ttl, json.dumps(data))
return data
# Sử dụng
def load_user_from_db(user_id):
return query_database(f"SELECT * FROM users WHERE id = {user_id}")
user = read_through("user:42", load_user_from_db, ttl=300)
Cache Invalidation Strategies
TTL-based Invalidation
Cách đơn giản nhất — đặt thời gian sống cho mỗi key. Hết hạn, Redis tự động xóa.
# Đặt TTL khác nhau cho từng loại dữ liệu
r.setex("user:42", 300, user_data) # 5 phút
r.setex("product:100", 3600, product_data) # 1 giờ
r.setex("config:site", 86400, config_data) # 24 giờ
# Kiểm tra TTL còn lại
ttl = r.ttl("user:42") # Trả về số giây còn lại, -1 = không có TTL, -2 = key không tồn tại
Event-driven Invalidation
Khi dữ liệu thay đổi, xóa cache ngay lập tức. Đảm bảo tính nhất quán cao nhất.
def update_product(product_id, data):
# 1. Cập nhật database
save_to_database(f"product:{product_id}", data)
# 2. Xóa cache ngay lập tức
r.delete(f"product:{product_id}")
print(f"[INVALIDATE] Đã xóa cache product:{product_id}")
# 3. (Tùy chọn) Pre-warm cache với dữ liệu mới
r.setex(f"product:{product_id}", 3600, json.dumps(data))
Xử lý Cache Avalanche và Cache Stampede
Cache Avalanche
Vấn đề: Nhiều key hết hạn cùng lúc → tất cả request đều cache miss → database bị tấn công đồng loạt.
Giải pháp: Thêm random jitter vào TTL để các key không hết hạn cùng lúc.
import random
def set_with_jitter(key, data, base_ttl=300):
"""Thêm random jitter 0-60 giây vào TTL"""
jitter = random.randint(0, 60)
actual_ttl = base_ttl + jitter
r.setex(key, actual_ttl, json.dumps(data))
print(f"[CACHE] {key} TTL = {actual_ttl}s (base={base_ttl} + jitter={jitter})")
# Thay vì: r.setex(key, 300, data) → tất cả hết hạn cùng lúc
# Dùng: set_with_jitter(key, data, 300) → hết hạn lệch nhau 0-60s
Cache Stampede
Vấn đề: Nhiều request cùng lúc cache miss → tất cả cùng query database → database quá tải.
Giải pháp: Mutex/Lock — chỉ 1 request query DB, các request khác chờ.
import redis
import time
import json
r = redis.Redis(host='localhost', port=6379, db=0)
def get_with_lock(key, loader_func, ttl=300):
# Kiểm tra cache
cached = r.get(key)
if cached:
return json.loads(cached)
# Cache miss — thử lấy lock
lock_key = f"lock:{key}"
acquired = r.set(lock_key, "1", nx=True, ex=10) # Lock 10 giây
if acquired:
# Đây là request "may mắn" được query DB
try:
data = loader_func(key)
r.setex(key, ttl, json.dumps(data))
return data
finally:
r.delete(lock_key)
else:
# Các request khác chờ một chút rồi thử lại
time.sleep(0.1)
cached = r.get(key)
if cached:
return json.loads(cached)
# Nếu vẫn miss, query DB trực tiếp (fallback)
return loader_func(key)
Graceful Degradation
Nếu Redis down, ứng dụng fallback query database trực tiếp thay vì crash.
def safe_get(key, loader_func):
try:
cached = r.get(key)
if cached:
return json.loads(cached)
except redis.ConnectionError:
print("[WARNING] Redis không khả dụng, fallback DB")
# Fallback: query database trực tiếp
return loader_func(key)
So sánh các Cache Patterns
| Tiêu chí | Cache-Aside | Write-Through | Write-Behind | Read-Through |
|---|---|---|---|---|
| Độ phức tạp | Thấp | Trung bình | Cao | Trung bình |
| Tốc độ đọc | Nhanh | Rất nhanh | Rất nhanh | Nhanh |
| Tốc độ ghi | Nhanh | Chậm | Rất nhanh | Nhanh |
| Nhất quán | Có thể lỗi thời | Cao | Trung bình | Cao |
| Mất dữ liệu | Không | Không | Có thể | Không |
| Use case | Đọc nhiều | Cần nhất quán | Ghi nhiều | Đơn giản hóa |

Best Practices
- Đặt TTL hợp lý — Không bao giờ cache forever. Dữ liệu thường xuyên thay đổi → TTL ngắn (5-10 phút). Dữ liệu ít thay đổi → TTL dài (1-24 giờ).
- Cache key naming convention — Dùng format rõ ràng:
object_type:id:field(VD:user:42:profile). - Không cache quá nhiều — Chỉ cache dữ liệu được truy cập thường xuyên. Cache everything sẽ lãng phí bộ nhớ.
- Monitor cache hit rate — Tỷ lệ hit > 90% là tốt. Nếu thấp, xem xét lại TTL hoặc key strategy.
- Handle cache failure gracefully — Luôn có fallback khi Redis down. Không để cache crash kéo theo toàn bộ hệ thống.
- Pre-warm cache — Khi deploy, load dữ liệu quan trọng vào cache trước để tránh cold start.
- Serialize đúng cách — Dùng JSON hoặc MessagePack, không dùng pickle (nguy cơ bảo mật).
Bước tiếp theo
Bạn đã nắm vững các chiến lược cache với Redis! Tiếp theo, chúng ta sẽ tìm hiểu cách deploy và monitor Redis trong môi trường production.
👉 Bài tiếp theo: Redis trong Production: Best Practices và Monitoring