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;
}
Caching
draftedCache 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.- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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~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~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~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~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ệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Cache-aside (lazy loading) | App tự đọc cache → miss thì đọc DB rồi set cache | get(k) ?? set(k, await db.find(), ttl) |
| Read-through | Cache layer/library tự load từ DB khi miss | DataLoader/cache library có loader function |
| Write-through | Ghi DB và cache đồng bộ trong cùng request | Update 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-ahead | Làm mới key trước khi hết hạn | Còn 10% TTL thì background refresh |
| TTL + jitter | Hạn sống + ngẫu nhiên hoá để key không hết hạn cùng lúc | ttl = 300 + rand(0..60) |
| Stampede / thundering herd | Hot key hết hạn → N request cùng miss và cùng đánh DB | Trang chủ hết hạn lúc 12:00:00 |
| Penetration | Query key không tồn tại → luôn miss → luôn đánh DB | GET /product/-1 bị bot spam |
| Avalanche | Nhiều key hết hạn cùng lúc hoặc cache cluster chết | Warm cache lúc deploy với cùng TTL |
| Hot key / big key | 1 key nhận quá nhiều traffic / 1 key quá lớn | config:global 200k rps; hash 2GB |
| Negative caching | Cache cả kết quả "không có" với TTL ngắn | set("p:-1", NULL_SENTINEL, 30) |
| Eviction vs expiration | Eviction = bị đuổi vì hết memory; expiration = hết TTL | maxmemory-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
| Pattern | Read latency | Write latency | Consistency | Rủi ro chính | Hợp khi |
|---|---|---|---|---|---|
| Cache-aside | Miss chậm (2 round-trip) | Không đổi | Eventual, cửa sổ stale = TTL hoặc tới lúc DEL | Race reader/writer, stampede | Read-heavy, default choice |
| Read-through | Như cache-aside | Không đổi | Eventual | Phụ thuộc library/provider | Muốn gom logic load vào 1 chỗ |
| Write-through | Hit rate cao sau write | Tăng (2 lần ghi) | Tốt hơn nhưng không atomic giữa DB và cache | Cache chứa data không ai đọc; lỗi nửa chừng | Data vừa ghi được đọc ngay |
| Write-behind | Nhanh | Rất nhanh | Yếu; DB trễ | Mất dữ liệu nếu cache chết trước flush | Counter, metrics, chấp nhận mất |
| Refresh-ahead | Luôn hit với hot key | Không đổi | Vẫn stale trong khoảng refresh | Refresh cả key không còn ai dùng | Hot key dự đoán được |
| Redis | Memcached | |
|---|---|---|
| Data model | String, Hash, List, Set, ZSet, Stream, Bitmap, HyperLogLog, Geo, JSON (module/8.x) | Chỉ string/blob |
| Threading | Command execution đơn luồng (I/O threads từ 6.0) | Multi-threaded |
| Persistence / replication | RDB, AOF, replica, Sentinel, Cluster | Không (thuần cache) |
| Eviction | Nhiều policy cấu hình được | LRU theo slab class |
| Hợp | Cần data structure, atomic op, lock, pub/sub, rate limit | Cache 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ặcDELmộ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 NXrồiDELkhông kiểm tra owner → xoá lock của người khác. - HTTP:
Cache-Control: publiccho 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ỏ,UNLINKthayDEL. - Eviction: default
noeviction; cache thuần →allkeys-lruhoặcallkeys-lfu;volatile-*chỉ đuổi key có TTL. - Persistence: RDB = snapshot (fork + copy-on-write), AOF = log ghi (
appendfsync everysecmặ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-maxagecho shared cache;ETag→304;Varynằm trong cache key.
References
- Redis docs — Key eviction ↗
- Redis docs — Persistence (RDB / AOF) ↗
- Redis docs — Distributed locks with Redis ↗
- Redis docs — Cluster specification ↗
- Redis docs — Client-side caching ↗
- Martin Kleppmann — How to do distributed locking ↗
- AWS Builders' Library — Caching challenges and strategies ↗
- RFC 9111 — HTTP Caching ↗
- RFC 5861 — stale-while-revalidate / stale-if-error ↗
- MDN — Cache-Control ↗
- Memcached wiki ↗
- Vattani et al. — Optimal Probabilistic Cache Stampede Prevention (VLDB 2015) ↗
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
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);
});