Skip to content

Zoom-in: Cache

Karify98·
Cover Image for Zoom-in: Cache

Chậm thì thêm Redis. Ứng dụng nhanh hơn, xong việc.

graph LR
    C(["👤 Client"]) -->|"request"| A(["⚡ Ứng dụng"])
    A -->|"response nhanh"| C
    style C fill:#1e293b,stroke:#475569,color:#cbd5e1
    style A fill:#1e293b,stroke:#475569,color:#cbd5e1

Phóng to dần vào black box đó.


Layer 1 — Vấn đề gốc: mỗi request đều đắt

Tại sao cần cache? Vì một số thao tác tốn kém theo thời gian hoặc tài nguyên — và kết quả không thay đổi giữa các lần gọi.

sequenceDiagram
    participant C as Client
    participant A as Ứng dụng
    participant DB as Database

    C->>A: GET /products/top-10
    A->>DB: SELECT TOP 10 products ORDER BY sales
    Note over DB: Quét 10 triệu rows...
    DB-->>A: Kết quả (800ms)
    A-->>C: Danh sách (800ms)

    C->>A: GET /products/top-10 (request thứ 2)
    A->>DB: SELECT TOP 10 products ORDER BY sales
    Note over DB: Quét lại 10 triệu rows...
    DB-->>A: Cùng kết quả (800ms)
    A-->>C: Cùng danh sách (800ms)

Kết quả giống nhau ở cả hai request — nhưng vẫn tốn 800ms mỗi lần. Top 10 sản phẩm không thay đổi mỗi giây.

Vấn đề còn lại: cần lưu kết quả lại để lần sau không phải tính lại từ đầu.

Layer 2 — Cache hit và miss: cơ chế cơ bản

Cache hoạt động theo một vòng lặp đơn giản: kiểm tra trước, tìm thấy thì dùng luôn, không tìm thấy thì tính rồi lưu.

graph TD
    R["Request"] --> Q{"Cache có\nkết quả không?"}

    Q -->|"Cache hit ✓"| H["Trả kết quả ngay\n(vài ms)"]
    Q -->|"Cache miss ✗"| M["Tính kết quả\n(800ms)"]
    M --> S["Lưu vào cache"]
    S --> H2["Trả kết quả"]

    style R fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    style Q fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
    style H fill:#1a3a2a,stroke:#22c55e,color:#86efac
    style H2 fill:#1a3a2a,stroke:#22c55e,color:#86efac
    style M fill:#3b1a1a,stroke:#ef4444,color:#fca5a5
    style S fill:#1e293b,stroke:#475569,color:#cbd5e1

Cache không thể lưu mãi mãi — bộ nhớ có giới hạn và dữ liệu có thể thay đổi. Cần quy tắc về bao lâu thì dữ liệu hết hạnxóa gì khi hết chỗ.

Vấn đề còn lại: dữ liệu trong cache có thể lỗi thời — cần chiến lược về thời hạn và xóa.

Layer 3 — TTL và eviction: quản lý vòng đời cache

TTL (Time-To-Live) là thời gian một entry được giữ trong cache trước khi tự động hết hạn.

SET product:top10 <data> EX 300    # hết hạn sau 5 phút

Khi cache đầy, cần chọn entry nào xóa trước — gọi là eviction policy:

LRU Least Recently Used — xóa entry lâu nhất chưa được dùng. Phổ biến nhất vì phù hợp với hành vi thực tế.
LFU Least Frequently Used — xóa entry ít được truy cập nhất. Tốt hơn LRU khi có hot data truy cập đều đặn.
TTL Expire first — ưu tiên xóa entry đã hết hạn trước. Redis mặc định kết hợp với LRU/LFU.

TTL ngắn → ít stale data, nhiều cache miss hơn. TTL dài → ít DB query, nhưng dữ liệu cũ hơn thực tế. Không có TTL "đúng" — tùy vào mức độ chấp nhận được của stale data.

Vấn đề còn lại: dữ liệu thay đổi giữa các lần hết TTL — người dùng nhìn thấy dữ liệu cũ mà không biết.

Layer 4 — Cache invalidation: bài toán khó nhất

Phil Karlton từng nói: "There are only two hard things in Computer Science: cache invalidation and naming things."

Khi dữ liệu gốc thay đổi, cache phải được cập nhật — nhưng cache không tự biết điều đó.

sequenceDiagram
    participant A as Admin
    participant App as Ứng dụng
    participant Cache as Redis Cache
    participant DB as Database

    A->>App: PUT /products/1 (cập nhật giá)
    App->>DB: UPDATE products SET price = 99
    DB-->>App: OK

    Note over Cache: Cache vẫn giữ dữ liệu cũ với giá 149

    A->>App: GET /products/top-10
    App->>Cache: Lấy top-10
    Cache-->>App: Dữ liệu cũ (giá 149)
    App-->>A: ❌ Sai giá

Ba chiến lược phổ biến:

  • Cache-aside + TTL: để TTL tự hết hạn. Đơn giản nhất, chấp nhận stale trong khoảng TTL.
  • Write-through: khi update DB, update cache luôn. Nhất quán hơn, nhưng phức tạp hơn.
  • Event-driven invalidation: khi DB thay đổi, phát sự kiện để xóa cache liên quan. Linh hoạt nhất, nhưng cần thêm cơ sở hạ tầng.

Full picture

Một request đi qua các tầng cache trước khi chạm DB.

graph LR
    C["👤 Client"] --> BC["Browser Cache\n(Service Worker / HTTP cache)"]
    BC -->|"miss"| CDN["🌐 CDN\n(CloudFront, Cloudflare)"]
    CDN -->|"miss"| AC["⚡ App Cache\n(Redis, Memcached)"]
    AC -->|"miss"| DB["🗄️ Database"]

    DB -->|"kết quả"| AC
    AC -->|"lưu + trả"| CDN
    CDN -->|"lưu + trả"| BC
    BC -->|"lưu + trả"| C

    style C fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    style BC fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
    style CDN fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
    style AC fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
    style DB fill:#1a3a2a,stroke:#22c55e,color:#86efac

Mỗi tầng giải quyết một bài toán khác nhau: browser cache giảm request mạng; CDN giảm latency địa lý; app cache giảm tải DB. Khi debug, cần biết request đang bị cache ở tầng nào.


Ba nhầm lẫn phổ biến

Redis là database

Redis là in-memory store — dữ liệu mất khi restart nếu không bật persistence. Dùng Redis để cache (mất được) hoặc session (cần TTL rõ ràng), không dùng để lưu dữ liệu quan trọng cần tồn tại lâu dài.

Cache nhiều hơn luôn tốt hơn

Cache key cần quản lý: quá nhiều entry → eviction không đoán được; cache cho dữ liệu thay đổi thường xuyên → stale data liên tục. Cache phù hợp nhất cho dữ liệu đọc nhiều, thay đổi ít, chấp nhận được nếu cũ vài giây đến vài phút.

Xóa cache là đủ để update

Xóa cache (invalidation) không đồng bộ với update DB — khoảng thời gian giữa delete cache và DB commit có thể khiến request khác đọc dữ liệu cũ rồi lưu ngược lại vào cache. Trong môi trường concurrent, thứ tự các thao tác quan trọng hơn người ta thường nghĩ.

Takeaway

Cache tồn tại vì một số thao tác đắt hơn giá trị của sự nhất quán tuyệt đối. TTL là cam kết về mức độ stale data có thể chấp nhận được; eviction policy là chiến lược khi bộ nhớ có giới hạn; cache invalidation là bài toán khó vì DB và cache là hai hệ thống độc lập không biết về nhau.

Khi debug dữ liệu sai, câu hỏi đầu tiên: "request đang đọc từ tầng cache nào?" — browser devtools, CDN headers (X-Cache: HIT), và Redis TTL <key> sẽ chỉ ra tầng có vấn đề.


Bài viết được hỗ trợ bởi Amy 🌸 - AI Assistant. Nội dung đã được kiểm duyệt bởi tác giả.

Related Posts