Danh sách bài viết

Bài 13: Negative Caching

Khi một query hỏi tới dữ liệu không tồn tại (user chưa đăng ký, sản phẩm đã gỡ, id sai), cache luôn miss và mỗi request lại đập thẳng xuống database. Hiện tượng này gọi là cache penetration, và nó càng nguy hiểm khi bị khai thác có chủ đích để dập DB. Negative caching là kỹ thuật cache cả kết quả không tồn tại với TTL ngắn để chặn dòng request đó. Bài này phân tích cách dùng sentinel value để phân biệt cache miss và cached null, cung cấp code cache-aside có negative caching bằng ioredis (TypeScript) và redis-py (Python), nhắc tới Bloom filter như giải pháp bổ sung, và đi qua các trade-off cùng pitfall nguy hiểm nhất.

25/05/2026
12 phút đọc
0 lượt xem
1

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

  • Hiểu bài toán cache penetration: query tới dữ liệu không tồn tại luôn miss cache nên mỗi request đều xuống database, gây spam DB.
  • Nắm ý tưởng negative caching: cache cả kết quả không tồn tại (null) với TTL ngắn để chặn dòng request lặp lại đó.
  • Phân biệt rõ cache miss (chưa có gì trong cache) với cached null (đã cache giá trị không tồn tại), và lý do cần một sentinel value để tách hai trạng thái.
  • Viết được hàm đọc theo cache-aside có negative caching bằng cả ioredis (TypeScript) và redis-py (Python).
  • Hiểu cache penetration attack (query hàng loạt key ngẫu nhiên không tồn tại) và vai trò bổ sung của Bloom filter để chặn từ đầu.
  • Biết các trade-off (cached null có thể stale, tốn RAM cho null entries) và cách invalidate negative cache khi dữ liệu được tạo.
  • Nhận diện pitfall nguy hiểm nhất: cache null cho lỗi tạm thời của hạ tầng (DB timeout, exception) thay vì chỉ cache khi DB xác nhận thực sự không tồn tại.
2

Bài Toán: Query Dữ Liệu Không Tồn Tại

Ở bài cache-aside, luồng đọc xử lý rất gọn cho trường hợp dữ liệu trong DB: miss thì đọc DB, ghi cache, các lần sau hit. Nhưng có một trường hợp luồng đó bỏ ngỏ: khi dữ liệu không tồn tại trong database.

Hãy nhìn lại đoạn cache-aside điển hình:

const cached = await redis.get(key);
if (cached !== null) return JSON.parse(cached); // HIT

const user = await readUserFromDb(id);
if (user === null) {
  return null;        // DB không có -> trả null, KHÔNG ghi gì vào cache
}
await redis.set(key, JSON.stringify(user), "EX", TTL_SECONDS);
return user;

Khi user === null, ta trả về null mà không ghi gì vào cache. Hệ quả: lần đọc tiếp theo cho cùng id đó lại GET ra nil (vì chẳng có gì được ghi) → lại bị coi là MISS → lại xuống DB. Với một id không tồn tại bị hỏi liên tục, 100% request đều đập xuống DB. Cache hoàn toàn vô dụng cho đường này.

Hiện tượng này gọi là cache penetration (xuyên thủng cache): request đi xuyên qua lớp cache như thể cache không tồn tại, vì cache chẳng bao giờ giữ được câu trả lời "không có". Các tình huống thực tế:

  • Truy vấn user:99999999 với id chưa hề tồn tại (sai id, gõ nhầm, id đã bị xoá).
  • Tra cứu sản phẩm theo mã đã ngừng kinh doanh hoặc chưa từng có.
  • Lookup theo slug/email/token sai mà client (hoặc bot) gọi đi gọi lại.
# Một id không tồn tại bị hỏi 10.000 lần/s
# Cache-aside thuần (không negative caching):
#   GET user:99999999  -> nil  (MISS mỗi lần)
#   SELECT ... WHERE id = 99999999  -> 0 rows  (DB chạy MỖI request)
# => 10.000 query/s vô ích đập vào DB, hit ratio cho đường này = 0%

Khác với một key hot có thật (miss một lần rồi hit mãi), key không tồn tại miss vĩnh viễn. Đây chính là lỗ hổng mà negative caching vá lại.

3

Negative Caching Là Gì

Negative caching (cache phủ định) là kỹ thuật cache cả kết quả "không tồn tại", chứ không chỉ cache dữ liệu có thật. Khi DB xác nhận một id không có bản ghi, ta vẫn ghi vào cache một dấu hiệu "đã hỏi, kết quả là không có" — kèm một TTL ngắn.

Luồng đọc mở rộng so với cache-aside thường:

  1. Application GET key.
  2. Nếu giá trị là dữ liệu thật → HIT, trả về ngay.
  3. Nếu giá trị là sentinel "không tồn tại" → cũng là HIT (negative hit), trả null ngay mà không xuống DB.
  4. Nếu key chưa có gì (MISS thật) → đọc DB.
  5. DB có dữ liệu → ghi cache value thật, TTL bình thường (vd 5 phút), trả về.
  6. DB xác nhận không có → ghi cache sentinel với TTL ngắn (vd 30-60 giây), trả null.

Điểm cốt lõi: TTL của negative entry phải NGẮN hơn nhiều so với positive entry. Lý do là dữ liệu "chưa tồn tại" có thể xuất hiện sau (user mới đăng ký, sản phẩm mới nhập kho). Nếu TTL null quá dài, dữ liệu mới đã có trong DB nhưng cache vẫn ngoan cố trả null cho tới khi entry hết hạn.

# So sánh TTL hai loại entry (con số minh hoạ)
# Positive (có dữ liệu thật):  EX 300   (5 phút)
# Negative (sentinel "không"):  EX 30    (30 giây - ngắn hơn nhiều)
#
# Với id không tồn tại bị hỏi 10.000 req/s:
#   - request đầu: MISS -> DB trả 0 rows -> SET sentinel EX 30
#   - 30 giây kế tiếp: tất cả là negative HIT, KHÔNG xuống DB
#   => DB chỉ nhận ~1 query mỗi 30s thay vì 10.000/s

Như vậy negative caching biến đường "không tồn tại" từ chỗ luôn miss thành chỗ được cache như mọi dữ liệu khác, chỉ với TTL ngắn hơn để giới hạn độ stale.

4

Sentinel: Phân Biệt Cache Miss & Cached Null

Vấn đề kỹ thuật mấu chốt của negative caching là phải tách bạch ba trạng thái, trong khi GET của Redis chỉ trả về hai khả năng (một chuỗi, hoặc nil):

  • Cache miss: key chưa từng được ghi (hoặc đã hết hạn). Redis trả nil. Ý nghĩa: "chưa biết, phải hỏi DB".
  • Cached value: dữ liệu thật được serialize (vd JSON của user). Ý nghĩa: "có, trả ngay".
  • Cached null: ta đã hỏi DB và DB xác nhận không có. Ý nghĩa: "đã biết là không tồn tại, trả null mà không cần hỏi DB".

Nếu ta cache null bằng cách... ghi chuỗi rỗng, hoặc tệ hơn là không ghi gì, thì GET trả nil trùng với cache miss → không phân biệt được "chưa biết" và "biết là không có". Giải pháp là dùng một sentinel value: một chuỗi đặc biệt, dễ nhận, không bao giờ trùng với dữ liệu hợp lệ. Ví dụ "__NULL__".

# Ba trạng thái, ánh xạ qua sentinel:
GET user:1          -> "{\"id\":1,\"name\":\"An\"}"   # cached value  -> trả user
GET user:99999999   -> "__NULL__"                      # cached null   -> trả null (không hỏi DB)
GET user:42         -> (nil)                            # cache MISS    -> hỏi DB

Logic quyết định sau khi GET:

value = GET key
if value == nil:        -> CACHE MISS  -> đọc DB
elif value == "__NULL__": -> CACHED NULL -> trả null ngay
else:                   -> CACHED VALUE -> deserialize, trả về

Chọn sentinel sao cho không thể nào là một giá trị JSON hợp lệ của domain (ví dụ chuỗi bắt đầu/kết thúc bằng dấu gạch dưới như __NULL__, hoặc một marker riêng). Một số nơi dùng JSON kiểu {"__tombstone__": true}; ý tưởng giống nhau: phải nhận diện được negative entry mà không nhầm với value thật hay với nil.

5

Code Với ioredis (TypeScript)

Triển khai cache-aside có negative caching bằng ioredis. Dùng sentinel "__NULL__", TTL positive 300s và TTL negative 30s. Chú ý điểm quan trọng: chỉ cache null khi DB xác nhận không có — nếu DB ném lỗi (timeout, mất kết nối) thì không ghi sentinel, để lần sau còn hỏi lại.

import Redis from "ioredis";

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

const TTL_POSITIVE = 300; // 5 phút cho dữ liệu thật
const TTL_NEGATIVE = 30;  // 30 giây cho "không tồn tại" (NGẮN)
const NULL_SENTINEL = "__NULL__";

interface User {
  id: number;
  name: string;
  email: string;
}

// Đọc DB. Trả về:
//  - User  : có bản ghi
//  - null  : DB xác nhận KHÔNG có bản ghi (0 rows)
// Nếu DB lỗi thì hàm này THROW (không nuốt lỗi thành null!)
async function readUserFromDb(id: number): Promise<User | null> {
  const row = await db.query("SELECT id, name, email FROM users WHERE id = $1", [id]);
  return row ?? null; // row == undefined => 0 rows => null
}

function userKey(id: number): string {
  return `user:${id}`;
}

export async function getUser(id: number): Promise<User | null> {
  const key = userKey(id);

  // 1. Check cache
  const cached = await redis.get(key);
  if (cached === NULL_SENTINEL) {
    // negative HIT: đã biết không tồn tại, KHÔNG xuống DB
    return null;
  }
  if (cached !== null) {
    // positive HIT
    return JSON.parse(cached) as User;
  }

  // 2. CACHE MISS thật -> đọc DB.
  //    Nếu DB lỗi, để exception ném ra: KHÔNG cache gì cả.
  const user = await readUserFromDb(id);

  if (user === null) {
    // 3. DB XÁC NHẬN không tồn tại -> cache sentinel với TTL NGẮN
    await redis.set(key, NULL_SENTINEL, "EX", TTL_NEGATIVE);
    return null;
  }

  // 4. Có dữ liệu thật -> cache value với TTL bình thường
  await redis.set(key, JSON.stringify(user), "EX", TTL_POSITIVE);
  return user;
}

Lưu ý readUserFromDb chỉ trả null khi DB thực sự không có bản ghi; mọi lỗi hạ tầng đều để db.query ném exception và getUser không chạm tới lệnh set sentinel. Đây là khác biệt sống còn (xem mục Pitfalls).

6

Code Với redis-py (Python)

Phiên bản tương đương bằng redis-py. Đặt decode_responses=True để client trả về str. Cùng nguyên tắc: sentinel riêng, TTL negative ngắn, và chỉ cache null khi DB xác nhận không có (để exception lan ra khi DB lỗi).

import json
import redis

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

TTL_POSITIVE = 300   # 5 phút cho dữ liệu thật
TTL_NEGATIVE = 30    # 30 giây cho "không tồn tại" (NGẮN)
NULL_SENTINEL = "__NULL__"


def read_user_from_db(user_id: int) -> dict | None:
    # Trả dict nếu có bản ghi, None nếu DB xác nhận 0 rows.
    # Nếu DB lỗi -> để exception ném ra, KHÔNG nuốt thành None.
    row = db.fetchone("SELECT id, name, email FROM users WHERE id = %s", (user_id,))
    return dict(row) if row else None


def user_key(user_id: int) -> str:
    return f"user:{user_id}"


def get_user(user_id: int) -> dict | None:
    key = user_key(user_id)

    # 1. Check cache
    cached = r.get(key)
    if cached == NULL_SENTINEL:
        # negative HIT: biết là không tồn tại, không xuống DB
        return None
    if cached is not None:
        # positive HIT
        return json.loads(cached)

    # 2. CACHE MISS thật -> đọc DB (DB lỗi sẽ raise, không cache gì)
    user = read_user_from_db(user_id)

    if user is None:
        # 3. DB XÁC NHẬN không tồn tại -> sentinel + TTL NGẮN
        r.set(key, NULL_SENTINEL, ex=TTL_NEGATIVE)
        return None

    # 4. Có dữ liệu thật -> cache value + TTL bình thường
    r.set(key, json.dumps(user), ex=TTL_POSITIVE)
    return user

Hai bản code có cấu trúc giống hệt nhau. Khác biệt với cache-aside thuần chỉ nằm ở hai chỗ: (1) nhánh kiểm tra sentinel ngay sau GET, và (2) nhánh ghi sentinel với TTL_NEGATIVE khi DB trả về không có.

7

Cache Penetration Attack & Bloom Filter

Cache penetration không chỉ là tai nạn ngẫu nhiên; nó có thể bị khai thác có chủ đích. Trong một cache penetration attack, attacker sinh ra hàng loạt key ngẫu nhiên gần như chắc chắn không tồn tại (vd user:1832901, user:9921774, ... mỗi request một id khác nhau) và bắn dồn dập.

Vì mỗi key chỉ được hỏi đúng một lần (key sau đã khác), cache về cơ bản không giúp được gì: mỗi request là một miss mới → mỗi request xuống DB một lần. Cache bị bỏ qua hoàn toàn và DB lãnh trọn tải, dễ dẫn tới quá tải.

# Attack: mỗi request một id ngẫu nhiên không tồn tại
GET user:1832901  -> nil -> DB 0 rows -> SET __NULL__ EX 30
GET user:9921774  -> nil -> DB 0 rows -> SET __NULL__ EX 30
GET user:5510028  -> nil -> DB 0 rows -> SET __NULL__ EX 30
# ... key không lặp lại -> negative cache hầu như không bao giờ HIT

Negative caching giảm thiểu nhưng không triệt tiêu hẳn: nó chỉ cứu được khi cùng một key bị hỏi lại trong cửa sổ TTL. Với attack dùng key không lặp, mỗi key vẫn tốn đúng một query DB cộng một entry rác trong Redis (tốn RAM). Trong nhiều trường hợp, một query DB cho một id chắc chắn không tồn tại vẫn là khá rẻ, nên negative caching + rate limit thường đủ. Nhưng nếu cần chặn từ gốc, ta cần một lớp lọc đứng trước cả Redis.

Bloom filter (sẽ deep ở Module 8)

Bloom filter là cấu trúc dữ liệu xác suất trả lời nhanh câu hỏi "phần tử này chắc chắn không nằm trong tập, hay có thể nằm trong tập". Đặc tính: không có false negative — nếu Bloom filter nói "không có" thì chắc chắn không có; chỉ có thể false positive (nói "có thể có" trong khi thực ra không).

Áp dụng: nạp tất cả id hợp lệ vào một Bloom filter. Trước khi chạm Redis/DB, hỏi Bloom filter trước:

  • Bloom filter trả "chắc chắn không có" → trả null ngay, không chạm cache lẫn DB. Chặn được toàn bộ key ngẫu nhiên của attack từ đầu.
  • Bloom filter trả "có thể có" → đi tiếp luồng cache-aside + negative caching như bình thường.

Redis có module RedisBloom (lệnh BF.ADD, BF.EXISTS) hỗ trợ Bloom filter ngay trong Redis. Chi tiết cách dựng, kích thước, tỉ lệ false positive và đồng bộ filter sẽ được phân tích sâu ở Module 8. Ở bài này chỉ cần nhớ: negative caching xử lý các key lặp lại, Bloom filter bổ sung để chặn các key không tồn tại ngay từ cửa.

8

Invalidate Negative Cache Khi Tạo Dữ Liệu

Negative entry sinh ra một loại stale mới: ta đã cache "id X không tồn tại", nhưng sau đó X được tạo ra (user đăng ký, sản phẩm nhập kho). Nếu chỉ dựa vào TTL, client sẽ nhận null cho tới khi negative entry hết hạn — vẫn báo "không có" dù dữ liệu đã có thật.

Có hai cách kiểm soát, nên dùng kết hợp:

  1. TTL negative ngắn: giới hạn cận trên của stale window. Đây là lưới an toàn bắt buộc, kể cả khi quên invalidate.
  2. Chủ động invalidate negative cache khi insert: ngay sau khi tạo bản ghi trong DB, DEL key tương ứng để xoá sentinel (nếu có). Lần đọc kế tiếp sẽ miss và nạp lại dữ liệu thật.
// Khi TẠO MỚI user: ghi DB trước, rồi xoá negative cache (nếu có)
export async function createUser(u: User): Promise<void> {
  await db.query(
    "INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
    [u.id, u.name, u.email],
  );
  // Trước đó có thể đã cache "__NULL__" cho id này -> xoá đi
  await redis.del(userKey(u.id));
}
# Bản Python tương đương
def create_user(u: dict) -> None:
    db.execute(
        "INSERT INTO users (id, name, email) VALUES (%s, %s, %s)",
        (u["id"], u["name"], u["email"]),
    )
    # Xoá negative entry "__NULL__" nếu trước đó đã cache id này
    r.delete(user_key(u["id"]))

Lưu ý DEL trên một key chưa từng được cache là vô hại (trả 0). Việc invalidate này giống invalidation khi update ở cache-aside, chỉ khác là ta đang xoá một negative entry. Với những id sinh tuần tự do DB cấp (auto-increment), tình huống "negative entry rồi mới tạo" hiếm hơn; nhưng với id/slug/email do client cung cấp trước (vd kiểm tra username còn trống rồi mới đăng ký) thì rất dễ gặp, nên đừng bỏ bước invalidate.

9

Trade-off

Negative caching không miễn phí. Các đánh đổi cần cân nhắc:

  • Cached null có thể stale: trong khoảng từ lúc dữ liệu được tạo tới lúc negative entry hết hạn (hoặc bị invalidate), client vẫn nhận null dù DB đã có dữ liệu. Đây là lý do TTL negative phải ngắn: nó chính là cận trên của cửa sổ "báo không có dù đã có".
  • Tốn RAM cho null entries: mỗi key không tồn tại bị hỏi đều sinh một entry. Khi có nhiều id rác (đặc biệt dưới attack), số negative entry phình lên. Giảm thiểu bằng TTL ngắn (entry tự dọn) và bằng eviction policy phù hợp; với tải tấn công lớn thì cần Bloom filter chặn trước.
  • Thêm độ phức tạp logic: phải xử lý ba trạng thái (miss / value / null), thêm sentinel, thêm nhánh invalidate khi insert. Code dễ sai nếu không nhất quán sentinel giữa các nơi đọc/ghi.
  • Cân bằng TTL: TTL negative quá ngắn thì hiệu quả chống penetration giảm (nhiều request vẫn lọt xuống DB); quá dài thì stale window rộng. Chọn theo mức chấp nhận: dữ liệu "có thể xuất hiện bất cứ lúc nào" nên để vài chục giây; dữ liệu gần như không bao giờ tồn tại có thể để dài hơn chút.

Nhìn tổng thể, đây là đánh đổi giữa bảo vệ DBđộ tươi của trường hợp "vừa xuất hiện". Với hầu hết hệ thống, một TTL negative ngắn (10-60 giây) cộng invalidate khi insert là điểm cân bằng hợp lý.

10

Pitfalls & Anti-patterns

  • Cache null cho LỖI TẠM THỜI (nguy hiểm nhất): nếu readUserFromDb nuốt mọi exception (DB timeout, mất kết nối, deadlock) thành null rồi ghi sentinel, hệ thống sẽ "đóng băng" trạng thái lỗi: trong suốt TTL negative, mọi request đều trả null cho một dữ liệu thực sự có thật. Chỉ cache null khi DB xác nhận 0 rows; với lỗi hạ tầng thì để exception lan ra (hoặc retry), tuyệt đối không ghi sentinel.
  • TTL negative quá dài: cache "không tồn tại" với TTL bằng TTL positive (vd 5-10 phút) khiến dữ liệu mới tạo không hiện ra trong thời gian dài. TTL negative phải ngắn hơn nhiều so với positive.
  • Không phân biệt cached-null với chưa-cache: nếu cache null bằng chuỗi rỗng hoặc bằng cách không ghi gì, GET trả nil trùng với cache miss → không chặn được penetration (vẫn xuống DB mỗi lần). Bắt buộc dùng sentinel riêng, dễ nhận diện.
  • Sentinel trùng với value hợp lệ: chọn sentinel có thể là một giá trị thật của domain (vd dùng chuỗi "null" trong khi domain có thể chứa từ đó) sẽ gây trả null nhầm cho dữ liệu có thật. Dùng marker không thể nhầm như "__NULL__".
  • Quên invalidate khi insert: tạo bản ghi mới mà không DEL negative entry khiến dữ liệu vừa tạo vẫn bị báo "không có" tới khi TTL hết. Đặt lệnh DEL ngay cạnh mọi đường insert.
  • Dựa hoàn toàn vào negative caching để chống attack: với attack dùng key không lặp lại, negative caching hầu như không HIT nên không cứu được DB và còn làm phình RAM. Bổ sung Bloom filter (Module 8) và/hoặc rate limit ở tầng trước.
  • Áp negative caching cho dữ liệu thay đổi liên tục giữa có/không: với dữ liệu mà sự tồn tại bật/tắt nhanh, TTL negative dễ gây hiển thị sai. Cân nhắc TTL cực ngắn hoặc không negative cache cho đường đó.
11

Tổng Kết & Quiz

Tổng kết

  • Cache penetration: query tới dữ liệu không tồn tại luôn miss nên mỗi request đều xuống DB; cache-aside thuần không vá được vì không ghi gì khi DB trả 0 rows.
  • Negative caching: cache cả kết quả "không tồn tại" bằng một sentinel value, với TTL ngắn vì dữ liệu có thể xuất hiện sau.
  • Phải dùng sentinel (vd "__NULL__") để phân biệt ba trạng thái: cache miss (nil), cached value, cached null.
  • Cache penetration attack dùng key ngẫu nhiên không lặp; negative caching chỉ giảm thiểu, Bloom filter (Module 8) chặn từ đầu.
  • Invalidate negative entry khi insert dữ liệu mới; kết hợp TTL ngắn làm lưới an toàn.
  • Pitfall nguy hiểm nhất: cache null cho lỗi hạ tầng tạm thời — chỉ cache khi DB xác nhận thực sự không có.

Quiz 5 câu

  1. Cache penetration là gì? Vì sao cache-aside thuần không bảo vệ được DB trước các query tới dữ liệu không tồn tại?
  2. Tại sao cần một sentinel value? Hãy nêu ba trạng thái mà ta phải phân biệt sau khi gọi GET và ý nghĩa mỗi trạng thái.
  3. Vì sao TTL của negative entry phải ngắn hơn nhiều so với positive entry? Điều gì xảy ra nếu để TTL negative quá dài?
  4. Trong cache penetration attack với key ngẫu nhiên không lặp lại, vì sao negative caching chỉ giảm thiểu chứ không triệt tiêu? Bloom filter bổ sung như thế nào và nhờ tính chất gì?
  5. Vì sao tuyệt đối không được cache sentinel null khi DB ném lỗi tạm thời (timeout, mất kết nối)? Hậu quả là gì?

Đáp án gợi ý

  1. Cache penetration là khi request đi xuyên qua cache xuống DB vì cache không bao giờ giữ được câu trả lời "không có". Cache-aside thuần khi DB trả 0 rows thì trả null mà không ghi gì vào cache, nên lần đọc sau lại GET ra nil (miss) và lại xuống DB — id không tồn tại bị hỏi lặp sẽ đập DB 100% số request.
  2. Cần sentinel vì GET chỉ trả chuỗi hoặc nil, không đủ tách "chưa biết" với "biết là không có". Ba trạng thái: nil = cache miss (phải hỏi DB); chuỗi dữ liệu = cached value (trả ngay); sentinel (vd "__NULL__") = cached null (trả null mà không hỏi DB).
  3. Vì dữ liệu "chưa tồn tại" có thể xuất hiện sau (user đăng ký, hàng nhập kho). TTL negative là cận trên của cửa sổ "báo không có dù đã có"; để quá dài thì dữ liệu mới bị che giấu lâu (stale nghiêm trọng) và tốn RAM cho entry rác.
  4. Vì negative caching chỉ HIT khi cùng một key bị hỏi lại trong cửa sổ TTL; attack dùng key không lặp nên gần như không HIT, mỗi key vẫn tốn một query DB và một entry rác. Bloom filter đặt trước cache trả lời "chắc chắn không có" cho các id ngẫu nhiên (nhờ tính chất không có false negative), chặn chúng trước khi chạm Redis/DB.
  5. Vì khi đó dữ liệu thật vẫn tồn tại, nhưng nếu ta cache sentinel null thì suốt TTL negative mọi request đều trả null sai cho dữ liệu có thật, "đóng băng" trạng thái lỗi tạm thời thành kết quả sai kéo dài. Chỉ cache null khi DB xác nhận 0 rows; lỗi hạ tầng phải để exception lan ra hoặc retry, không ghi sentinel.

Bài tiếp theo

Bài 14 phân tích cache stampede (còn gọi thundering herd / dog-pile): khi một key hot hết hạn, hàng loạt request cùng miss và cùng lúc đập xuống DB để rebuild cache. Bạn sẽ tìm hiểu cách dùng mutex lock (chỉ một request được phép tái tạo cache, các request khác chờ hoặc trả bản cũ) để dập làn sóng đó.

Tham khảo