Danh sách bài viết

Bài 19: Checklist & Anti-patterns Caching — Bài Học Từ Incident KEYS *

Bài cuối Module 1 tổng kết toàn bộ Caching Trong Production qua hai phần chính. Phần đầu là incident thật: lệnh KEYS * chạy trên production Redis với 50M key, block toàn bộ command trong 8 phút, làm P99 latency từ 5ms vọt lên 8 giây, kéo theo cascade auto-scaling và spike chi phí cloud. Phần sau là checklist production-ready và top 10 anti-pattern rút gọn từ bài 9 đến 18, kèm decision matrix chọn caching pattern cho từng use case và self-assessment trước khi qua Module 2.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

  • Hiểu vì sao KEYS * là command nguy hiểm nhất trên Redis production và diễn biến cụ thể khi nó gây incident.
  • Biết dùng SCAN thay KEYS: cơ chế cursor, các trade-off (không snapshot, có thể duplicate/miss) và cách lặp đến hết.
  • Nhận diện 10 anti-pattern caching phổ biến nhất và hiểu tác hại cụ thể của từng cái.
  • Sử dụng checklist production-ready như một công cụ review: key design, TTL, stampede defense, invalidation, monitoring, safety.
  • Chọn đúng caching pattern cho từng use case dựa trên decision matrix.
  • Tự đánh giá được mức hiểu Module 1 trước khi qua Module 2 Data Structures.
2

Incident: KEYS * Trên Production Redis 50M Key

Bối cảnh

Backend team mới deploy một script debug để "tìm tất cả key có pattern X" phục vụ điều tra lỗi data. Ngày hôm sau, khi đang xử lý bug trên production, một developer chạy:

redis-cli KEYS "user:*"

Điều không may: developer đang kết nối shell vào production Redis — không phải môi trường dev như tưởng. Database lúc đó có ~50 triệu key. Thời điểm: 14:00 giờ cao điểm.

Diễn biến

KEYS là lệnh O(N) — nó quét tuần tự toàn bộ keyspace để lọc theo pattern. Redis đơn luồng (single-threaded event loop): trong khi KEYS đang chạy, mọi command khác phải chờ. Với 50M key, lệnh này chiếm event loop khoảng 3-5 giây tùy cấu hình máy chủ.

Tuy nhiên hậu quả không dừng ở đó:

  • 14:00 – 14:08: KEYS block event loop. Toàn bộ GET/SET từ application bị queue. P99 latency vào Redis từ 5ms vọt lên 8 giây. Các API có timeout 5 giây bắt đầu trả lỗi hàng loạt.
  • 14:01: Load balancer phát hiện health check timeout, đánh dấu một số instance unhealthy.
  • 14:02: Auto-scaling trigger vì error rate cao → spawn thêm 50 instance mới. Các instance mới khởi động → cache cold → toàn bộ miss → đổ thẳng vào database.
  • 14:03 – 14:07: Database quá tải do traffic từ 50 instance mới không có cache. Connection pool exhausted. Cascade thứ hai bắt đầu.
  • 14:08: KEYS trả xong kết quả (khoảng 2.1M key), event loop Redis giải phóng. Nhưng lúc này database đang ngộp và cache vẫn cold trên các instance mới.
  • 14:15: Sau khi database hồi phục từng phần, cache dần warm lại, hệ thống trở về bình thường. Tổng thời gian degraded: ~15 phút. Chi phí cloud tháng đó tăng do 50 instance thừa chạy không tắt kịp.

Tóm tắt timeline

14:00  KEYS "user:*" chạy trên Redis production 50M key
       → event loop bị chiếm, mọi command queue lại

14:00–14:08  P99 latency: 5ms → 8s
             API timeout hàng loạt

14:01  health check fail → instance bị đánh dấu unhealthy

14:02  auto-scaling: +50 instance mới
       → cache cold → 100% miss → database quá tải

14:08  KEYS trả xong, Redis event loop giải phóng
       nhưng DB vẫn quá tải, cache vẫn cold

14:15  hệ thống hồi phục dần sau khi DB ổn định và cache warm
3

Root Cause & Fix

Root cause

  • Kỹ thuật: KEYS được document là "không nên dùng trong production" từ Redis 2.x. Redis docs ghi rõ: "Warning: consider KEYS as a command that should only be used in production environments with extreme care." Lệnh này không bao giờ được thiết kế để chạy trên keyspace lớn trong production.
  • Quy trình: Developer đọc tutorial cũ, không kiểm tra Redis documentation hiện tại. Script debug không qua code review.
  • Access control: Production và dev environment dùng chung credential hoặc không có network isolation rõ ràng — developer kết nối nhầm shell vào production.

Fix ngay lập tức

# Ngắt lệnh đang chạy nếu còn (dùng CLIENT LIST để tìm)
redis-cli CLIENT LIST
redis-cli CLIENT KILL ID <client_id>

# Nếu Redis dùng Redis 7.x: dùng LOLWUT để check memory, không dùng KEYS
redis-cli MEMORY DOCTOR

Fix dài hạn — 3 tầng

Tầng 1 — Disable lệnh nguy hiểm trên production

Trong file redis.conf:

# Rename về chuỗi rỗng = disable hoàn toàn
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command DEBUG ""
rename-command CONFIG ""   # hoặc rename thành chuỗi bí mật cho ops

Sau khi reload config, gọi KEYS sẽ trả về ERR unknown command 'KEYS'. Lệnh không thể chạy dù ai cố tình hay vô ý.

Tầng 2 — Thay KEYS bằng SCAN

Mọi script cần duyệt key theo pattern phải dùng SCAN (chi tiết ở mục 4).

Tầng 3 — Network isolation

Production Redis chỉ cho phép kết nối từ application servers qua VPC/private network. Không có đường kết nối trực tiếp từ máy developer. Mọi debug phải qua bastion host có audit log.

Các lệnh O(N) khác cần phòng tương tự

Không chỉ KEYS. Mọi lệnh O(N) đều có thể block event loop trên keyspace/value lớn:

  • SMEMBERS key — trả toàn bộ Set: nếu Set có hàng triệu member → block dài. Thay bằng SSCAN.
  • HGETALL key — trả toàn bộ Hash: nếu Hash có hàng trăm nghìn field → block. Thay bằng HSCAN.
  • LRANGE key 0 -1 — trả toàn bộ List dài → block. Dùng phân trang với offset.
  • SORT key trên List/Set lớn — O(N+M log M) → rất chậm.
  • ZRANGEBYSCORE key -inf +inf không giới hạn trên ZSet lớn.

Nguyên tắc chung: bất kỳ lệnh nào trả về tập lớn hoặc quét toàn bộ cấu trúc đều phải được phân trang (cursor-based hoặc LIMIT).

4

SCAN vs KEYS — Cơ Chế & Code

SCAN được thiết kế để duyệt keyspace mà không block server. Thay vì quét toàn bộ một lần, nó chia công việc thành nhiều iteration nhỏ thông qua cursor.

Syntax cơ bản

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

# Bắt đầu với cursor = 0
redis-cli SCAN 0 MATCH "user:*" COUNT 100
# Trả về: 1) "cursor_mới"  2) ["user:1", "user:42", ...]
#
# Tiếp tục với cursor mới cho đến khi cursor trả về = "0"
redis-cli SCAN 3821 MATCH "user:*" COUNT 100
# ...
# Khi trả về "0" là đã duyệt hết

COUNT là gợi ý số lượng Redis xử lý mỗi lần — không phải số key trả về. Redis có thể trả ít hơn hoặc nhiều hơn. Tăng COUNT giảm số round-trip nhưng mỗi call tốn thêm thời gian một chút.

SCAN trong TypeScript (ioredis)

import Redis from "ioredis";

const redis = new Redis({ host: "127.0.0.1", port: 6379 });

/**
 * Duyệt toàn bộ key khớp pattern bằng SCAN cursor.
 * Không block event loop Redis — mỗi iteration chỉ xử lý ~100 key.
 */
async function scanKeys(pattern: string): Promise<string[]> {
  const keys: string[] = [];
  let cursor = "0";

  do {
    // [cursor_mới, [key1, key2, ...]]
    const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
    cursor = nextCursor;
    keys.push(...batch);
  } while (cursor !== "0");

  return keys;
}

// Ví dụ: xóa tất cả key user session hết hạn theo pattern
async function deleteByPattern(pattern: string): Promise<number> {
  const keys = await scanKeys(pattern);
  if (keys.length === 0) return 0;

  // Xóa theo batch để tránh pipeline quá lớn
  const BATCH = 500;
  let deleted = 0;
  for (let i = 0; i < keys.length; i += BATCH) {
    const batch = keys.slice(i, i + BATCH);
    deleted += await redis.del(...batch);
  }
  return deleted;
}

SCAN trong Python (redis-py)

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


def scan_keys(pattern: str) -> list[str]:
    """Duyệt key theo pattern bằng SCAN cursor — không block Redis."""
    keys = []
    cursor = 0

    while True:
        cursor, batch = r.scan(cursor, match=pattern, count=100)
        keys.extend(batch)
        if cursor == 0:
            break

    return keys


def delete_by_pattern(pattern: str) -> int:
    """Xóa tất cả key khớp pattern theo batch 500."""
    keys = scan_keys(pattern)
    if not keys:
        return 0

    deleted = 0
    batch_size = 500
    for i in range(0, len(keys), batch_size):
        batch = keys[i : i + batch_size]
        deleted += r.delete(*batch)

    return deleted

redis-py cũng cung cấp helper scan_iter(match=pattern) trả về iterator, cho phép xử lý key ngay khi nhận mà không cần load hết vào memory:

for key in r.scan_iter(match="user:*", count=100):
    # xử lý từng key ngay lập tức
    process(key)

Trade-off của SCAN cần biết

  • Không phải snapshot: trong khi SCAN đang chạy, key có thể được thêm hoặc xóa. SCAN có thể miss key mới tạo ra sau khi iteration bắt đầu, và có thể trả duplicate key nếu keyspace bị resize (rehash) giữa chừng.
  • Không guarantee thứ tự: thứ tự key trả về không xác định.
  • COUNT chỉ là gợi ý: trên keyspace nhỏ Redis có thể trả toàn bộ key trong một lần dù COUNT nhỏ.
  • Vẫn tốn thời gian tổng cộng: SCAN toàn bộ 50M key vẫn phải đọc 50M key — chỉ là trải đều qua nhiều lần gọi, mỗi lần không block lâu. Tổng chi phí giống KEYS về CPU, nhưng impact trên latency của command khác được giảm tối thiểu.
5

Top 10 Caching Anti-patterns

Dưới đây là 10 anti-pattern phổ biến nhất rút từ thực tế production, sắp xếp theo mức độ nguy hiểm từ cao xuống thấp.

1. KEYS * (hoặc bất kỳ lệnh O(N) toàn keyspace)

Tác hại: block event loop Redis từ giây đến phút trên keyspace lớn. Mọi command khác phải chờ → latency spike toàn hệ thống. Xem incident ở mục 2.
Fix: dùng SCAN, SSCAN, HSCAN, ZSCAN; rename-command KEYS "" trên production.

2. Key không có TTL

Tác hại: memory tăng không có giới hạn. Khi invalidation lỡ sót dù một lần, dữ liệu stale tồn tại vĩnh viễn. Khi Redis đầy memory và maxmemory-policynoeviction, mọi write đều trả lỗi.
Fix: mọi key phải có TTL, kể cả "dữ liệu không đổi" — đặt TTL dài (vd 7 ngày) như fail-safe.

3. TTL quá dài cho dữ liệu hay thay đổi

Tác hại: user đọc được dữ liệu sai trong khoảng thời gian dài. TTL 24 giờ cho bảng giá sản phẩm → giá sai suốt một ngày sau khi update.
Fix: TTL phải tương ứng với mức chấp nhận stale của từng loại dữ liệu. Bảng giá: 60-300s. Cấu hình hệ thống: 1-6h.

4. TTL quá ngắn cho dữ liệu ổn định

Tác hại: cache expiry liên tục trước khi được đọc lại → hit rate thấp → DB chịu tải không cần thiết. TTL 1s cho hồ sơ user tải mỗi lần page load → cache vô nghĩa.
Fix: đo hit rate thực tế. Nếu hit rate thấp dù traffic cao, TTL có thể quá ngắn hoặc data không phù hợp cache.

5. Cache "everything" không có chiến lược

Tác hại: RAM tốn cao cho dữ liệu ít được đọc lại. Hit rate thấp. Eviction policy xóa nhầm dữ liệu hot để nhường chỗ cho dữ liệu cold vừa được cache.
Fix: cache chỉ hot data (đọc nhiều, tốn kém khi tính, thay đổi ít). Cache-aside tự lọc hot data theo nhu cầu thực nếu không warm-up thủ công bừa bãi.

6. Không có invalidation strategy

Tác hại: cache ngày càng phân kỳ khỏi database. User đọc được dữ liệu ngày càng sai hơn theo thời gian. Trong trường hợp xấu: dữ liệu sai được phục vụ đến khi Redis restart.
Fix: chọn ít nhất một trong ba: TTL-only (đơn giản nhất), DELETE on write, event-driven invalidation qua CDC. Không để mặc định "tự xử lý sau".

7. Hot key không được nhận diện

Tác hại: 1 key bị đọc bởi hàng chục nghìn request/giây. Trên Redis Cluster, toàn bộ tải đổ lên 1 node → bottleneck. Xem bài 18 về hot key detection.
Fix: dùng redis-cli --hotkeys hoặc OBJECT FREQ key, theo dõi hit counter ở application level, shard hot key nếu cần.

8. Cache miss không có lock — cache stampede

Tác hại: hot key hết hạn → hàng nghìn request đồng thời miss → cùng query DB → DB quá tải → cascade. Xem bài 14 về cache stampede.
Fix: mutex lock (SET NX EX) cho expensive rebuild, TTL jitter, hoặc stale-while-revalidate (bài 15).

9. Cache value quá lớn

Tác hại: value > 100KB tốn nhiều bandwidth mạng mỗi lần đọc. Serialize/deserialize tốn CPU. Nếu value > 512MB là hard limit của Redis String. Một key lớn cũng tốn thời gian xử lý và có thể gây latency spike nhỏ.
Fix: cache chỉ những field cần thiết, không cache toàn bộ object. Tách thành nhiều key nhỏ nếu cần. Cân nhắc compression cho value > 10KB.

10. Không có fallback khi Redis down

Tác hại: Redis lỗi → toàn bộ API trả 500 dù database vẫn hoạt động. Application treat cache như required dependency thay vì optional accelerator.
Fix: wrap tất cả Redis call trong try/catch với fallback đọc thẳng DB. Theo dõi Redis health riêng biệt. Circuit breaker để ngắt Redis tự động khi lỗi liên tục.

6

Checklist Production-Ready

Dùng checklist này như một công cụ review trước khi deploy caching layer mới hoặc audit caching layer hiện tại.

Architecture

  • [ ] Pattern caching đã được chọn rõ ràng (cache-aside / write-through / SWR / TTL-only) và documented.
  • [ ] Fallback khi Redis down: application vẫn đọc được DB, chỉ chậm hơn.
  • [ ] Circuit breaker cho Redis: tự động ngắt sau N lỗi liên tiếp, tự thử reconnect sau cooldown.

Key design

  • [ ] Naming convention nhất quán trong toàn codebase: entity:id hoặc entity:id:field.
  • [ ] Version trong key khi schema thay đổi: user:123:v2user:123:v3 khi đổi format.
  • [ ] Độ dài key hợp lý: tránh key > 100 ký tự (tốn memory, tốn bandwidth so sánh).
  • [ ] Không nhúng thông tin nhạy cảm vào key name (key name có thể bị log).

TTL

  • [ ] Mọi key đều có TTL — kể cả dữ liệu "vĩnh viễn" (đặt TTL dài như fail-safe, vd 7 ngày).
  • [ ] TTL được chọn dựa trên volatility của dữ liệu, không dùng giá trị mặc định tùy tiện.
  • [ ] TTL jitter cho hot data để tránh mass expiry đồng thời gây stampede.
  • [ ] TTL được review lại ít nhất một lần sau khi có dữ liệu hit/miss thực tế.

Cache stampede defense

  • [ ] Expensive DB query có mutex lock (SET NX EX) — chỉ một worker rebuild, các worker khác chờ hoặc dùng stale.
  • [ ] Stale-while-revalidate cho hot path UX-critical: trả stale ngay, refresh nền.
  • [ ] Negative caching cho lookup hay miss: cache sentinel với TTL ngắn để không đập DB liên tục.

Invalidation

  • [ ] Strategy đã chọn và documented (TTL-only / DELETE on write / event-driven).
  • [ ] Race condition awareness: ghi DB trước, DELETE cache sau — không ghi cache trước rồi ghi DB.
  • [ ] Multi-key dependency được track: nếu update X làm cache Y, Z lỗi thời, DELETE cả Y, Z.
  • [ ] Không cache lỗi tạm thời (connection error, timeout từ DB) — chỉ cache kết quả hợp lệ.

Monitoring

  • [ ] Hit rate metric được đo và có target rõ ràng (vd > 80% cho hot data).
  • [ ] Miss rate alerting khi hit rate tụt đột ngột (có thể Redis restart hoặc bug invalidation).
  • [ ] Latency P95/P99 đến Redis được monitor (alert khi > ngưỡng, vd P99 > 10ms).
  • [ ] Hot key detection: dùng sampling (redis-cli --hotkeys) hoặc counter application-level.
  • [ ] Memory usage alert khi > 70-80% maxmemory.

Safety

  • [ ] rename-command KEYS "" trên production config.
  • [ ] rename-command FLUSHALL ""rename-command FLUSHDB "" trên production.
  • [ ] maxmemory-policy được set phù hợp — không để mặc định noeviction cho caching use case.
  • [ ] Big key detection routine định kỳ: redis-cli --bigkeys hoặc redis-cli --memkeys.
  • [ ] Slowlog được monitor: CONFIG SET slowlog-log-slower-than 10000 (10ms) và alert khi có entry.
  • [ ] Production Redis không accessible trực tiếp từ máy developer — phải qua bastion với audit log.
7

Decision Matrix — Chọn Pattern

Khi đứng trước một use case mới, câu hỏi không phải "dùng Redis không" mà là "dùng pattern nào". Bảng dưới tóm tắt dựa trên đặc điểm của workload.

Use case Pattern nên dùng Lý do
Đọc nhiều, write ít (hồ sơ user, catalog) Cache-aside + DELETE on write Đơn giản, resilient, chỉ cache hot data theo nhu cầu thực
Write-heavy nhưng đọc cần realtime (số lượng tồn kho) Write-through Cache luôn đồng bộ với DB sau mỗi write, không có stale window
Hot path UX-critical, cần P99 < 50ms Multi-layer + SWR Luôn có dữ liệu để trả (stale), refresh nền không block user
Data ổn định, consistency không quan trọng (static config) TTL-only Không cần invalidation logic, đơn giản nhất, ít bug nhất
Chống abuse / lookup hay miss (username check) Negative caching Cache kết quả không tồn tại để chặn DB lookup lặp lại
Invalidation phụ thuộc nhiều table (order thay đổi → 5 cache khác nhau) Event-driven (CDC hoặc message queue) Decoupled: service publish event, cache consumer tự invalidate
Hot key, traffic cực cao (> 100K req/s cùng 1 key) Local in-process cache + Redis (multi-layer) Giảm tải Redis bằng cách cache tại application memory

Trong thực tế, một hệ thống thường dùng nhiều pattern cùng lúc cho các loại dữ liệu khác nhau. Điều quan trọng là quyết định có chủ ý và documented, không phải dùng một pattern cho tất cả.

8

Module 1 — Bản Đồ Khái Niệm

Module 1 gồm 11 bài xây dựng từ nền tảng đến production engineering. Bảng dưới là bản đồ khái niệm để xem lại trước khi chuyển module.

Bài Khái niệm cốt lõi Câu hỏi kiểm tra nhanh
Bài 9 — Cache-Aside Lazy loading: check cache → hit trả ngay, miss → DB → ghi cache + TTL Vì sao DELETE cache khi update thay vì UPDATE trực tiếp?
Bài 10 — Write patterns Write-through, Write-behind, Read-through — ai ghi cache, khi nào Write-through khác Write-behind ở điểm gì về consistency và performance?
Bài 11 — Key design Naming convention, namespace, version, độ dài, compression Tại sao cần version trong key name khi đổi schema?
Bài 12 — TTL strategy Chọn TTL theo volatility, TTL như safety net, jitter Tại sao TTL vẫn cần dù đã có invalidation on write?
Bài 13 — Negative caching Cache kết quả không tồn tại để chặn cache penetration Rủi ro gì khi cache null kết quả từ DB đang lỗi tạm thời?
Bài 14 — Cache stampede Thundering herd: hot key expiry + concurrent miss → DB quá tải Mutex lock SET NX EX giải stampede thế nào? Vì sao lock cần TTL?
Bài 15 — Stale-while-revalidate Trả stale ngay, refresh nền — soft TTL và hard expire SWR khác mutex lock ở trade-off gì về latency vs freshness?
Bài 16 — Multi-layer cache In-process cache + Redis: giảm tải Redis cho hot key cực cao Invalidation L1 (local) khi Redis L2 thay đổi: cơ chế nào?
Bài 17 — Invalidation & consistency Race condition, thứ tự ghi, double-delete, eventual consistency Thứ tự nào an toàn hơn: ghi DB trước hay DELETE cache trước?
Bài 18 — Hot key Detect hot key bằng tool và app-level, các kỹ thuật phân tán tải Key shard và local replica cache khác nhau thế nào về trade-off?
Bài 19 — Checklist & Anti-patterns Incident KEYS *, top 10 anti-pattern, checklist production, decision matrix Bài này.

Nếu bất kỳ câu hỏi nào ở cột cuối mà bạn không trả lời được ngay, hãy đọc lại bài tương ứng trước khi qua Module 2.

9

Self-Assessment Trước Khi Qua Module 2

Đánh dấu từng mục nếu bạn có thể trả lời không cần tra cứu:

  • [ ] Giải thích được vì sao TTL-only (không có explicit invalidation) thường đủ cho 70% use case thực tế — và 30% còn lại cần gì thêm.
  • [ ] Biết khi nào chọn write-through vs cache-aside: đặc điểm workload nào thích hợp cho từng cái.
  • [ ] Mô tả được race condition stale-set trong cache-aside và ít nhất hai cách giảm thiểu.
  • [ ] Biết detect hot key bằng redis-cli --hotkeys, OBJECT FREQ, và app-level counter — và hiểu giới hạn của từng phương pháp.
  • [ ] Đã viết (hoặc đọc hiểu) code mutex lock SET NX EX cho cache stampede trong ít nhất một ngôn ngữ.
  • [ ] Có thể áp dụng checklist ở mục 6 để review một caching layer mới trong 15-20 phút.

Nếu chưa pass tất cả

Module 2 về Data Structures sẽ build on top của kiến thức Module 1. Không nhất thiết phải hoàn hảo 100%, nhưng ba mục tối thiểu phải nắm trước khi tiếp tục: TTL strategy (bài 12), cache stampede (bài 14), và invalidation race condition (bài 17). Các bài Module 2 sẽ dùng cache-aside như building block mặc định khi demo dữ liệu.

10

Tổng Kết & Quiz

Tổng kết

  • KEYS * trên Redis production là O(N) block toàn bộ event loop — disable bằng rename-command KEYS "" và thay bằng SCAN cursor.
  • SCAN chia nhỏ công việc qua nhiều iteration, không block server, nhưng không phải snapshot: có thể duplicate hoặc miss key đang thay đổi.
  • 10 anti-pattern cốt lõi: KEYS *, no TTL, TTL sai, cache everything, no invalidation, hot key không nhận diện, stampede không có lock, value quá lớn, no fallback — mỗi cái đều có hậu quả đo được trên production.
  • Checklist production-ready bao gồm 6 nhóm: architecture, key design, TTL, stampede defense, invalidation, monitoring, safety.
  • Không có pattern nào đúng cho mọi use case: decision matrix giúp chọn đúng dựa trên đặc điểm workload.

Quiz 5 câu

  1. Giải thích cơ chế tại sao KEYS * block toàn bộ Redis command trong khi đang chạy. Yếu tố kiến trúc nào của Redis gây ra điều này?
  2. SCAN có thể trả duplicate key. Trong trường hợp nào và tại sao? Điều này ảnh hưởng gì đến code sử dụng kết quả SCAN?
  3. Trong incident ở mục 2, cascade xảy ra theo hai bước. Mô tả từng bước và yếu tố nào trong kiến trúc (auto-scaling + cold cache) khuếch đại tác hại ban đầu.
  4. Một developer muốn cache kết quả query "danh sách 100 sản phẩm bán chạy nhất" với TTL 60 giây. Anti-pattern nào có thể xảy ra nếu traffic cao vào đúng thời điểm key hết hạn? Dùng kỹ thuật nào từ Module 1 để xử lý?
  5. Theo decision matrix, khi nào nên dùng event-driven invalidation thay vì DELETE on write? Cho ví dụ use case cụ thể.

Đáp án gợi ý

  1. Redis là single-threaded event loop: tại một thời điểm chỉ xử lý một command. KEYS là O(N), phải quét toàn bộ dictionary trong memory trước khi trả kết quả. Trong suốt thời gian quét, event loop bị chiếm, mọi GET/SET từ client khác phải đợi trong queue. Với 50M key, quét có thể mất nhiều giây.
  2. SCAN có thể trả duplicate khi Redis rehash dictionary (mở rộng hoặc thu hẹp hashtable) trong khi iteration đang diễn ra. Cursor encoding phụ thuộc vào kích thước hashtable tại thời điểm gọi; nếu hashtable resize, một số bucket có thể được trả lại. Code sử dụng kết quả SCAN cần idempotent với duplicate (vd dùng Set để deduplicate, hoặc DELETE chấp nhận DEL key không tồn tại).
  3. Bước 1: KEYS block event loop Redis → latency spike → API timeout → cascade lỗi. Bước 2: auto-scaling spawn instance mới để xử lý lỗi → instance mới có cache cold → 100% miss → toàn bộ traffic đổ vào DB → DB quá tải → cascade thứ hai. Auto-scaling khuếch đại vì nó tăng số nguồn tải lên DB đúng lúc DB yếu nhất.
  4. Cache stampede: key hết hạn lúc traffic cao → nhiều request đồng thời miss → cùng query DB để rebuild "danh sách 100 sản phẩm bán chạy nhất" (heavy query) → DB quá tải. Xử lý: mutex lock SET NX EX (bài 14) để chỉ một worker rebuild, hoặc stale-while-revalidate (bài 15) để trả stale tức thì và refresh nền. TTL jitter (vd 60 ± 10s) cũng giúp tránh mass expiry đồng thời nếu có nhiều key tương tự.
  5. Dùng event-driven khi một thao tác write làm nhiều cache khác nhau hết hiệu lực, đặc biệt khi các cache đó nằm ở nhiều service khác nhau. Ví dụ: update Order → cần invalidate cache của OrderService (chi tiết đơn), UserService (lịch sử mua hàng), AnalyticsService (doanh thu realtime), NotificationService (template email). DELETE on write trong OrderService phải biết và gọi đến 4 cache — tight coupling. Event-driven: OrderService publish event "order.updated", mỗi consumer tự invalidate cache của mình — decoupled và dễ mở rộng hơn.

Bài tiếp theo

Module 2: Bài 20 — String: Cache, Counter, Token, Bitfield — đi sâu vào data structure String với các use case thực tế: atomic counter, distributed token, Bitfield cho feature flag và packed storage.