Skip to content
DevRecall
← Roadmap
16

Caching

drafted

Cache patterns, invalidation, stampede/penetration/hot key, Redis internals, Redis vs Memcached, HTTP/CDN cache, multi-tenant cache keys.

0/62 rated · mastery 0%

Bài học

13 bài · ~165 phút đọc. Học theo thứ tự, rồi làm câu hỏi.
  1. 01~14 phút· 6 câuverify

    Nền tảng caching: hit ratio, TTL và cái gì nên cache

    Cache là bản sao có thể sai của dữ liệu gốc: các tầng cache, hit ratio và cách đo đúng trên Redis, chọn TTL theo staleness budget, dữ liệu không nên cache, và cách chứng minh (hoặc gỡ) một cache — kèm số đo thật trên Redis 8.

  2. 02~12 phút· 6 câuverify

    Các pattern: cache-aside, read-through, write-through, write-behind, refresh-ahead

    Read path và write path của năm pattern cache phổ biến, logic nằm ở đâu, pattern nào có thể mất dữ liệu, vì sao không pattern nào cho strong consistency, và cách làm write-behind an toàn khi retry — kèm output chạy thật trên Redis 8.

  3. 03~13 phút· 5 câuverify

    Invalidation & consistency: delete vs update, race và replica lag

    Vì sao delete-on-write là mặc định, DEL trước/sau/trong transaction khác nhau thế nào, interleaving làm bản cũ quay lại cache dù đã DEL sau commit, delayed double delete và version-checked SET bằng Lua, và bug replica lag + cache — tái hiện bằng code chạy thật.

  4. 04~13 phút· 5 câuverify

    Invalidation ở quy mô lớn: CDC, outbox, namespace version và tag

    Khi nhiều service, batch job và hệ thống legacy cùng ghi một dữ liệu: invalidation dựa trên CDC (Postgres logical decoding) và outbox, map một row thành nhiều cache key, namespace version và tag cho trang listing, sync hàng loạt từ provider ngoài, và Elasticsearch như một read model có độ trễ — với output chạy thật.

  5. 05~13 phút· 6 câuverify

    Cache stampede: singleflight, lock, stale-while-revalidate, XFetch

    Hot key hết hạn làm hàng nghìn request cùng đánh DB: phân biệt stampede với penetration/avalanche, đo số query thật khi có và không có singleflight và distributed lock trên 5 pod, stale-while-revalidate ở tầng app, probabilistic early expiration (XFetch) với mô phỏng, và cách xử lý sự cố lúc đang cháy.

  6. 06~13 phút· 4 câuverify

    Penetration, avalanche và cache lạnh khi deploy

    Request cho key không tồn tại (null caching, Bloom filter tự cài và BF.* của Redis 8), nhiều key hết hạn cùng lúc (TTL jitter, warm có kiểm soát), và cache lạnh sau deploy/restart (readiness chờ warm, version key) — với số đo false positive, memory và phân bố expiry chạy thật.

  7. 07~14 phút· 4 câuverify

    Cache nhiều tầng và khi Redis chậm hoặc chết

    In-process vs distributed cache trong Node nhiều replica, thiết kế L1 + L2 và giữ L1 không stale quá lâu (TTL ngắn, Pub/Sub, client-side caching RESP3), timeout/offline queue của ioredis, circuit breaker, fail-open vs fail-closed, bulkhead trước DB, và kịch bản Redis sập giờ cao điểm — với số đo thật khi Redis treo và chết.

  8. 08~13 phút· 6 câuverify

    Redis core: single thread, data structures, Lua, Pub/Sub vs Streams, Memcached

    Vì sao Redis nhanh dù thực thi lệnh trên một luồng và hệ quả khi dùng (lệnh O(N), Lua dài), các cấu trúc dữ liệu và use case production, atomicity với Lua so với MULTI/EXEC, Pub/Sub (at-most-once) so với Streams (consumer group, ack), và chọn Redis, Memcached hay Valkey — với output chạy thật trên Redis 8 và Memcached 1.6.

  9. 09~13 phút· 5 câuverify

    Memory, eviction và persistence của Redis

    Expiration khác eviction thế nào, các maxmemory-policy (LRU/LFU xấp xỉ, allkeys vs volatile, và LRM mới ở Redis 8.x), lỗi OOM khi noeviction và cách chẩn đoán, fragmentation, RDB (fork + copy-on-write) so với AOF (multi-part), và những câu hỏi phải đặt ra trước khi dùng Redis làm primary store cho giỏ hàng/session — với số đo eviction thật trên Redis 8.

  10. 10~13 phút· 5 câuverify

    Redis Cluster, hot key và big key

    Hash slot và CRC16, MOVED/ASK, CROSSSLOT và hash tag, Sentinel vs Cluster vs managed (ElastiCache/MemoryDB/Valkey), phát hiện và xử lý hot key (L1, nhân bản key, replica, HOTKEYS của Redis 8.6), big key (HGETALL/DEL block, UNLINK, chia bucket) và endpoint dọn cache dùng KEYS — trên cluster 3 node chạy thật.

  11. 11~13 phút· 4 câuverify

    Distributed lock với Redis và giới hạn của nó

    Lock đơn instance đúng cách (SET NX PX + token + compare-and-delete), vì sao SETNX + EXPIRE + DEL làm hai worker cùng chạy, TTL và watchdog, Redlock và phê bình của Kleppmann, fencing token ở tầng lưu trữ, và bảo vệ tồn kho trong flash sale bằng Lua reservation hoặc UPDATE có điều kiện — tái hiện bằng Redis 8 và PostgreSQL 16.

  12. 12~11 phút· 4 câuverify

    HTTP & CDN caching cho backend

    Cache-Control nhìn từ phía backend: no-store/no-cache/private/max-age=0, s-maxage cho shared cache, ETag/Last-Modified và 304, Vary và cache key, stale-while-revalidate/stale-if-error, chiến lược asset có hash vs HTML khi deploy, và bug CDN trả giá của tenant khác — tái hiện bằng một shared cache nhỏ chạy thật.

  13. 13~12 phút· 6 câuverify

    Cache key design và multi-tenant caching

    Key phải chứa mọi dimension làm output khác nhau: đặt tên, normalize và hash query, schema version, helper tạo key tập trung với tenant từ context đã xác thực, test chống leak giữa tenant, debug helper cache-aside làm trang rỗng và rò tenant, thiết kế cache cho catalog multi-tenant (giá, khuyến mãi, visibility), noisy neighbor, và cách kể câu chuyện Redis trong CV.

Tóm tắt & cheat sheet của track

TL;DR

  • Cache là bản sao có thể sai của dữ liệu gốc. Mọi quyết định cache thực chất là trả lời 3 câu: được phép stale bao lâu?, invalidate thế nào?, khi cache chết thì hệ thống sống ra sao?
  • Pattern phổ biến nhất ở backend Node/TS là cache-aside + TTL + delete-on-write. Các pattern khác (read-through, write-through, write-behind, refresh-ahead) là biến thể đổi latency/consistency/complexity.
  • Senior interview xoáy vào failure modes: stampede (thundering herd), penetration, avalanche, hot key, big key, race giữa writer và reader, Redis down → DB sập theo.
  • Redis không chỉ là key-value: data structures, persistence (RDB/AOF), eviction policy, Cluster 16384 slots, Lua atomic, pub/sub, lock (và giới hạn của Redlock).
  • Multi-tenant: tenant id phải nằm trong cache key (và trong CDN cache key / Vary), nếu không là data leak chứ không chỉ là bug.

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
Cache-aside (lazy loading)App tự đọc cache → miss thì đọc DB rồi set cacheget(k) ?? set(k, await db.find(), ttl)
Read-throughCache layer/library tự load từ DB khi missDataLoader/cache library có loader function
Write-throughGhi DB và cache đồng bộ trong cùng requestUpdate profile → db.update rồi cache.set
Write-behind (write-back)Ghi cache trước, flush DB bất đồng bộCounter view count gom batch mỗi 5s
Refresh-aheadLàm mới key trước khi hết hạnCòn 10% TTL thì background refresh
TTL + jitterHạn sống + ngẫu nhiên hoá để key không hết hạn cùng lúcttl = 300 + rand(0..60)
Stampede / thundering herdHot key hết hạn → N request cùng miss và cùng đánh DBTrang chủ hết hạn lúc 12:00:00
PenetrationQuery key không tồn tại → luôn miss → luôn đánh DBGET /product/-1 bị bot spam
AvalancheNhiều key hết hạn cùng lúc hoặc cache cluster chếtWarm cache lúc deploy với cùng TTL
Hot key / big key1 key nhận quá nhiều traffic / 1 key quá lớnconfig:global 200k rps; hash 2GB
Negative cachingCache cả kết quả "không có" với TTL ngắnset("p:-1", NULL_SENTINEL, 30)
Eviction vs expirationEviction = bị đuổi vì hết memory; expiration = hết TTLmaxmemory-policy allkeys-lru

Cơ chế hoạt động

Cache-aside read + delete-on-write

Chống stampede (quyết định khi miss)

Redis Cluster routing

  • slot = CRC16(key) mod 16384; mỗi master giữ một dải slot. Client cluster-aware cache bảng slot và xử lý MOVED/ASK.
  • Multi-key command (MGET, Lua, MULTI) chỉ chạy khi các key cùng slot → dùng hash tag {tenant:acme}:cart (chỉ phần trong {} được hash). Lạm dụng hash tag = dồn hết vào một node (hot shard).

Trade-offs

PatternRead latencyWrite latencyConsistencyRủi ro chínhHợp khi
Cache-asideMiss chậm (2 round-trip)Không đổiEventual, cửa sổ stale = TTL hoặc tới lúc DELRace reader/writer, stampedeRead-heavy, default choice
Read-throughNhư cache-asideKhông đổiEventualPhụ thuộc library/providerMuốn gom logic load vào 1 chỗ
Write-throughHit rate cao sau writeTăng (2 lần ghi)Tốt hơn nhưng không atomic giữa DB và cacheCache chứa data không ai đọc; lỗi nửa chừngData vừa ghi được đọc ngay
Write-behindNhanhRất nhanhYếu; DB trễMất dữ liệu nếu cache chết trước flushCounter, metrics, chấp nhận mất
Refresh-aheadLuôn hit với hot keyKhông đổiVẫn stale trong khoảng refreshRefresh cả key không còn ai dùngHot key dự đoán được
RedisMemcached
Data modelString, Hash, List, Set, ZSet, Stream, Bitmap, HyperLogLog, Geo, JSON (module/8.x)Chỉ string/blob
ThreadingCommand execution đơn luồng (I/O threads từ 6.0)Multi-threaded
Persistence / replicationRDB, AOF, replica, Sentinel, ClusterKhông (thuần cache)
EvictionNhiều policy cấu hình đượcLRU theo slab class
HợpCần data structure, atomic op, lock, pub/sub, rate limitCache blob đơn giản, scale theo core, ít vận hành

Pitfalls

  • Update cache thay vì delete khi write → hai writer đồng thời có thể để lại giá trị cũ trong cache vĩnh viễn (đến hết TTL).
  • Không có TTL "vì sẽ invalidate khi write" → bất kỳ đường ghi nào quên DEL (script, migration, service khác) = stale mãi mãi.
  • Cache lỗi / undefined / response rỗng do timeout như dữ liệu thật.
  • Key thiếu tenantId, locale, currency, role → user A thấy dữ liệu user B.
  • Dùng KEYS * hoặc DEL một big key trên production → block event loop của Redis.
  • Coi Redis là "luôn sống": không timeout, không circuit breaker → Redis chậm kéo toàn bộ API chậm theo; Redis chết → 100% traffic đổ vào DB.
  • Distributed lock bằng SET NX rồi DEL không kiểm tra owner → xoá lock của người khác.
  • HTTP: Cache-Control: public cho response có cookie/Authorization → CDN cache dữ liệu cá nhân.

Cheat sheet

  • Default: cache-aside + TTL có jitter + DEL sau khi commit DB + singleflight trong process.
  • Stampede: singleflight · lock SET k v NX PX · stale-while-revalidate · refresh-ahead · probabilistic early expiration (XFetch).
  • Penetration: validate input · cache null TTL ngắn · Bloom filter. Avalanche: jitter TTL · multi-level cache · HA cluster · rate limit tới DB.
  • Hot key: L1 in-process cache vài giây · nhân bản key k#1..k#N · client-side caching (RESP3 tracking).
  • Big key: redis-cli --bigkeys / --memkeys, MEMORY USAGE, chia nhỏ, UNLINK thay DEL.
  • Eviction: default noeviction; cache thuần → allkeys-lru hoặc allkeys-lfu; volatile-* chỉ đuổi key có TTL.
  • Persistence: RDB = snapshot (fork + copy-on-write), AOF = log ghi (appendfsync everysec mặc định). Cache thuần có thể tắt cả hai.
  • Lock: SET lock:k <uuid> NX PX 10000; release bằng Lua compare-and-delete; việc quan trọng cần fencing token.
  • HTTP: no-cache = được lưu nhưng phải revalidate; no-store = không lưu; s-maxage cho shared cache; ETag → 304; Vary nằm trong cache key.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 62 / 62

async function getProduct(tenantId: string, id: string): Promise<Product | null> {
  const key = `t:${tenantId}:product:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
  const row = await db.product.findFirst({ where: { tenantId, id } });
  if (row) await redis.set(key, JSON.stringify(row), "EX", 300 + Math.floor(Math.random() * 60));
  return row;
}
t0 Reader: GET k -> miss
t1 Reader: SELECT -> v1
t2 Writer: UPDATE -> v2, COMMIT
t3 Writer: DEL k
t4 Reader: SET k v1 EX 300   <-- stale for up to 300s
const inflight = new Map<string, Promise<unknown>>();
export function singleflight<T>(key: string, fn: () => Promise<T>): Promise<T> {
  const existing = inflight.get(key);
  if (existing) return existing as Promise<T>;
  const p = fn().finally(() => inflight.delete(key));
  inflight.set(key, p);
  return p;
}
// usage: await singleflight(key, () => loadFromDbAndSet(key))
const ttl = (base: number) => base + Math.floor(Math.random() * base * 0.2);
await redis.set(key, value, "EX", ttl(3600));
type Entry<T> = { v: T; softExp: number };
async function swr<T>(key: string, load: () => Promise<T>, softSec = 60, hardSec = 600) {
  const raw = await redis.get(key);
  if (raw) {
    const e: Entry<T> = JSON.parse(raw);
    if (Date.now() > e.softExp && (await redis.set(`${key}:lock`, "1", "PX", 10_000, "NX"))) {
      void refresh(key, load, softSec, hardSec).catch(log.warn); // background, never awaited by user
    }
    return e.v;
  }
  return refresh(key, load, softSec, hardSec);
}
// set only if incoming version is newer (prevents stale overwrite)
const SET_IF_NEWER = `
  local cur = redis.call('HGET', KEYS[1], 'ver')
  if (not cur) or tonumber(ARGV[1]) > tonumber(cur) then
    redis.call('HSET', KEYS[1], 'ver', ARGV[1], 'data', ARGV[2])
    redis.call('EXPIRE', KEYS[1], ARGV[3])
    return 1
  end
  return 0`;
await redis.eval(SET_IF_NEWER, 1, key, String(row.version), JSON.stringify(row), "300");
const token = crypto.randomUUID();
const ok = await redis.set("lock:rebuild:home", token, "PX", 10_000, "NX");
if (ok) {
  try { await rebuild(); } finally {
    await redis.eval(
      "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end",
      1, "lock:rebuild:home", token);
  }
}
const key = (t: string, id: string, locale: string) => `shop:${t}:product:${id}:${locale}:v3`;
// list with filters -> normalize then hash
const listKey = (t: string, q: Record<string, string>) =>
  `shop:${t}:plp:${sha1(JSON.stringify(Object.entries(q).sort()))}:v3`;
async function getProducts(tenantId: string, category: string) {
  const key = `products:${category}`;
  const hit = await redis.get(key);
  if (hit !== null) return JSON.parse(hit);
  let rows: Product[] = [];
  try {
    rows = await withTimeout(db.products.find({ tenantId, category }), 500);
  } catch (e) {
    log.warn("db slow", e);
  }
  await redis.set(key, JSON.stringify(rows), "EX", 300);
  return rows;
}
async function runExclusive(job: () => Promise<void>) {
  const acquired = await redis.setnx("lock:nightly", "1");
  if (!acquired) return;
  await redis.expire("lock:nightly", 60);
  try {
    await job(); // sometimes takes 3 minutes
  } finally {
    await redis.del("lock:nightly");
  }
}
function shouldRefresh(expiresAtMs: number, deltaMs: number, beta = 1) {
  return Date.now() - deltaMs * beta * Math.log(Math.random()) >= expiresAtMs;
}
app.get("/api/products/:id", auth, async (req, res) => {
  const p = await getProductWithPrice(req.tenantId, req.params.id);
  res.set("Cache-Control", "public, max-age=300");
  res.json(p);
});
// tenant resolved from Authorization header / subdomain cookie
app.post("/admin/cache/flush-tenant/:t", async (req, res) => {
  const keys = await redis.keys(`shop:${req.params.t}:*`);
  if (keys.length) await redis.del(...keys);
  res.sendStatus(204);
});