Mục lục
- Mục Tiêu Bài Học
- Bài Toán: Cache Stale Là Gì & Tại Sao Khó
- Trade-off Cốt Lõi: Consistency vs. Performance vs. Complexity
- Chiến Lược 1 — TTL-only (Passive Invalidation)
- Chiến Lược 2 — Cache-Aside Với DELETE On Write
- Chiến Lược 3 — Write-Through & Write-Around
- Chiến Lược 4 — Event-Driven Invalidation (CDC)
- Chiến Lược 5 — Versioned Cache Key
- Race Condition: Cache-Aside Gặp Concurrent Write
- Delete-Twice: Giảm Xác Suất Race
- Multi-key Invalidation & Tag-Based Caching
- Consistency Models Thực Tế
- Anti-patterns
- Tổng Kết & Quiz
Mục Tiêu Bài Học
- Hiểu tại sao cache invalidation khó: cache là bản sao, DB là source of truth, và mọi chiến lược đều có trade-off.
- Phân biệt và so sánh năm chiến lược invalidation: TTL-only, cache-aside với DELETE, write-through/write-around, event-driven (CDC), versioned key.
- Phân tích race condition kinh điển trong cache-aside khi có concurrent write — tại sao cache có thể chứa giá trị cũ dù đã DELETE.
- Hiểu kỹ thuật delete-twice: cơ chế, giới hạn và khi nào nên dùng.
- Triển khai tag-based invalidation bằng Redis Set để giải quyết bài toán multi-key.
- Phân biệt ba mức consistency thực tế: eventually consistent, read-your-write, monotonic read.
- Nhận diện các anti-pattern phổ biến trong invalidation.
Bài Toán: Cache Stale Là Gì & Tại Sao Khó
Mô hình cơ bản của caching rất đơn giản: database là nguồn sự thật (source of truth), cache lưu bản sao để tăng tốc đọc. Vấn đề xuất hiện khi database thay đổi mà cache chưa được cập nhật:
DB: user:123 = { name: "Alice", email: "[email protected]" } # sau UPDATE
Cache: user:123 = { name: "Alice", email: "[email protected]" } # chưa đồng bộ
# → reader nhận được email cũ
Trạng thái này gọi là cache stale. Cache invalidation là tập hợp các kỹ thuật quyết định: khi nào và bằng cách nào cache được xoá hoặc cập nhật khi source of truth thay đổi.
Vấn đề khó vì một số nguyên nhân:
- Distributed timing: DB write và cache delete là hai thao tác độc lập, xảy ra trên hai hệ thống khác nhau, không atomic. Giữa hai thao tác luôn có khoảng trống thời gian.
- Concurrent access: nhiều writer và reader chạy đồng thời — thứ tự thực tế của chúng không đoán trước được.
- Partial failure: DB write thành công nhưng cache delete thất bại (timeout, Redis tạm chết, process crash), không có rollback tự động.
- Transitive stale: một entity được tham chiếu bởi nhiều cache key — update entity A có thể stale cache B, C, D nếu các key đó chứa dữ liệu của A.
Không có chiến lược nào giải quyết hoàn toàn. Mỗi chiến lược chọn một điểm khác nhau trên tam giác trade-off.
Trade-off Cốt Lõi: Consistency vs. Performance vs. Complexity
Ba mục tiêu cạnh tranh nhau:
- Consistency: cache luôn phản ánh giá trị mới nhất của DB. Càng tốt thì càng phải làm nhiều thao tác đồng bộ.
- Performance: cache phải nhanh, ít round-trip. Mỗi bước "đồng bộ" thêm là một bước tốn latency.
- Complexity: code phải duy trì được. Event-driven CDC mạnh hơn TTL-only nhưng cần infra Kafka/Debezium, consumer riêng, xử lý idempotency.
| Chiến lược | Consistency | Performance | Complexity |
|---|---|---|---|
| TTL-only | Eventually (window = TTL) | Tốt nhất | Thấp nhất |
| Cache-aside + DELETE | Eventually (window nhỏ) | Tốt | Thấp |
| Write-through | Tốt hơn, vẫn có race | Write chậm hơn | Trung bình |
| Event-driven (CDC) | Tốt, eventual | Tốt (async) | Cao |
| Versioned key | Không stale nhìn thấy | Tốt (read) | Trung bình |
Hầu hết hệ thống production chấp nhận eventual consistency với window nhỏ (milliseconds đến vài giây). Strong consistency yêu cầu pessimistic lock trên cache entry — phần lớn lợi ích hiệu năng của cache sẽ mất.
Chiến Lược 1 — TTL-only (Passive Invalidation)
Cách đơn giản nhất: không làm gì khi DB thay đổi — cache tự expire theo TTL. Lần đọc sau TTL sẽ miss và rebuild bản mới từ DB.
# Chỉ cần SET với TTL khi nạp cache, không cần code invalidation
r.set(f"user:{user_id}", json.dumps(user), ex=300) # 5 phút
# Khi update DB: không cần làm gì với cache
db.execute("UPDATE users SET email = %s WHERE id = %s", (email, user_id))
# cache user:{user_id} sẽ stale tối đa 5 phút, rồi tự expire
Khi nào phù hợp:
- Dữ liệu đọc nhiều, ghi ít, và hệ thống chấp nhận stale tối đa bằng TTL.
- Ví dụ: danh mục sản phẩm, cấu hình hệ thống, giá công bố (không phải giá real-time), tỉ giá lấy mỗi 5 phút.
Giới hạn: stale window cố định bằng TTL. Nếu TTL = 10 phút, mọi reader đọc sai tối đa 10 phút sau mỗi write. Không thể giảm window mà không giảm TTL, nhưng giảm TTL tăng load DB.
Chiến Lược 2 — Cache-Aside Với DELETE On Write
Pattern phổ biến nhất trong production: khi ghi DB, xoá (invalidate) key tương ứng trong cache. Lần đọc kế tiếp sẽ miss và tự nạp lại bản mới.
def update_user(user_id: int, data: dict) -> None:
db.execute(
"UPDATE users SET email = %s, name = %s WHERE id = %s",
(data["email"], data["name"], user_id),
)
r.delete(f"user:{user_id}") # invalidate — lần đọc sau sẽ rebuild
async function updateUser(userId: number, data: Partial<User>): Promise<void> {
await db.query(
"UPDATE users SET email = $1, name = $2 WHERE id = $3",
[data.email, data.name, userId],
);
await redis.del(`user:${userId}`);
}
Vì sao DELETE thay vì SET? Đặt giá trị mới trực tiếp vào cache có vẻ hiệu quả hơn (reader sau không cần miss), nhưng có rủi ro: giá trị bạn SET vào cache có thể không giống hệt kết quả DB sẽ trả (do join, trigger, computed column, hoặc replication lag). DELETE an toàn hơn: để lần miss kế tiếp tự đọc lại bản chuẩn từ DB.
Race condition: chiến lược này vẫn có race condition — xem section 9 để hiểu chi tiết và cách giảm thiểu.
TTL vẫn cần thiết: ngay cả khi có DELETE inline, vẫn nên đặt TTL làm safety net cho trường hợp service crash sau DB write nhưng trước Redis delete, hoặc code path nào đó update DB mà không gọi invalidation.
Chiến Lược 3 — Write-Through & Write-Around
Bài 10 đã phân tích write-through và write-around ở góc độ chiến lược ghi. Ở góc độ invalidation, hai pattern này xử lý như sau:
Write-through: ghi đồng thời DB và cache trong cùng thao tác write. Cache luôn được cập nhật trực tiếp thay vì xoá.
def write_through_update(user_id: int, data: dict) -> None:
db.execute("UPDATE users ...", (..., user_id))
r.set(f"user:{user_id}", json.dumps(data), ex=300) # ghi thẳng bản mới
Rủi ro: nếu hai write concurrent xảy ra gần nhau, thứ tự SET vào cache có thể không khớp thứ tự commit vào DB, dẫn đến cache giữ giá trị của write cũ hơn.
Write-around: chỉ ghi DB, không động cache. Lần đọc kế tiếp sẽ miss và rebuild — về mặt invalidation, write-around giống DELETE (nhưng không tường minh xoá key). Phù hợp với dữ liệu ghi rồi không đọc lại ngay, tránh cache đầy bởi dữ liệu cold.
Cả hai vẫn có race condition khi có concurrent writer. Bài 10 đã đề cập write latency và trade-off consistency — bài này không lặp lại phân tích đó.
Chiến Lược 4 — Event-Driven Invalidation (CDC)
Thay vì invalidate trực tiếp trong application code, DB phát ra sự kiện thay đổi (change event) và một consumer riêng chịu trách nhiệm invalidate cache. Đây là kiến trúc CDC (Change Data Capture).
┌──────────────┐ binlog / WAL ┌───────────────┐
│ Database │ ─────────────────►│ CDC Tool │
│ (MySQL / │ │ (Debezium / │
│ Postgres) │ │ Maxwell) │
└──────────────┘ └───────┬───────┘
│ Kafka topic
▼
┌───────────────┐
│ Consumer │
│ (invalidate │
│ Redis key) │
└───────────────┘
Consumer nhận event và xoá (hoặc cập nhật) key tương ứng:
def handle_user_change_event(event: dict) -> None:
user_id = event["after"]["id"] # Debezium payload structure
r.delete(f"user:{user_id}")
# Nếu muốn invalidate nhiều key liên quan:
r.delete(f"user:{user_id}:posts", f"user:{user_id}:friends")
Ưu điểm so với DELETE inline:
- Không mất invalidation khi service crash: DB commit và event phát ra là atomic (binlog/WAL). Consumer có thể đọc lại từ offset nếu chết giữa chừng — invalidation eventually xảy ra.
- Tách biệt concern: application code không cần biết về cache; consumer độc lập có thể scale riêng.
- Bắt được mọi đường write: kể cả migration script, batch job, admin tool — miễn là ghi vào DB thì CDC đều thấy.
Giới hạn:
- Có độ trễ (lag) giữa DB commit và consumer xử lý — phụ thuộc throughput Kafka và tốc độ consumer. Thường milliseconds đến vài giây, vẫn là eventual consistency.
- Cần infra bổ sung: Kafka (hoặc tương đương), Debezium/Maxwell, consumer service, monitoring cho lag.
- Consumer cần idempotent: cùng event có thể đến hai lần (at-least-once delivery của Kafka), phải xử lý an toàn (DELETE idempotent nên không có vấn đề).
Debezium (debezium.io) là công cụ CDC phổ biến nhất cho MySQL và PostgreSQL, output ra Kafka với schema JSON hoặc Avro. Maxwell (github.com/zendesk/maxwell) là lựa chọn nhẹ hơn cho MySQL.
Chiến Lược 5 — Versioned Cache Key
Thay vì xoá key khi có thay đổi, đưa version vào tên key. Khi cần invalidate, tăng version — key cũ tự nhiên trở nên "stale" vì không ai đọc nữa, và sẽ tự expire theo TTL.
# Lưu version trong Redis (hoặc DB)
def get_user_version(user_id: int) -> int:
v = r.get(f"user:{user_id}:version")
return int(v) if v else 1
def user_cache_key(user_id: int) -> str:
v = get_user_version(user_id)
return f"user:{user_id}:v{v}"
def get_user(user_id: int) -> dict | None:
key = user_cache_key(user_id)
cached = r.get(key)
if cached:
return json.loads(cached)
user = db.fetchone("SELECT * FROM users WHERE id = %s", (user_id,))
if user:
r.set(key, json.dumps(dict(user)), ex=3600)
return dict(user) if user else None
def update_user(user_id: int, data: dict) -> None:
db.execute("UPDATE users SET ... WHERE id = %s", (..., user_id))
# Tăng version — key cũ (user:123:v1) không ai dùng nữa, sẽ tự TTL expire
r.incr(f"user:{user_id}:version")
Ưu điểm:
- Không có thao tác DELETE — invalidation chỉ là tăng một counter, atomic và nhanh.
- Key cũ tự "biến mất" theo TTL — không cần scan và xoá.
- Rất phù hợp khi cần migration schema: đổi version prefix (
user:v2:{id}) để toàn bộ cache cũ không dùng nữa, không cần flush.
Giới hạn:
- Key version cũ tốn memory cho tới khi TTL expire — cần chọn TTL đủ ngắn.
- Mỗi read phải đọc version trước (thêm 1 round-trip Redis), hoặc dùng Lua script / pipeline để gộp.
- Version key bản thân cũng cần TTL hoặc cleanup riêng, kẻo tích lũy theo thời gian.
Race Condition: Cache-Aside Gặp Concurrent Write
Dù đã DELETE cache sau DB write, cache-aside vẫn có race condition kinh điển khi có concurrent read + write. Kịch bản:
Timeline:
T0 Cache: user:123 = old (key đang tồn tại)
T1 Writer A: UPDATE DB → user:123 = new
T2 Reader B: GET user:123 → MISS (giả sử key vừa expire hoặc A đã DEL ngay sau T1)
T3 Reader B: SELECT FROM DB → đọc được old (replication lag, hoặc read replica chưa sync)
T4 Writer A: DEL user:123 ← A delete cache
T5 Reader B: SET user:123 = old ← B ghi bản cũ vào cache!
───────────────────────────────────────────────────────────
Kết quả: DB = new, Cache = old. Stale window = tới lần DEL tiếp theo hoặc TTL expire.
Sequence này xảy ra khi:
- Reader đọc được bản cũ từ DB (do read replica lag, transaction isolation, hoặc race trên primary).
- Writer DEL cache xảy ra trước khi reader kịp SET cache.
- Reader sau đó SET bản cũ vào cache, ghi đè trạng thái đúng.
Xác suất xảy ra: thấp trong điều kiện bình thường nhưng không phải zero, đặc biệt với read replica lag cao hoặc hệ thống write heavy. TTL đóng vai trò giới hạn thời gian tồn tại của stale entry.
Một số cách giảm thiểu:
- Luôn đọc primary khi sắp ghi cache (không dùng read replica cho path dẫn đến SET cache).
- Conditional SET: dùng Lua script kiểm tra version/timestamp trước khi SET, không ghi đè nếu bản đang có trong cache mới hơn.
- Versioned key (section 8): key mới sau mỗi write, không bao giờ overwrite key cũ.
- Delay invalidation: đợi vài giây sau DB write để read replica sync xong, rồi mới DEL cache. Phức tạp hơn nhưng giảm lag race.
Delete-Twice: Giảm Xác Suất Race
Kỹ thuật delete-twice (còn gọi là double delete, hay "Cao Lương defense" trong một số tài liệu Trung Quốc) là biến thể của cache-aside với DELETE, thêm một lần xoá trước write và một lần xoá delayed sau write:
import time
def update_user_double_delete(user_id: int, data: dict, delay_ms: int = 500) -> None:
key = f"user:{user_id}"
# Bước 1: Xoá cache trước khi write DB
# Mục đích: reader đang đọc sẽ thấy miss, xuống DB đọc bản cũ (nhưng chưa ghi cache)
r.delete(key)
# Bước 2: Ghi DB
db.execute("UPDATE users SET email = %s WHERE id = %s", (data["email"], user_id))
# Bước 3: Đợi một khoảng ngắn (đủ để reader trong bước 1 kịp hoàn thành SET của mình)
time.sleep(delay_ms / 1000)
# Bước 4: Xoá cache lần 2 — kill bất kỳ entry stale nào reader đã SET vào giữa T1-T3
r.delete(key)
Logic:
- Lần DELETE thứ nhất (trước write): bất kỳ reader nào đang trong quá trình "miss → đọc DB → chuẩn bị SET" sẽ thấy miss và tạm không có cache.
- Sau DB write, chờ
delay_ms: đủ để các reader đó hoàn thành SET bản cũ vào cache. - Lần DELETE thứ hai: xoá bất kỳ entry stale nào được ghi vào giữa khoảng thời gian đó.
Giới hạn — quan trọng:
- Không loại bỏ hoàn toàn race: vẫn còn cửa sổ nhỏ giữa DELETE lần 2 và lần reader tiếp theo SET cache — nếu reader đọc DB trước DELETE lần 2 nhưng SET vào cache sau nó.
- Thêm latency write:
delay_mslà blocking sleep trong write path. Phải nhỏ (100–500ms) và không nên dùng trên hot write path. - Tốt hơn khi chạy async: nhiều implementation chạy DELETE lần 2 trong background task hoặc delayed job thay vì sleep blocking.
Delete-twice là pragmatic mitigation cho môi trường không thể dùng versioned key hoặc CDC, chứ không phải giải pháp đảm bảo consistency hoàn toàn.
Multi-key Invalidation & Tag-Based Caching
Update một entity thường phải invalidate nhiều cache key liên quan. Ví dụ: update thông tin user:123 có thể stale:
user:123— profile cơ bảnuser:123:posts— danh sách bài viết (có chứa tên tác giả)user:123:friends— danh sách bạn bèfeed:user:123— feed tổng hợpsearch:results:author:123— kết quả tìm kiếm
Liệt kê thủ công tất cả key trong code rất dễ bị bỏ sót khi thêm key mới. Tag-based invalidation giải quyết bằng cách lưu mapping ngược từ tag đến danh sách key dùng Redis Set:
TAG_TTL = 86400 # tag set sống 24h, đủ dài hơn cache key
def cache_set_with_tag(key: str, value: str, ttl: int, tags: list[str]) -> None:
"""Ghi cache và đăng ký key vào các tag tương ứng."""
pipe = r.pipeline()
pipe.set(key, value, ex=ttl)
for tag in tags:
pipe.sadd(f"tag:{tag}", key)
pipe.expire(f"tag:{tag}", TAG_TTL) # tag set cũng cần TTL
pipe.execute()
def invalidate_by_tag(tag: str) -> int:
"""Xoá tất cả cache key liên quan đến tag."""
tag_key = f"tag:{tag}"
keys = r.smembers(tag_key)
if not keys:
return 0
pipe = r.pipeline()
pipe.delete(*keys)
pipe.delete(tag_key)
pipe.execute()
return len(keys)
# --- Sử dụng ---
# Khi set cache feed của user
cache_set_with_tag(
key=f"feed:user:123",
value=json.dumps(feed_data),
ttl=300,
tags=["user:123"],
)
# Khi update user:123, invalidate tất cả key có tag "user:123"
invalidate_by_tag("user:123")
# → xoá feed:user:123, user:123:posts, ... tất cả key đã đăng ký tag đó
Lưu ý khi triển khai:
- Tag Set cần TTL: nếu cache key expire trước khi bị invalidate thì key đó vẫn nằm trong Set nhưng đã không tồn tại — không gây lỗi (DEL key không tồn tại là no-op) nhưng Set phình to. Đặt TTL cho Set dài hơn TTL của cache key để đảm bảo cleanup.
- Dùng pipeline để gộp nhiều lệnh — không DELETE từng key một trong vòng lặp Python.
- Trong môi trường Redis Cluster: key và tag Set có thể nằm trên shard khác nhau, cần hash tag (
{user:123}) để đảm bảo cùng shard nếu dùng multi-key pipeline. - Tag-based invalidation phù hợp với số lượng key vừa phải (hàng trăm). Với hàng triệu key per tag, cần cách tiếp cận khác (ví dụ: prefix scan + UNLINK, hoặc versioned namespace).
Consistency Models Thực Tế
Khi thiết kế cache invalidation, hữu ích để nói rõ mức consistency nào là yêu cầu, thay vì chỉ nói "cần fresh":
Eventually consistent (default)
Cache có thể stale trong một khoảng thời gian hữu hạn. Phần lớn hệ thống production chấp nhận eventual consistency. Stale window phụ thuộc chiến lược invalidation đang dùng:
- TTL-only: window = TTL (hàng giây đến hàng phút).
- Cache-aside + DELETE: window = khoảng thời gian giữa DB write và Redis delete thành công (milliseconds đến vài giây, trừ khi gặp race).
- Event-driven CDC: window = Kafka consumer lag (thường dưới 1 giây ở điều kiện bình thường).
Read-your-write consistency
User vừa write phải thấy được bản mới ngay lập tức trên lần đọc tiếp theo. Khó đạt được với cache vì:
- Sau write, cache key vừa bị DELETE → lần đọc kế tiếp miss và rebuild từ DB. Nếu đọc từ read replica chưa sync, vẫn thấy bản cũ.
- Giải pháp phổ biến: sau write, force đọc từ primary (không qua cache hoặc không qua replica) cho request của user đó; hoặc ghi thẳng bản mới vào cache bằng write-through.
- Hoặc: lưu "write token" vào session/cookie, khi đọc nếu có token thì bypass cache và đọc thẳng DB primary.
Monotonic read
User không thấy dữ liệu "đi lùi" — nếu đã thấy bản mới thì các lần đọc sau không thấy bản cũ hơn. Vi phạm thường xảy ra khi:
- Có nhiều node cache, user đọc từ node khác nhau mỗi lần (load balancer round-robin).
- Một node đã nhận invalidation, node kia chưa.
Trong Redis single instance, monotonic read tự động đảm bảo. Với Redis Cluster hoặc nhiều instance độc lập, cần sticky routing hoặc invalidation broadcast.
Strong consistency
Cache luôn phản ánh đúng bản DB tại mọi thời điểm. Về mặt lý thuyết đòi hỏi lock đọc trên cache entry để không có reader nào đọc trong lúc write đang xảy ra — mất phần lớn lợi ích hiệu năng của cache. Trong thực tế, strong consistency thường đồng nghĩa với "đọc thẳng DB, không qua cache" cho những đường đọc cần tươi tuyệt đối.
Anti-patterns
-
Invalidate trước write, không invalidate sau: xoá cache trước DB write thì reader thấy cache miss và đọc DB bản cũ, ghi bản cũ vào cache. DB write hoàn thành sau đó nhưng cache vẫn là bản cũ cho đến TTL.
# SAI: invalidate trước r.delete(f"user:{user_id}") db.execute("UPDATE users ...") # cache đã miss, reader ghi bản cũ vào trước khi dòng này xong # ĐÚNG: write DB trước, invalidate sau db.execute("UPDATE users ...") r.delete(f"user:{user_id}") - Không có TTL + bỏ sót invalidation: nếu một code path update DB mà không gọi invalidation, và key không có TTL, cache stale vĩnh viễn. Luôn đặt TTL làm safety net.
- Ghi cache sau khi ghi DB thất bại: nếu DB transaction rollback nhưng code vẫn tiếp tục SET cache, cache chứa dữ liệu không tồn tại trong DB. Kiểm tra kết quả DB write trước khi invalidate.
- Quên invalidate entity con: update parent entity (user) mà không invalidate cache các child hoặc derived entity (user:posts, feed, search index). Tag-based invalidation giúp giải quyết vấn đề này.
- Synchronous invalidation hàng triệu key: invalidate theo Set với số lượng lớn trong request path → blocking, tăng latency. Nên dùng UNLINK (async delete) thay vì DEL, và xử lý batch ngoài request path nếu cần invalidate số lượng lớn.
- SET cache khi đọc từ read replica có lag cao: reader đọc bản cũ từ replica rồi cache lại — như race condition section 9. Không SET cache từ read replica nếu lag không kiểm soát được.
- Coi write-through là strong consistency: ghi đồng thời DB và cache không phải atomic — nếu DB commit thành công nhưng Redis SET fail, hai hệ thống lệch nhau. Cần retry hoặc TTL đủ ngắn để tự recover.
Tổng Kết & Quiz
Tổng kết
- Cache invalidation khó vì DB write và cache delete là hai thao tác độc lập, không atomic, trên hai hệ thống khác nhau.
- Năm chiến lược chính: TTL-only (đơn giản nhất, stale window = TTL), cache-aside + DELETE (phổ biến nhất), write-through/write-around (bài 10), event-driven CDC (đáng tin nhất, phức tạp nhất), versioned key (phù hợp schema migration).
- Race condition kinh điển trong cache-aside: reader đọc bản cũ từ DB rồi SET cache sau khi writer đã DELETE — cache chứa bản cũ. TTL và versioned key là hai cách giảm thiểu tốt nhất.
- Delete-twice giảm xác suất race nhưng không loại bỏ hoàn toàn — dùng khi không có lựa chọn tốt hơn.
- Tag-based invalidation dùng Redis Set để invalidate nhiều key liên quan đến một entity — cần chú ý TTL cho tag Set và hiệu năng khi số lượng key lớn.
- Hầu hết hệ thống production chấp nhận eventual consistency với window nhỏ. Strong consistency thường đồng nghĩa với không dùng cache cho đường đọc đó.
Quiz 5 câu
- Mô tả race condition trong cache-aside dẫn đến cache chứa bản cũ dù DB đã có bản mới. Yếu tố nào có thể làm tăng xác suất race này?
- So sánh event-driven invalidation (CDC) và cache-aside + DELETE về: đảm bảo invalidation khi service crash, khả năng bắt các đường update không đi qua application code, và độ phức tạp infra.
- Versioned cache key giải quyết bài toán gì tốt hơn DELETE? Nêu một giới hạn của nó.
- Trong tag-based invalidation, tại sao tag Set cần TTL dài hơn TTL của cache key? Điều gì xảy ra nếu tag Set không có TTL?
- Read-your-write consistency trong context cache nghĩa là gì? Nêu một cách đạt được nó mà không cần strong consistency toàn bộ hệ thống.
Đáp án gợi ý
- T0: cache key tồn tại. T1: writer A update DB. T2: reader B miss (key expire hoặc A đã DEL). T3: reader B đọc DB bản cũ (read replica lag). T4: writer A DEL cache. T5: reader B SET cache bằng bản cũ → stale. Xác suất tăng khi: read replica lag cao, write heavy (nhiều cơ hội interleaving), TTL dài (reader có nhiều thời gian giữ bản cũ).
- CDC: nếu consumer crash sau DB commit thì event vẫn trong Kafka, consumer đọc lại từ offset — invalidation eventually xảy ra. CDC thấy mọi write kể cả migration/batch. Nhưng cần Kafka + CDC tool + consumer service. Cache-aside + DELETE: nếu service crash sau DB write nhưng trước DEL thì invalidation mất, chỉ TTL cứu. Không bắt được write từ tool bên ngoài không gọi code invalidation. Đơn giản hơn nhiều.
- Versioned key không bao giờ overwrite entry cũ — tránh race condition từ concurrent writer. Phù hợp schema migration (đổi prefix version). Giới hạn: key cũ chiếm memory tới khi TTL expire; mỗi read cần thêm một lần đọc version (round-trip).
- Nếu cache key expire trước khi bị invalidate, key vẫn còn trong tag Set dù không tồn tại trên Redis. DEL key không tồn tại là no-op nên không gây lỗi, nhưng Set phình to vô hạn. TTL cho tag Set đảm bảo Set tự expire và không tích lũy. Nếu tag Set không có TTL và không bao giờ được cleanup, memory bị chiếm bởi các entry đã hết hạn.
- Read-your-write: user vừa ghi phải thấy được bản mới ngay trên lần đọc tiếp theo — không thấy bản cũ. Cách đạt được mà không lock toàn bộ: sau write, write-through để cache có ngay bản mới; hoặc lưu "write token" vào session, khi đọc nếu có token thì bypass cache đọc từ primary DB và xoá token.
Bài tiếp theo
Bài 18 chuyển sang Hot Keys: nhận diện hot key bằng Redis Monitor và công cụ, hiểu tại sao hot key gây bottleneck và các kỹ thuật xử lý cơ bản.
