Chiến lược cache với Redis: cache-aside, TTL và tránh cache stampede

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Thêm Redis vào ứng dụng không tự khiến nó nhanh hơn. Cái làm nên khác biệt là chiến lược: lưu gì, lưu bao lâu, xóa khi nào và xử lý ra sao khi cache trống. Bài này tổng hợp những mẫu cache phổ biến và các cạm bẫy cần tránh.

Cache-aside: mẫu phổ biến nhất

Ứng dụng chủ động quản lý cache. Khi đọc: kiểm tra cache trước, nếu không có thì đọc database rồi ghi vào cache.

import json, redisr = redis.Redis(decode_responses=True)def get_product(product_id):    key = f"product:{product_id}"    cached = r.get(key)    if cached:        return json.loads(cached)          # cache hit    product = db.fetch_product(product_id)  # cache miss: hỏi database    if product:        r.set(key, json.dumps(product), ex=300)  # lưu 5 phút    return product

Ưu điểm: đơn giản, chỉ dữ liệu thực sự được yêu cầu mới vào cache, và nếu Redis chết thì ứng dụng vẫn chạy được (chậm hơn) nhờ database.

Các mẫu khác

  • Write-through: mỗi lần ghi, cập nhật cả database và cache cùng lúc. Cache luôn mới nhưng thao tác ghi chậm hơn.
  • Write-behind: ghi vào cache trước, đẩy xuống database bất đồng bộ. Rất nhanh nhưng có rủi ro mất dữ liệu nếu cache hỏng trước khi kịp ghi.
  • Read-through: thư viện hoặc lớp cache tự lo việc nạp khi thiếu, ứng dụng chỉ nói chuyện với cache.

Cache-aside là điểm khởi đầu hợp lý cho hầu hết ứng dụng.

TTL và việc làm mới cache

TTL (thời gian sống) là lưới an toàn: dù bạn quên xóa, dữ liệu cũ cũng tự hết hạn. Chọn TTL theo mức chịu đựng dữ liệu cũ của nghiệp vụ: vài giây cho dữ liệu nóng thay đổi liên tục, vài phút hay vài giờ cho danh mục ít đổi. Nên cộng thêm một chút ngẫu nhiên vào TTL để nhiều khóa không cùng hết hạn một lúc:

import randomttl = 300 + random.randint(0, 60)r.set(key, value, ex=ttl)

Khi dữ liệu thay đổi, xóa khóa tương ứng để lần đọc sau nạp lại bản mới:

def update_product(product_id, data):    db.update_product(product_id, data)    r.delete(f"product:{product_id}")

Thứ tự "cập nhật database rồi xóa cache" thường an toàn hơn thứ tự ngược lại, vì tránh được tình huống một request khác nạp lại dữ liệu cũ vào cache ở giữa hai bước.

Cache stampede

Khi một khóa nóng hết hạn, hàng trăm request cùng thấy cache trống và cùng đổ về database để tính lại, có thể làm database quá tải. Một số cách giảm thiểu:

  1. Khóa tính lại: chỉ một request được phép nạp, những request khác chờ ngắn hoặc dùng dữ liệu cũ.
  2. Làm mới sớm: tính lại trước khi hết hạn thật sự.
  3. TTL ngẫu nhiên: như ví dụ trên, tránh hết hạn đồng loạt.
if r.set(f"lock:{key}", "1", nx=True, ex=10):   # giành được khóa    value = compute_expensive()    r.set(key, value, ex=300)    r.delete(f"lock:{key}")

Cache penetration: hỏi những thứ không tồn tại

Nếu ai đó liên tục hỏi id không có thật, mọi request đều xuyên qua cache tới database. Giải pháp: cache cả kết quả "không tìm thấy" với TTL ngắn, hoặc dùng bộ lọc như Bloom filter cho các hệ thống lớn.

Những điều nên làm

  • Cache dữ liệu đọc nhiều, ít thay đổi; đừng cache tất cả.
  • Đặt tên khóa nhất quán, có phiên bản khi cấu trúc dữ liệu đổi (ví dụ product:v2:42).
  • Theo dõi tỷ lệ trúng cache (hit ratio) bằng lệnh INFO stats.
  • Cấu hình giới hạn bộ nhớ và chính sách loại bỏ như allkeys-lru.
  • Đảm bảo ứng dụng vẫn đúng khi cache trống hoặc Redis tạm thời không phản hồi.

Phân biệt cache dùng chung và cache theo người dùng

Một lỗi nghiêm trọng thường gặp là cache nội dung riêng của từng người dùng dưới một khóa chung, khiến người này nhìn thấy dữ liệu của người khác. Với dữ liệu cá nhân, hãy đưa định danh người dùng vào khóa, ví dụ cart:user:42, hoặc đơn giản là không cache. Tương tự, khi cache phản hồi API theo tham số, mọi tham số ảnh hưởng đến kết quả (ngôn ngữ, bộ lọc, trang) đều phải nằm trong khóa. Sai một chi tiết nhỏ ở đây có thể gây lộ dữ liệu, nên hãy xem lại kỹ khi review code.

Bài tập gợi ý

Hãy lấy endpoint chậm nhất của bạn, thêm cache-aside với TTL ngắn, rồi đo thời gian phản hồi trước và sau. Hãy luôn kiểm tra lại bằng thực hành thay vì chỉ đọc lý thuyết: dựng một dự án nhỏ, cố tình tạo lỗi, quan sát log và công cụ gỡ lỗi, rồi sửa. Sau đó thử giả lập hết hạn đồng loạt để xem hiện tượng dồn tải và thử cách khóa tính lại.

Kết luận

Cache giỏi là cache có kế hoạch: mẫu cache-aside, TTL hợp lý, xóa khi dữ liệu đổi và phòng ngừa các hiện tượng dồn tải. Hãy bắt đầu từ truy vấn chậm nhất, đo trước và sau, và luôn nhớ rằng database mới là nguồn sự thật.

Nếu muốn luyện tập cùng bài tập thực tế, hãy ghé các khóa học tại codebem.com khi bạn sẵn sàng.

author_photo
Nguyễn Đình Cường

0 comment