Caching & Redis: Mô hình tư duy 'Tờ giấy nhớ'
Tại sao Redis làm mọi thứ nhanh hơn? Hướng dẫn chuyên sâu về cache invalidation (vấn đề khó nhất CS), các chiến lược eviction, và Redis data types.
“Thêm Redis vào là performance sẽ tăng thôi.”
Câu này anh em dev nào cũng hay nói. Nhưng ít người hiểu tại sao nó lại hiệu quả — hay khi nào nó thất bại thảm hại.
Cache là tối ưu hiệu năng mạnh nhất bạn có thể áp dụng. Đồng thời cũng là cách dễ nhất để serve dữ liệu cũ và để user nhìn thấy thông tin sai trong nhiều giờ.
Đây là Hướng dẫn chuyên sâu về Caching. Chúng ta sẽ đi qua Redis data types, 3 chiến lược eviction, và vấn đề khó nhất Khoa học Máy tính, và các pattern phân biệt senior với junior.
Phần 1: Nền tảng (Mô hình tư duy)
Tờ giấy nhớ vs. Tủ hồ sơ ở tầng hầm
Mỗi khi app cần dữ liệu, nó phải chọn:
-
Database = Tủ hồ sơ (ở tầng hầm) Chính xác. Có tất cả mọi thứ. Nhưng bạn phải đi bộ xuống tầng hầm, mở ngăn, tìm hồ sơ, rồi leo lên lại. Chậm: 5–100ms mỗi lần.
-
Cache (Redis) = Tờ giấy nhớ dán trên màn hình Bạn đã ghi câu trả lời ở đây trước rồi. Cứ nhìn vào là xong. Nhanh: 0.1–1ms.
Mục tiêu: trả lời từ Tờ giấy nhớ bất cứ khi nào có thể. Chỉ xuống Tầng hầm khi tờ giấy không có hoặc thông tin đã lỗi thời.
Hai câu hỏi cốt lõi
Mọi quyết định về caching đều xoay quanh hai câu hỏi:
- Dữ liệu “tươi” được bao lâu? (TTL — Time To Live)
- Khi nào cache đầy thì làm gì? (Eviction Strategy)
Phần 2: Điều tra (Redis Data Types)
Redis không chỉ là key-value store đơn giản. Nó có 5 cấu trúc dữ liệu chính, mỗi cái giải quyết một vấn đề khác nhau.
| Kiểu | Vật thể tương ứng | Use Case |
|---|---|---|
| String | Tờ giấy nhớ | Session token, cache đơn giản, bộ đếm. |
| Hash | Bảng tính mini | User profile (user:123 → {name, email, tuổi}). |
| List | Hàng đợi (FIFO) | Bản tin hoạt động (50 hành động gần nhất). Task queue. |
| Set | Hộp bi không trùng | Khách truy cập duy nhất hôm nay. Tags của bài viết. |
| Sorted Set | Bảng xếp hạng | Top 10 user theo điểm. Cửa sổ rate limiting. |
TTL (Time To Live)
Mọi cache entry phải có TTL — thời gian hết hạn. Không có TTL là memory leak.
import redis
r = redis.Redis()
# Set với TTL 5 phút
r.set("user:123:profile", json.dumps(user_data), ex=300)
# Kiểm tra thời gian còn lại
r.ttl("user:123:profile") # Trả về giây còn lại, -1 nếu không TTL, -2 nếu key không tồn tại
Phần 3: Chẩn đoán (Cache Invalidation — Vấn đề khó nhất)
Phil Karlton có câu nói nổi tiếng:
“Chỉ có hai thứ khó trong Khoa học Máy tính: cache invalidation và đặt tên.”
3 Chiến lược Eviction (Khi Cache đầy)
Khi Redis hết RAM, nó phải xóa thứ gì đó. Xóa cái nào?
| Chiến lược | Cái bị xóa | Dùng khi |
|---|---|---|
| LRU (Ít được dùng nhất gần đây) | Key lâu không được truy cập nhất. | Tổng quát. Mặc định an toàn. |
| LFU (Ít được dùng nhất tổng cộng) | Key được truy cập ít lần nhất. | Khi tần suất truy cập quan trọng. |
| Random (allkeys-random) | Một key ngẫu nhiên. | Hầu như không bao giờ dùng. Nguy hiểm. |
Cấu hình eviction policy:
maxmemory-policy allkeys-lru
3 Dạng thất bại của Cache
1. Cache Miss (Bình thường) Key không có trong cache → lấy từ DB → lưu vào cache → trả về. Đây là bình thường.
2. Cache Stampede (Nguy hiểm) TTL hết hạn. 10,000 user cùng lúc request cùng một dữ liệu. Tất cả đều miss. Tất cả đều đánh vào DB cùng lúc. DB sập.
Cách sửa: Dùng lock (Redis SETNX) khi regenerate cache entry. Chỉ một process regenerate; các process còn lại chờ.
import redis, time
r = redis.Redis()
def get_heavy_data(key: str):
cached = r.get(key)
if cached:
return json.loads(cached)
# Chỉ MỘT process được regenerate
lock_key = f"lock:{key}"
if r.set(lock_key, "1", nx=True, ex=10): # nx=SETNX (chỉ set nếu chưa tồn tại)
data = expensive_db_query()
r.set(key, json.dumps(data), ex=300)
r.delete(lock_key)
return data
else:
time.sleep(0.1)
return get_heavy_data(key) # Thử lại
3. Cache Poisoning (Thảm họa) Bạn cache dữ liệu sai. Giờ mọi user đều thấy thông tin sai cho đến khi TTL hết. Không có cách sửa dễ — phải xóa key thủ công.
# Xóa một key cụ thể
redis-cli DEL "user:123:profile"
# Xóa tất cả key theo pattern (cẩn thận khi dùng production!)
redis-cli --scan --pattern "user:*" | xargs redis-cli DEL
Phần 4: Giải pháp (Sách nấu ăn Python)
1. Cache-Aside Pattern (Chuẩn mực)
App tự điều khiển cache. Pattern phổ biến nhất.
def get_user(user_id: int) -> dict:
cache_key = f"user:{user_id}"
# 1. Thử cache trước
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached) # Cache HIT
# 2. Cache miss: truy vấn DB
user = User.objects.get(id=user_id)
data = serialize(user)
# 3. Lưu vào cache cho lần sau (TTL: 5 phút)
redis_client.set(cache_key, json.dumps(data), ex=300)
return data # Cache MISS
2. Invalidate on Write (Giữ cache trung thực)
Khi dữ liệu thay đổi, xóa cache ngay lập tức.
def update_user(user_id: int, new_data: dict):
# 1. Cập nhật DB
User.objects.filter(id=user_id).update(**new_data)
# 2. XÓA NGAY cache (đừng chờ TTL!)
redis_client.delete(f"user:{user_id}")
3. Rate Limiting (Redis làm bộ đếm)
Giới hạn API: 100 request mỗi user mỗi phút.
def is_rate_limited(user_id: int, limit: int = 100) -> bool:
key = f"rate:{user_id}:{int(time.time() // 60)}" # Key đổi mỗi phút
count = redis_client.incr(key) # Tăng nguyên tử (Atomic increment)
if count == 1:
redis_client.expire(key, 60) # Đặt TTL ngay lần đầu tăng
return count > limit
Mô hình tư duy chốt hạ
Cache (Redis) -> Tờ giấy nhớ. Nhanh, tạm thời, có thể lỗi thời.
Database -> Tủ hồ sơ tầng hầm. Chậm, chính xác, vĩnh viễn.
LRU Eviction -> "Xóa thứ lâu ngày không ai ngó đến."
Cache Stampede -> 10,000 người lật tờ giấy nhớ cùng lúc.
Cache Invalidation -> Vấn đề khó nhất: biết KHI NÀO vứt tờ giấy nhớ đi.
Quy tắc vàng:
- Đặt TTL cho mọi thứ. Không TTL = Memory leak.
- Xóa cache khi ghi, không chỉ chờ TTL hết hạn.
- Cache đúng tầng: Tránh cache dữ liệu một nửa mà khó biết khi nào vô hiệu hóa.
Bài viết liên quan
-
CDN: Mô hình tư duy 'Cửa hàng Tiện lợi'
Tại sao ảnh load ngay với user Mỹ nhưng ì ạch ở Việt Nam? Hướng dẫn chuyên sâu về CDN Edge Node, Cache-Control header và cache busting.
-
Task Queue & Message Broker: Celery, RabbitMQ và Kafka — Phân biệt rõ ràng
Tại sao gửi email làm đứng API? Hướng dẫn chuyên sâu về xử lý bất đồng bộ: từ Celery/Django-Q (hàng đợi tác vụ) đến RabbitMQ (môi giới) và Kafka (luồng sự kiện).
-
Rate Limiting & Circuit Breaker: Mô hình tư duy 'Đèn Giao thông & Hộp Cầu dao'
Làm sao ngăn một client xấu làm sập toàn bộ API của bạn? Hướng dẫn chuyên sâu về chiến lược rate limiting, circuit breaker và các pattern tăng cường độ bền.
-
Tại Sao Phần Cứng Rẻ Không Thể Giúp Redis Thay Thế Database Cốt Lõi
Khi phần cứng ngày càng rẻ, tại sao không dùng Redis làm cơ sở dữ liệu chính? Câu trả lời ngắn gọn: vì giới hạn không nằm ở tốc độ, mà nằm ở các yếu tố đảm bảo và ngữ nghĩa dữ liệu.