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.

10 phút đọcCập nhật 2026-05-22
Nghe bài viết
Để Claude đọc bài cho bạn — vừa nghe vừa làm việc khác
Mục lục bài viết(27)
Sơ đồ memory hierarchy — từ register CPU tới đĩa cứng, mỗi tầng nhanh hơn nhưng đắt hơn tầng dưới

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ựcWeb tương đươngTốc độ
1. BrowserTúi giấy mang theoBrowser cache (disk + memory)0ms
2. CDNVăn phòng phẩm gần nhàCloudflare/CloudFront POP5-20ms
3. Reverse ProxyTủ tài liệu công tyNginx/Varnish cache1-5ms
4. AppBàn làm việcRedis/Memcached1-3ms
5. DatabaseKho thư việnPostgres shared_buffers5-50ms
(origin)Thư viện quốc giaDB disk read100ms+

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
HeaderVai trò
max-age=NCache N giây, không cần check server
immutableFile này không bao giờ đổi — đừng check lại (cho asset có hash)
ETagHash của content. Browser gửi If-None-Match: <etag> → server trả 304 nếu chưa đổi
Last-ModifiedTimestamp đổ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

StrategyKhi nào
products:{id}Cache 1 entity, invalidate khi update
products:list:{page}:{limit}Cache list với pagination
user:{id}:profilePer-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àoCách fix
Web update content nhưng user thấy cũBrowser hoặc CDNHard refresh (Ctrl+Shift+R) + purge CDN
2 user thấy data nhauCDN cache HTML có sessionSet Cache-Control: private cho HTML có user data
Login OK nhưng vẫn thấy "Đăng nhập" buttonCDN cache HTMLBypass cache khi có cookie session
Redis full RAM, OOMCache key không TTLSET với EX flag mọi key, monitor maxmemory
Cache stampede sau deploy1000 user cùng request key vừa expirestale-while-revalidate + lock
Postgres slow sau scaleshared_buffers nhỏ vs working setTăng shared_buffers, EXPLAIN ANALYZE query chậm
Service Worker keep stale foreverUpdate strategy saiUse Workbox skipWaiting + clientsClaim
ETag không hoạt độngServer gen ETag không deterministicHash 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

Câu hỏi thường gặp

Cache và Buffer khác nhau gì?
Cache = lưu lại để DÙNG LẠI nhanh (tăng tốc, giảm cost). Buffer = lưu tạm để TRUNG GIAN (chờ data đủ để xử lý batch). Ví dụ: cache là 'tủ bánh' trong quán cafe — bánh đã làm sẵn để bán nhanh. Buffer là 'khay tích bánh' trong lúc nướng — phải đợi đủ 10 bánh mới ra lò. Trong code: Redis cache giảm load DB. TCP buffer tích packet để truyền hiệu quả. Hai khái niệm khác nhau dù đều 'lưu tạm'.
TTL nên set bao nhiêu? Có công thức không?
Quy tắc theo loại data: (1) Static asset có hash filename (style.abc123.css): 1 năm immutable; (2) Image hero: 1 tháng; (3) Blog HTML: 1 giờ - 4 giờ; (4) API public list (sản phẩm, danh mục): 5-15 phút; (5) User profile/cart: KHÔNG cache hoặc 30s; (6) Real-time data (giá crypto, stock): 5-30 giây. Cân nhắc: TTL ngắn = data tươi, server tải cao. TTL dài = nhanh, có thể stale. Tăng dần TTL khi monitor cache hit rate < 80% — chứng tỏ TTL chưa đủ dài.
Cache stampede là gì? Tránh thế nào?
Cache stampede = khi cache key hết hạn cùng lúc, tất cả request đồng thời miss → đập vào DB cùng lúc → DB sập. Ví dụ: blog post hot cache 5 phút. Phút thứ 5 hết hạn, 1000 user cùng request → 1000 query DB cùng lúc. Cách fix: (1) Stale-while-revalidate — serve bản cũ trong khi background fetch bản mới; (2) Probabilistic early expiration — 1 request lẻ tự refresh trước khi hết hạn thật; (3) Lock — chỉ 1 request được fetch, các request khác đợi. Redis có pattern lock, Cloudflare có 'Tiered Cache' hỗ trợ.
Cache-Control header — directive nào quan trọng nhất?
3 directive PHẢI biết: (1) `max-age=N` — browser cache N giây; (2) `s-maxage=N` — CDN/proxy cache N giây (override max-age); (3) `public` vs `private` — public cho phép cache shared (CDN), private chỉ browser cache. Combo phổ biến: static asset → `public, max-age=31536000, immutable`. HTML public → `public, max-age=0, s-maxage=300, stale-while-revalidate=600`. Per-user data → `private, no-cache`. Đừng quên `vary: Accept-Encoding` để cache đúng Brotli vs Gzip.
Redis và Memcached chọn cái nào?
Mặc định: Redis. Lý do: (1) Hỗ trợ data structure đa dạng (string, list, set, hash, sorted set, stream); (2) Persistence option (AOF + RDB); (3) Pub/Sub built-in; (4) Cluster + replication mature; (5) Roadmap active. Memcached chỉ có key-value string đơn giản — nhanh hơn Redis cho pure get/set nhưng thiếu features. Quy tắc: project mới → Redis. Project legacy đang dùng Memcached → giữ, không migrate vô lý. Cloud: Upstash Redis free tier 10k command/ngày, AWS ElastiCache có cả Redis + Memcached.
Service Worker và HTTP cache khác nhau?
HTTP cache (browser) tuân theo Cache-Control header server gửi. Service Worker là code JavaScript chạy trong browser, intercept network request, có thể trả response từ cache custom (cacheStorage API). Service Worker mạnh hơn: (1) Hoạt động offline; (2) Logic cache custom (cache by query param, fallback strategy); (3) Background sync. Trade-off: phức tạp setup, debug khó. Use case: PWA, offline-first app, e-commerce với inventory cache. Workbox là library Google làm Service Worker easier.
Khi nào KHÔNG nên cache?
5 trường hợp cấm cache HTML: (1) Dashboard/admin có data per-user; (2) Cart, checkout, payment — data thay đổi mỗi second; (3) Form auth (login, register, reset password); (4) Realtime data (chat, stock price, score); (5) Admin actions có CSRF token. Cách: gửi header `Cache-Control: private, no-cache, no-store, must-revalidate` + `Pragma: no-cache`. KHÔNG dựa vào CDN tự bypass — đôi khi cache vẫn cache theo URL nếu config sai. Đo lường: nếu user A đăng nhập tài khoản B → bug cache critical.