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.
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ạn và xóa gì khi hết chỗ.
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:
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.
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
Zoom-in: TCP
Mọi HTTP request đều đi trên TCP — nhưng trước khi byte đầu tiên của dữ liệu đi qua, đã có 3 gói tin trao đổi mà không mang dữ liệu nào. TCP giải quyết vấn đề mà Internet không giải quyết được.
Zoom-in: OAuth 2.0
'Đăng nhập bằng Google' ẩn sau đó một cơ chế ủy quyền mà không bao giờ để password rời khỏi Google. OAuth 2.0 giải quyết bài toán delegation mà không hi sinh bảo mật.
Zoom-in: Load Balancer
Một domain, hàng triệu request mỗi ngày. Load balancer không chỉ phân phối traffic — nó là điểm quyết định routing, health check, và session management cho toàn bộ hệ thống.