Caching toàn cảnh — 5 tầng cache giữa user và database
Khi user thấy web 'không cập nhật' dù bạn đã đổi, đa số là 1 trong 5 tầng cache đang giữ bản cũ. Hiểu Browser → CDN → Reverse Proxy → App → Database cache giúp bạn debug nhanh + tối ưu hiệu suất 10-100x. 'Cache invalidation' là 1 trong 2 việc khó nhất ngành CS.
Mục lục bài viết(27)

Memory hierarchy trong máy tính — register CPU (nhanh nhất, đắt nhất, ít nhất) → L1/L2/L3 cache → RAM → SSD → HDD → tape backup. Pattern caching trong web cũng theo nguyên lý này: browser cache (nhanh nhất) → CDN cache → reverse proxy cache → app cache → database cache. Mỗi tầng phục vụ vai trò khác nhau trong cùng pipeline. Nguồn: Wikimedia Commons (CC BY-SA 4.0).
Hiểu đơn giản nhất
Tưởng tượng bạn cần tài liệu từ thư viện quốc gia. 5 tầng "cache" có thể giúp:
| Tầng | Đời thực | Web tương đương | Tốc độ |
|---|---|---|---|
| 1. Browser | Túi giấy mang theo | Browser cache (disk + memory) | 0ms |
| 2. CDN | Văn phòng phẩm gần nhà | Cloudflare/CloudFront POP | 5-20ms |
| 3. Reverse Proxy | Tủ tài liệu công ty | Nginx/Varnish cache | 1-5ms |
| 4. App | Bàn làm việc | Redis/Memcached | 1-3ms |
| 5. Database | Kho thư viện | Postgres shared_buffers | 5-50ms |
| (origin) | Thư viện quốc gia | DB disk read | 100ms+ |
Mỗi request đi từ tầng 1 → 5. HIT bất kỳ tầng nào = nhanh. Chỉ khi miss cả 5 → đập tới origin (disk read).
Một request "good" qua web hiện đại:
User click "Xem sản phẩm 123"
↓
[1] Browser cache HIT? Có (CSS/JS từ visit trước) → 0ms
[2] CDN cache HIT cho /api/products/123? Có → 15ms response
↓
DONE — total ~15ms
Cùng request không có cache:
[1] Browser cache MISS
[2] CDN MISS (chưa từng cache product 123)
[3] Reverse proxy MISS
[4] App Redis MISS
[5] DB query → JOIN 5 table → 80ms
↓
Total ~250ms (HTML parse + render thêm)
Chênh 16x chỉ nhờ caching đúng.
Tại sao bạn cần biết
- Debug "tại sao web không update". Bạn deploy code mới, user vẫn thấy bản cũ → check 5 tầng. Đa số case là CDN hoặc browser cache.
- Performance budget thực tế. Web 1k visit/giờ cache 80% → DB chỉ chịu 200 query/giờ thay vì 1000 query. Khả năng scale 5x mà không cần thêm server.
- Tiết kiệm chi phí cloud. AWS DB query tính tiền theo IOPS. Cache 80% = bill giảm 80%.
- Hiểu vì sao "cache invalidation" khó. Phil Karlton (Netscape) quote 1996: "There are only two hard things in Computer Science: cache invalidation and naming things."
- Avoid catastrophic bug. 2 user thấy data của nhau, password lộ qua cache shared, cart user A bị checkout bởi user B — đều do cache config sai.
Tầng 1: Browser Cache
Mỗi browser (Chrome, Firefox, Safari) có cache nội bộ chia 2 phần:
Memory cache (RAM)
- Sống trong session (đóng tab = xoá)
- Cực nhanh (~0ms)
- Dùng cho asset request lặp trong cùng session
Disk cache (file system)
- Sống qua reboot
- Nhanh (~5ms đọc disk)
- Có thể xoá qua DevTools → Application → Clear storage
Header điều khiển
Cache-Control: public, max-age=31536000, immutable
ETag: "abc123"
Last-Modified: Mon, 22 May 2026 10:00:00 GMT| Header | Vai trò |
|---|---|
max-age=N | Cache N giây, không cần check server |
immutable | File này không bao giờ đổi — đừng check lại (cho asset có hash) |
ETag | Hash của content. Browser gửi If-None-Match: <etag> → server trả 304 nếu chưa đổi |
Last-Modified | Timestamp đổi. Browser gửi If-Modified-Since → 304 nếu chưa đổi |
Pattern: cache busting
File CSS đổi mỗi deploy. Nếu cache 1 năm thì user kẹt bản cũ. Fix: hash trong tên file.
Trước: style.css → cache 1 năm → đổi nội dung → user kẹt
Sau: style.abc123.css → deploy mới → style.def456.css → URL mới
Webpack/Vite/Next.js tự generate hash. Mỗi deploy = file URL mới = browser fetch lại tự nhiên.
Tầng 2: CDN Cache
Đã chi tiết trong bài CDN là gì. Tóm tắt:
- 100-300 POP global, user fetch từ POP gần nhất
- Cache static asset chính (CSS, JS, image, font)
- Có thể cache HTML nếu config (s-maxage header)
- Cloudflare free tier không giới hạn bandwidth
Quy tắc cache CDN
Cache-Control: public, s-maxage=300, stale-while-revalidate=86400
- CDN cache 5 phút
- Hết hạn → serve bản cũ + fetch mới background (stale-while-revalidate) trong 24 giờ
- User không bao giờ chờ — luôn có cached response
Tầng 3: Reverse Proxy Cache
Nginx, Varnish, Caddy đặt trước app server. Cache:
- HTML static (Marketing landing page)
- API GET response (sản phẩm, danh mục)
- Asset từ upstream
Nginx cache example
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m;
server {
location /api/products {
proxy_cache app_cache;
proxy_cache_valid 200 5m;
proxy_cache_valid 404 1m;
proxy_cache_key "$request_uri";
proxy_cache_lock on; # tránh stampede
proxy_pass http://backend:3000;
}
}Nginx cache HIT trả response trong 1-3ms, không đập backend.
Khi nào dùng: backend chạy chậm (PHP, Django), Nginx ngồi trước hấp thụ traffic.
Tầng 4: Application Cache (Redis/Memcached)
App tự lưu trong RAM (in-memory) hoặc shared store (Redis).
In-memory cache (cùng process)
// Next.js example
const cache = new Map<string, { value: unknown; expires: number }>();
function memoize<T>(key: string, ttlSec: number, fn: () => Promise<T>): Promise<T> {
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) {
return cached.value as T;
}
return fn().then((value) => {
cache.set(key, { value, expires: Date.now() + ttlSec * 1000 });
return value;
});
}
// Usage
const products = await memoize('products:hot', 300, () => db.query.products.findMany({...}));⚠️ In-memory cache chỉ hoạt động trong 1 process. Multi-server thì mỗi server có cache riêng → inconsistency.
Redis (shared across servers)
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
async function getProducts() {
const cached = await redis.get('products:hot');
if (cached) return JSON.parse(cached);
const products = await db.query.products.findMany({ where: { isHot: true } });
await redis.set('products:hot', JSON.stringify(products), 'EX', 300);
return products;
}Redis có thể serve 100k+ ops/giây — sweet spot cho app cache. Cluster mode scale tới triệu ops.
Cache key strategy
| Strategy | Khi nào |
|---|---|
products:{id} | Cache 1 entity, invalidate khi update |
products:list:{page}:{limit} | Cache list với pagination |
user:{id}:profile | Per-user data |
query:{hash(sql)} | Query result cache (cẩn thận với params) |
Tầng 5: Database Cache
DB cũng có cache nội bộ:
Postgres shared_buffers
Postgres mặc định cấp 128MB RAM cho shared_buffers — cache table/index data trong memory. Increase tuỳ server:
- VPS 2GB RAM: shared_buffers = 512MB
- VPS 8GB RAM: shared_buffers = 2GB
- VPS 32GB RAM: shared_buffers = 8GB
Query plan cache
Postgres analyze query → tạo execution plan → cache plan. Lần sau cùng query (different params) dùng lại plan. Tiết kiệm 5-20ms parsing.
Materialized view
CREATE MATERIALIZED VIEW sales_summary AS
SELECT product_id, SUM(amount) FROM orders GROUP BY product_id;
REFRESH MATERIALIZED VIEW CONCURRENTLY sales_summary;View được "frozen" với pre-computed result. Query view = chỉ table scan (nhanh) thay vì JOIN 5 table.
Cache invalidation strategies
3 chiến lược chính:
1. TTL (Time To Live) — đơn giản nhất
Set hết hạn cố định. Hết → re-fetch. KHÔNG cần code invalidate.
✅ Đơn giản, ít bug ❌ Có window stale (data cũ đến TTL hết hạn)
2. Write-through — cập nhật cache khi DB update
async function updateProduct(id: string, data: ProductData) {
await db.update(products).set(data).where(eq(products.id, id));
await redis.set(`products:${id}`, JSON.stringify(data), 'EX', 3600);
}✅ Cache luôn fresh ❌ Tốn write 2 chỗ, có thể race condition
3. Cache invalidation explicit
async function deleteProduct(id: string) {
await db.delete(products).where(eq(products.id, id));
await redis.del(`products:${id}`);
await redis.del('products:list:*'); // Hoặc invalidate list keys
}✅ Control rõ ràng ❌ Dễ quên invalidate, code phức tạp
Ví dụ thực tế: blog wiki.vietcodex.com pipeline
User mở https://wiki.vietcodex.com/co-ban/api-la-gi:
[1] Browser cache HIT cho CSS/JS từ visit trước → 0ms
[2] HTML: Browser fetch → Cloudflare POP HN
- CDN cache HIT (s-maxage=300 từ deploy 2 phút trước) → 15ms response
[3] (skipped — request chỉ tới CDN, không tới origin)
[4] (skipped)
[5] (skipped)
Tổng: ~15ms
Nếu CDN MISS:
[2→3] Cloudflare POP fetch từ origin (Cloudflare Tunnel → homeserver VN) → 20ms
[3] Reverse proxy: Next.js standalone serve HTML → 5ms
[4] App: getArticleBySlug() read MDX file → file system cache 1ms
[5] (no DB cho static MDX)
Tổng MISS: ~50ms (still fast vì static MDX)
Dashboard page (per-user) /admin-hub/menu:
[1] Browser HTML không cache (private)
[2] CDN bypass (Cache-Control: private)
[3] Nginx bypass
[4] App: getCurrentUser() → Redis session cache HIT 2ms
[4] App: getTenantBySlug() → Redis tenant cache HIT 2ms
[4] App: getMenuItems() → no cache → DB query
[5] Postgres: shared_buffers HIT → 8ms
Tổng: ~30ms
Cái gì có thể sai
| Vấn đề | Tầng nào | Cách fix |
|---|---|---|
| Web update content nhưng user thấy cũ | Browser hoặc CDN | Hard refresh (Ctrl+Shift+R) + purge CDN |
| 2 user thấy data nhau | CDN cache HTML có session | Set Cache-Control: private cho HTML có user data |
| Login OK nhưng vẫn thấy "Đăng nhập" button | CDN cache HTML | Bypass cache khi có cookie session |
| Redis full RAM, OOM | Cache key không TTL | SET với EX flag mọi key, monitor maxmemory |
| Cache stampede sau deploy | 1000 user cùng request key vừa expire | stale-while-revalidate + lock |
| Postgres slow sau scale | shared_buffers nhỏ vs working set | Tăng shared_buffers, EXPLAIN ANALYZE query chậm |
| Service Worker keep stale forever | Update strategy sai | Use Workbox skipWaiting + clientsClaim |
| ETag không hoạt động | Server gen ETag không deterministic | Hash content body, không hash thời gian |
Tóm tắt 1 dòng
Cache pipeline web hiện đại = 5 tầng: Browser → CDN → Reverse Proxy → App (Redis) → Database. Mỗi tầng phục vụ pattern khác nhau: browser cho asset asset, CDN cho global static, app/Redis cho per-request data, DB cho query plan. Cache HIT 80%+ ở tầng cao = web nhanh 10-100x. Cache invalidation là 1 trong 2 việc khó nhất CS — TTL ngắn cho data đổi nhiều, immutable hash filename cho static.
Đọc tiếp
- CDN là gì — vì sao web load nhanh ở Hà Nội nhưng chậm ở Mỹ — tầng 2 trong pipeline
- Core Web Vitals — 3 số Google chấm điểm tốc độ web — cache giúp pass LCP
- Database là gì? So sánh dễ hiểu với Excel — tầng 5, nguồn truth nhất
- API là gì? Tại sao mọi app, mọi web hiện đại đều cần — API GET là candidate cache mạnh