Chuyển tới nội dung chính

Caching Strategies (Chiến lược Caching)

Tại sao Caching Quan trọng

Trong Hệ thống phân tán (Distributed Systems), Độ trễ (Latency) chủ yếu đến từ Network Hop (Bước nhảy mạng) và Disk I/O (Đọc ghi đĩa). Caching loại bỏ công việc thừa bằng cách phục vụ dữ liệu từ lớp nhanh hơn và gần người gọi hơn.

Không có CacheCó Cache
Mọi lần đọc đều đến DatabaseDữ liệu nóng (Hot Data) được phục vụ từ bộ nhớ
Network Round-trip cộng dồnÍt Hop = Độ trễ thấp hơn
Database trở thành Điểm nghẽn (Bottleneck)DB dư dung lượng cho truy vấn phức tạp
Mở rộng (Scale) cần thêm DB ReplicaMở rộng Cache rẻ hơn nhiều

Vị trí đặt Cache (Where to Place Caches)

Caching diễn ra ở mọi lớp (Layer) giữa người dùng và Database. Càng gần Client càng nhanh — nhưng dữ liệu càng kém mới (Stale):

Client (Trình duyệt / Ứng dụng Mobile)

[1] CDN — Tài sản tĩnh (Static Assets), phản hồi API ở Edge
↓ miss
[2] Reverse Proxy — Nginx / CloudFront full-page cache
↓ miss
[3] API Gateway — Response caching theo Route
↓ miss
[4] Application Layer — In-process (IMemoryCache) hoặc External (Redis / Memcached)
↓ miss
[5] Database — Buffer Pool, Query Cache (xem [Database Caching](/docs/database/caching))
Chọn lớp phù hợp

Cache ở lớp cao nhất có thể phục vụ dữ liệu với độ cũ chấp nhận được. Phản hồi CDN: 10–50ms; Redis lookup: 1–5ms; DB query: 5–100ms+. Mỗi lớp xuống thêm độ trễ nhưng tăng độ tươi (Freshness).

Năm Chiến lược Cache (The Five Cache Strategies)

Các chiến lược Cache chia thành hai nhóm — cách bạn Đọccách bạn Ghi. Chúng có thể kết hợp với nhau.

Chiến lược Đọc (Read Strategies)

Cache-Aside (Lazy Loading - Tải chậm)

Ứng dụng tự quản lý Cache. Khi đọc, kiểm tra Cache trước; nếu Miss, truy vấn nguồn và điền vào Cache.

Đọc:  App → Cache? → [hit] → return
→ [miss] → DB → lưu vào Cache → return
Ghi: App → DB → vô hiệu hóa Cache
  • Ưu điểm: Đơn giản, kiểm soát đầy đủ, hoạt động với bất kỳ Cache Store nào
  • Nhược điểm: Logic Cache trong mã ứng dụng, Khởi động nguội (Cold Start) chậm
  • Phù hợp nhất cho: Mục đích chung (General-purpose), workload Đọc/Ghi hỗn hợp

Read-Through

Cache nằm giữa ứng dụng và nguồn dữ liệu. Khi Miss, Cache Provider tự động tải từ Database. Ứng dụng chỉ giao tiếp với Cache.

Đọc:  App → Cache → [hit] → return
→ [miss] → Cache tải từ DB → lưu → return
Ghi: App → DB → vô hiệu hóa Cache
  • Ưu điểm: Mã ứng dụng sạch hơn (không có logic Cache), hành vi Cache nhất quán
  • Nhược điểm: Gắn chặt với Cache Provider, ít kiểm soát logic tải
  • Phù hợp nhất cho: Workload đọc nhiều (Read-heavy), muốn tách biệt trách nhiệm rõ ràng
  • Ví dụ: Spring Cache (@Cacheable), Hibernate Second-level Cache

Chiến lược Ghi (Write Strategies)

Write-Through

Mọi thao tác ghi đều đến cả Cache và Database đồng bộ trước khi Client nhận ACK.

Ghi:  App → Cache → DB → ACK
Đọc: App → Cache → [hit, đảm bảo tươi] → return
  • Ưu điểm: Tính nhất quán mạnh (Strong Consistency) — Cache và DB luôn đồng bộ
  • Nhược điểm: Độ trễ ghi cao (hai lần ghi đồng bộ)
  • Phù hợp nhất cho: Workload đọc nhiều, tính nhất quán quan trọng

Write-Around

Ghi trực tiếp đến Database, bỏ qua Cache hoàn toàn. Cache chỉ được điền khi đọc.

Ghi:  App → DB → ACK (Cache không ảnh hưởng)
Đọc: App → Cache → [miss] → DB → lưu → return
  • Ưu điểm: Cache không bị ô nhiễm bởi dữ liệu chỉ ghi, không khuếch đại ghi (No Write Amplification)
  • Nhược điểm: Dữ liệu vừa ghi không có trong Cache cho đến khi đọc lần đầu
  • Phù hợp nhất cho: Workload ghi nhiều, dữ liệu không được đọc lại ngay (Log, Analytics)

Write-Back (Write-Behind - Ghi ngược)

Ghi chỉ đến Cache; Client được ACK ngay lập tức. Database được cập nhật bất đồng bộ theo lô (Batch).

Ghi:  App → Cache → ACK (nhanh!)
Nền: Cache → flush lô → DB
  • Ưu điểm: Ghi rất nhanh, flush lô giảm tải DB
  • Nhược điểm: Nguy cơ mất dữ liệu nếu Cache Crash trước khi flush, Eventual Consistency
  • Phù hợp nhất cho: Workload ghi nhiều, chấp nhận mất dữ liệu (Telemetry, Counters)

Kết hợp Chiến lược (Combining Strategies)

Chiến lược Đọc và Ghi là trực giao (Orthogonal) — chọn một từ mỗi nhóm:

Chiến lược ĐọcChiến lược GhiHiệu quả
Cache-AsideWrite-AroundĐơn giản, Cache chỉ điền khi đọc
Cache-AsideWrite-ThroughNhất quán, ứng dụng quản lý Cache
Read-ThroughWrite-ThroughMã ứng dụng sạch + Tính nhất quán mạnh
Read-ThroughWrite-BehindMã ứng dụng sạch + Ghi nhanh (Eventual Consistency)
Cache-AsideWrite-BehindGhi nhanh, ứng dụng kiểm soát đọc

Kiến trúc Cache Phân tán (Distributed Cache Architecture)

Từ Đơn node đến Cluster (Từ một Node đến Cụm)

Một Instance Redis/Memcached đơn lẻ là Điểm lỗi đơn (Single Point of Failure). Hệ thống Production sử dụng Sharded Cluster:

Client → Cache Proxy (Twemproxy / HAProxy / Redis Cluster)
├── Shard 1 (keys hash 0–33%)
├── Shard 2 (keys hash 34–66%)
└── Shard 3 (keys hash 67–100%)

Chiến lược Sharding: Consistent Hashing (Băm nhất quán) giảm thiểu việc phân phối lại Key khi thêm/xóa Node. Mỗi Key được ánh xạ đến một điểm trên Hash Ring (Vòng băm).

Replication cho Ha (Sao chép cho Độ sẵn sàng cao)

Mỗi Shard có thể có Replica cho Failover:

  • Primary-Replica: Ghi đến Primary; Đọc có thể phục vụ từ Replica
  • Sentinel (Redis): Tự động Failover khi Primary xuống
  • Redis Cluster: Sharding + Replication tích hợp (16384 Hash Slot)
  • Đánh đổi: Nhiều Replica hơn = Throughput đọc tốt hơn + Ha cao hơn, nhưng chi phí Replication ghi cao hơn

Làm nóng Cache (Cache Warm-up)

Cache nguội (Cold Cache) gây trượt hàng loạt (Burst of Misses) khi triển khai. Các chiến lược tiền điền (Pre-populate):

Chiến lượcCách hoạt động
Pre-warm khi deployTải các Hot Key đã biết từ DB vào Cache trước khi định tuyến traffic
Gradual Rollout (Triển khai dần)Định hướng một phần nhỏ traffic đến Instance mới, tăng dần
Shadow TrafficPhát lại (Replay) các lần đọc Production vào Cache mới để làm nóng thụ động

CDN Caching

Cái gì được Cache ở CDN?

Loại nội dungKhả năng CacheTTL điển hình
Tài sản tĩnh - Static Assets (JS, CSS, hình ảnh)CaoNgày đến tháng (URL có phiên bản)
Phản hồi API - API Responses (dữ liệu công khai)Trung bìnhGiây đến phút
Phản hồi API (dữ liệu theo người dùng)ThấpThường không Cache ở CDN
Trang HTMLKhác nhauTùy thuộc vào nội dung động

Cache-Control Headers (Tiêu đề kiểm soát Cache)

Cache-Control: public, max-age=3600, s-maxage=86400
Chỉ thị (Directive)Ý nghĩa
publicBất kỳ Cache nào (CDN, Trình duyệt) đều có thể lưu phản hồi
privateChỉ Trình duyệt được lưu (không Cache CDN)
max-age=3600TTL Cache Trình duyệt — 1 giờ
s-maxage=86400TTL Cache CDN/Shared — 1 ngày (ghi đè max-age cho CDN)
no-cachePhải xác thực lại (Revalidate) với Server trước khi dùng bản Cache
no-storeKhông bao giờ Cache phản hồi này

Chiến lược Vô hiệu hóa CDN (CDN Invalidation Strategies)

CDN không biết khi nào dữ liệu thay đổi. Cách xử lý nội dung cũ (Stale Content):

  1. URL Versioning (Phiên bản URL): /app.v1.2.3.js — thay đổi URL khi nội dung thay đổi. URL cũ Cache vô thời hạn.
  2. Surrogate Keys: CDN liên kết Key với phản hồi; purge theo Key khi dữ liệu thay đổi.
  3. Short TTL + stale-while-revalidate: Phục vụ nội dung cũ ngay lập tức trong khi lấy nội dung mới ở nền.
  4. API Purge: Gọi API purge của CDN khi deploy (Cloudflare, Fastly, v.v.).
# Ví dụ stale-while-revalidate
Cache-Control: public, max-age=60, stale-while-revalidate=300
# → Phục vụ nội dung cũ tối đa 5 phút trong khi xác thực lại ở nền

Mẫu Ứng dụng (Application-Level Patterns)

Stale-While-Revalidate

Phục vụ dữ liệu cũ ngay lập tức trong khi lấy dữ liệu mới ở nền. Request tiếp theo nhận dữ liệu mới.

Request → cache hit (nhưng TTL hết hạn)
→ trả dữ liệu cũ cho Client ngay lập tức
→ lấy từ DB ở nền → cập nhật Cache
Request tiếp theo → cache hit (dữ liệu mới) → return
  • Người dùng thấy phản hồi nhanh (cũ nhưng chấp nhận được)
  • Dữ liệu hội tụ (Converge) đến trạng thái mới trong một chu kỳ Request
  • Hỗ trợ bởi CDN headers (stale-while-revalidate), SWR (React), StaleWhileRevalidate (.NET)

Refresh-Ahead (Làm mới trước khi hết hạn)

Trước khi TTL hết hạn, chủ động làm mới các Hot Entry ở nền. Người dùng không bao giờ thấy dữ liệu cũ cho Hot Key.

// Làm mới ở ~80% TTL với Jitter để tránh Thundering Herd
var ttl = TimeSpan.FromMinutes(30);
var refreshAt = TimeSpan.FromMinutes(24) + TimeSpan.FromSeconds(Random.Shared.Next(-30, 30));

Distributed Lock cho Stampede Prevention (Khóa phân tán chống dồn dập)

Khi một Hot Key hết hạn, chỉ một Request nên xây dựng lại — các Request còn lại chờ hoặc phục vụ dữ liệu cũ:

async Task<string> GetWithLockAsync(string key, CancellationToken ct = default)
{
var cached = await cache.GetStringAsync(key);
if (cached is not null) return cached;

var lockKey = $"lock:{key}";
// Thử acquire khóa ngắn hạn
var acquired = await cache.SetStringAsync(
lockKey, Environment.MachineName,
new() { Expiration = TimeSpan.FromSeconds(10) });

if (acquired)
{
try
{
var data = await db.QueryAsync(key, ct);
await cache.SetStringAsync(key, data, new() { Expiration = ttl });
return data;
}
finally
{
await cache.RemoveAsync(lockKey);
}
}

// Không acquire được Lock — Instance khác đang xây dựng lại. Thử lại sau delay ngắn.
await Task.Delay(100, ct);
return await GetWithLockAsync(key, ct);
}

Các lỗi Thường gặp (Common Pitfalls)

Cache Stampede / Thundering Herd (Dồn dập Cache)

Một Key phổ biến hết hạn và hàng ngàn Request đồng thời đều Miss và đánh vào Database.

Cách khắc phục: Distributed Lock (ở trên), Probabilistic Early Expiration, hoặc Per-request Coalescing.

Hot Keys (Khóa nóng)

Một Key duy nhất nhận lượng Traffic không tương xứng. Một Shard Cache trở thành Điểm nghẽn (Bottleneck).

Cách khắc phục: Nhân bản Hot Key với hậu tố ngẫu nhiên (user:1→a, user:1→b, user:1→c) và đọc từ một bản sao ngẫu nhiên. Redis Cluster xử lý điều này với Read Replica.

Large Values (Giá trị lớn)

Lưu JSON Document hoặc Blob lớn trong Cache lãng phí bộ nhớ và tăng chi phí Serialization.

Cách khắc phục: Chỉ Cache các trường cần thiết, nén giá trị, hoặc sử dụng Object Store riêng cho dữ liệu lớn.

Cascading Failures (Lỗi dây chuyền)

Nếu Cache xuống, toàn bộ Traffic đánh vào Database cùng lúc. DB quá tải và toàn bộ Hệ thống suy giảm.

Cách khắc phục: Circuit Breaker trên lần đọc Cache — nếu Cache không khả dụng, trả về phản hồi giảm cấp (Degraded Response) hoặc phục vụ từ Fallback Cache cục bộ thay vì quá tải DB.

Hướng dẫn Chọn Chiến lược (Strategy Selection Guide)

Tình huốngChiến lược ĐọcChiến lược Ghi
Danh mục sản phẩm (Đọc nhiều, ít thay đổi)Cache-Aside hoặc Read-ThroughWrite-Around
Hồ sơ người dùng (Đọc nhiều, cập nhật thỉnh thoảng)Read-ThroughWrite-Through
Bảng tin Mạng xã hội (Ghi nhiều, Eventual Consistency OK)Cache-AsideWrite-Back
Giỏ hàng (Cần Strong Consistency)Read-ThroughWrite-Through
Analytics / Telemetry (Ghi nhiều, ít đọc lại)Cache-AsideWrite-Around
Dữ liệu Session (Ghi nhanh, hết hạn theo TTL)Cache-AsideWrite-Back
Tài sản tĩnh (Static Assets)CDN (không phải Cache ứng dụng)Không áp dụng

Các chủ đề Liên quan

  • Database Caching — Buffer Pool, Query Cache, Eviction Policies chi tiết
  • Redis — Distributed Cache Engine phổ biến nhất
  • Scalability — Caching phù hợp trong bức tranh mở rộng lớn hơn
  • Load Balancing — Phân phối Traffic qua các Cache Node