Mục lục
Mục Tiêu Bài Học
- Hiểu Read-Through: cache layer tự load từ DB khi miss; application chỉ nói chuyện với cache qua một abstraction/library, khác Cache-Aside ở chỗ logic đọc DB nằm trong cache layer chứ không ở application code.
- Hiểu Write-Through: ghi đồng bộ cả cache và DB trong cùng một thao tác, cache luôn fresh nhưng write latency cao hơn và cache có thể chứa cả data không bao giờ đọc lại.
- Hiểu Write-Behind (Write-Back): ghi cache trước, flush xuống DB bất đồng bộ theo batch hoặc định kỳ; write rất nhanh và gộp được ghi, nhưng có risk mất data nếu cache chết trước khi flush.
- Hiểu Write-Around: ghi thẳng DB, không ghi cache; cache chỉ được điền khi đọc, tránh làm bẩn cache nhưng data mới luôn miss ở lần đọc đầu.
- Đọc và viết được code Write-Through và Write-Behind bằng ioredis (TypeScript) và redis-py (Python).
- Chọn được pattern phù hợp theo workload: read-heavy hay write-heavy, mức chấp nhận mất data, yêu cầu freshness.
- Nắm trade-off về consistency khi cache và DB là hai hệ thống tách rời (không atomic mặc định) và nhận diện các pitfall/anti-pattern.
Bức Tranh: Ai Điều Phối Cache & DB
Ở bài 9, Cache-Aside đặt toàn bộ logic điều phối vào tay application: app tự GET cache, tự đọc DB khi miss, tự SET lại. Khi update thì app tự ghi DB và DEL cache. Các pattern trong bài này khác nhau ở hai câu hỏi cốt lõi:
- Đường đọc: khi miss, ai chịu trách nhiệm đọc DB và nạp lại cache — application code (Cache-Aside) hay một cache layer/abstraction (Read-Through)?
- Đường ghi: khi có dữ liệu mới, app ghi vào đâu, theo thứ tự nào, đồng bộ hay bất đồng bộ — đó là điểm phân biệt Write-Through, Write-Behind và Write-Around.
Quan trọng: các pattern đọc và ghi độc lập nhau và có thể ghép. Ví dụ thường gặp là Read-Through + Write-Through (một cache layer lo cả hai đường), hoặc Cache-Aside đọc + Write-Around ghi. Một nguyên tắc xuyên suốt cần giữ trong đầu:
# Cache va DB la HAI he thong tach roi.
# Khong co transaction atomic mac dinh trai dai ca hai.
#
# ghi cache OK -> ghi DB FAIL => cache fresh, DB cu (lech!)
# ghi DB OK -> ghi cache FAIL => DB moi, cache cu (lech!)
#
# => Moi pattern ghi deu phai tra loi: neu mot ben that bai thi sao?
Trong bài này, database vẫn là nguồn sự thật (source of truth) trong hầu hết các pattern. Riêng Write-Behind tạm coi cache là nơi nhận ghi trước, nhưng dữ liệu cuối cùng vẫn phải nằm bền vững (durable) ở DB.
Read-Through
Read-Through (đọc xuyên qua cache) là pattern trong đó application chỉ nói chuyện với cache. Khi cache miss, chính cache layer (một abstraction, library, hoặc cache provider) tự đọc database, tự nạp kết quả vào cache rồi trả về cho application. Application không hề thấy database trên đường đọc.
So với Cache-Aside, luồng nghiệp vụ giống nhau (check cache, miss thì xuống DB, nạp lại cache) nhưng vị trí của logic khác hẳn:
- Cache-Aside: logic "đọc DB khi miss" nằm trong application code, lặp lại ở mọi nơi cần đọc.
- Read-Through: logic đó nằm trong cache layer; application chỉ gọi một hàm như
cache.get(key, loader)và quên đi sự tồn tại của DB.
┌─────────────┐ GET key
request │ │ ─────────────► ┌───────────────┐ MISS: tu doc DB
───────► │ Application │ │ Cache layer │ ──────────────► ┌──────────┐
│ │ ◄───────────── │ (read-through)│ ◄────────────── │ Database │
└─────────────┘ value └───────────────┘ nap lai cache └──────────┘
(logic load DB o DAY, khong o app)
Redis bản thân là một key-value store thuần, không tự biết về database của bạn. Vì vậy Read-Through không phải tính năng có sẵn của Redis; nó được hiện thực bằng một lớp bọc phía trên client: bạn truyền vào một hàm loader để cache layer gọi khi miss. Nhiều thư viện cache (ví dụ trong hệ sinh thái Java như Caffeine/Ehcache, hay các wrapper tự viết) cung cấp sẵn cơ chế này.
Ưu điểm: application code gọn, không lặp lại logic đọc DB, dễ thống nhất TTL và serialize ở một chỗ. Nhược điểm: phụ thuộc vào abstraction/provider; nếu cache layer là đường đọc bắt buộc thì khi nó chết, đường đọc cũng đứt (kém resilient hơn Cache-Aside thuần, nơi app có thể fallback đọc thẳng DB).
Write-Through
Write-Through (ghi xuyên qua cache) là pattern ghi trong đó mỗi lần update, dữ liệu được ghi đồng bộ vào cả cache lẫn database trong cùng một thao tác logic, trước khi thao tác được coi là hoàn tất.
- Application phát lệnh ghi (ví dụ cập nhật user).
- Ghi vào DB (nguồn sự thật).
- Ghi (cập nhật) cache với cùng giá trị đó, kèm TTL.
- Chỉ khi cả hai bước xong mới trả về thành công.
┌─────────────┐ 1. write DB ┌──────────┐
write │ │ ─────────────► │ Database │
───────► │ Application │ 2. write cache└──────────┘
│ │ ─────────────► ┌─────────┐
└─────────────┘ (dong bo) │ Redis │
trả ve sau khi CA HAI xong └─────────┘
Ưu điểm: cache luôn fresh ngay sau khi ghi — lần đọc kế tiếp chắc chắn HIT với dữ liệu mới nhất, stale window gần như bằng 0 (trong điều kiện cả hai ghi thành công). Hợp với dữ liệu vừa ghi xong là đọc lại ngay (read-after-write).
Nhược điểm:
- Write latency cao hơn: mỗi ghi phải đợi cả hai hệ thống, cộng dồn độ trễ.
- Cache chứa cả data không bao giờ đọc lại: vì ghi gì cũng nạp vào cache, dữ liệu ghi-một-lần-không-đọc-lại vẫn chiếm RAM (lãng phí, có thể cần TTL/eviction để dọn).
- Không atomic giữa hai hệ thống: nếu ghi DB xong nhưng ghi cache lỗi (hoặc ngược lại), hai bên lệch nhau. Phải có chiến lược xử lý lỗi từng phần (sẽ bàn ở section trade-off).
Write-Through thường đi kèm Read-Through để cache layer lo cả hai đường, cho cache luôn đồng bộ với DB.
Write-Behind (Write-Back)
Write-Behind (còn gọi là Write-Back) là pattern ghi trong đó application ghi vào cache trước và trả về ngay, còn việc đẩy (flush) dữ liệu xuống database được làm bất đồng bộ sau đó — theo batch hoặc định kỳ.
- Application ghi vào cache, đồng thời đưa thay đổi vào một buffer/queue.
- Trả về thành công ngay cho caller (không đợi DB).
- Một worker nền gom các thay đổi và flush xuống DB theo batch (ví dụ mỗi N bản ghi hoặc mỗi T giây).
┌─────────────┐ 1. write cache ┌─────────┐
write │ │ ───────────────► │ Redis │
───────► │ Application │ 2. enqueue │ + queue │
│ │ tra ve NGAY └────┬────┘
└─────────────┘ │ 3. flush bat dong bo (batch)
▼
┌──────────┐
│ Database │
└──────────┘
Ưu điểm:
- Write rất nhanh: caller chỉ đợi ghi vào RAM (Redis), không đợi DB.
- Gộp ghi (write coalescing/batching): nhiều update cùng một key trong cửa sổ flush có thể được gộp thành một lần ghi DB; tải ghi xuống DB giảm mạnh, hợp với workload write-heavy như đếm view, ghi metric, counter.
Nhược điểm lớn nhất — risk mất data: dữ liệu chỉ mới nằm trong cache (RAM) mà chưa flush xuống DB. Nếu cache chết (crash, mất điện, OOM) hoặc worker flush lỗi trước khi đẩy xuống DB, các thay đổi đó mất vĩnh viễn. Đây là khác biệt then chốt so với Write-Through (vốn đảm bảo DB đã có dữ liệu trước khi trả về).
Vì rủi ro này, buffer/queue phải càng tin cậy càng tốt. Ví dụ trong section 8 chỉ dùng một list/queue đơn giản để minh hoạ ý tưởng; trong thực tế nên dùng hàng đợi bền vững như Redis Streams hoặc một message queue có ack — chủ đề này được học kỹ ở Module 5. Write-Behind chỉ nên dùng khi mất một ít dữ liệu mới nhất là chấp nhận được, hoặc khi đã có cơ chế persistence/replication đủ mạnh.
Write-Around
Write-Around (ghi vòng qua cache) là pattern ghi trong đó dữ liệu được ghi thẳng vào database, không ghi vào cache. Cache chỉ được điền khi có người đọc (giống lazy loading của Cache-Aside). Để cache không giữ bản cũ sau khi ghi, thường kết hợp với việc DEL key tương ứng (invalidate) khi ghi.
- Application ghi thẳng vào DB.
- Không ghi cache (và thường
DELkey cũ nếu có để tránh stale). - Lần đọc kế tiếp sẽ miss và nạp dữ liệu mới vào cache theo đường đọc.
┌─────────────┐ 1. write DB ┌──────────┐
write │ │ ─────────────► │ Database │
───────► │ Application │ └──────────┘
│ │ 2. (tuy chon) DEL key cu ┌─────────┐
└─────────────┘ ─────────────────────────► │ Redis │
KHONG ghi du lieu moi vao cache └───────┘
Ưu điểm: tránh làm bẩn cache — dữ liệu ghi nhiều nhưng ít khi đọc lại sẽ không chiếm chỗ trong cache. Hợp với workload ghi nhiều, đọc lại thưa (log, audit, dữ liệu append-only). Write latency thấp vì chỉ ghi một hệ thống (DB).
Nhược điểm: dữ liệu mới ghi luôn miss ở lần đọc đầu tiên (phải xuống DB), nên không hợp read-after-write tức thì. Nếu quên invalidate khi ghi, cache có thể giữ bản cũ tới khi TTL hết.
Code Write-Through (ioredis & redis-py)
Ý tưởng: ghi DB trước (nguồn sự thật), rồi ghi cache đồng bộ với cùng giá trị. Ghi DB trước để nếu cache lỗi thì DB vẫn đúng — và lần đọc sau có thể tự nạp lại cache. Lưu ý: hai hệ thống không atomic, nên cần bắt lỗi rõ ràng cho bước ghi cache.
import Redis from "ioredis";
const redis = new Redis({ host: "127.0.0.1", port: 6379 });
const TTL_SECONDS = 300;
interface User {
id: number;
name: string;
email: string;
}
function userKey(id: number): string {
return `user:${id}`;
}
// Write-Through: ghi DB truoc, roi ghi cache dong bo voi cung gia tri.
export async function saveUser(user: User): Promise<void> {
// 1. Ghi DB (nguon su that) — neu loi se throw, khong dung toi cache
await db.query(
"UPDATE users SET name = $1, email = $2 WHERE id = $3",
[user.name, user.email, user.id],
);
// 2. Ghi cache dong bo. Hai he thong KHONG atomic:
// neu buoc nay loi, DB van dung -> chon invalidate de lan doc sau nap lai.
try {
await redis.set(userKey(user.id), JSON.stringify(user), "EX", TTL_SECONDS);
} catch (err) {
// Khong de cache lech: xoa key de doc sau MISS va nap lai tu DB.
await redis.del(userKey(user.id)).catch(() => {});
// log err de theo doi, khong nuot loi am tham
}
}
Bản tương đương bằng redis-py:
import json
import redis
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
TTL_SECONDS = 300
def user_key(user_id: int) -> str:
return f"user:{user_id}"
# Write-Through: ghi DB truoc, roi ghi cache dong bo voi cung gia tri.
def save_user(user: dict) -> None:
# 1. Ghi DB (nguon su that)
db.execute(
"UPDATE users SET name = %s, email = %s WHERE id = %s",
(user["name"], user["email"], user["id"]),
)
# 2. Ghi cache dong bo. Hai he thong KHONG atomic:
# neu buoc nay loi, DB van dung -> invalidate de lan doc sau nap lai.
try:
r.set(user_key(user["id"]), json.dumps(user), ex=TTL_SECONDS)
except redis.RedisError:
# Khong de cache lech: xoa key de doc sau MISS va nap lai tu DB.
try:
r.delete(user_key(user["id"]))
except redis.RedisError:
pass
# log loi de theo doi
Điểm cần nhớ: lệnh ghi DB và ghi cache là hai thao tác riêng. Nếu cần đảm bảo mạnh hơn, có thể đặt ghi cache sau commit transaction DB và coi cache là best-effort; khi ghi cache lỗi thì invalidate để đường đọc tự sửa.
Code Write-Behind (ioredis & redis-py)
Minh hoạ ý tưởng bằng một buffer/queue đơn giản: ghi cache trước, đẩy key vào một list trong Redis, rồi một worker nền flush xuống DB theo batch. Cảnh báo: list dùng ở đây chỉ để minh hoạ — nó không phải hàng đợi tin cậy (một mục có thể mất nếu crash giữa lúc pop và ghi DB). Hàng đợi bền vững với ack (Redis Streams hoặc message queue) được học ở Module 5.
import Redis from "ioredis";
const redis = new Redis({ host: "127.0.0.1", port: 6379 });
const DIRTY_QUEUE = "writebehind:dirty"; // list cac key can flush
const BATCH = 100;
const FLUSH_INTERVAL_MS = 2000;
function userKey(id: number): string {
return `user:${id}`;
}
// Write-Behind: ghi cache + enqueue, tra ve NGAY (khong doi DB).
export async function saveUserWriteBehind(user: {
id: number; name: string; email: string;
}): Promise<void> {
const key = userKey(user.id);
await redis.set(key, JSON.stringify(user)); // can nhac KHONG TTL voi data chua flush
await redis.lpush(DIRTY_QUEUE, key); // danh dau "dirty" can ghi xuong DB
// tra ve ngay; RUI RO: data chi o RAM cho toi khi worker flush thanh cong
}
// Worker nen: dinh ky gom batch va flush xuong DB.
async function flushWorker(): Promise<void> {
setInterval(async () => {
for (let i = 0; i < BATCH; i++) {
const key = await redis.rpop(DIRTY_QUEUE);
if (key === null) break; // het dirty
const raw = await redis.get(key);
if (raw === null) continue; // key da het han/bi xoa
const user = JSON.parse(raw);
// GOP GHI: nhieu update cung key truoc do da bi de len trong cache,
// o day chi ghi gia tri cache MOI NHAT xuong DB mot lan.
await db.query(
"UPDATE users SET name = $1, email = $2 WHERE id = $3",
[user.name, user.email, user.id],
).catch((err) => {
// flush loi: enqueue lai de thu lan sau, tranh mat data
redis.lpush(DIRTY_QUEUE, key);
});
}
}, FLUSH_INTERVAL_MS);
}
Bản tương đương bằng redis-py (worker chạy trong một thread/loop riêng):
import json
import time
import redis
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
DIRTY_QUEUE = "writebehind:dirty" # list cac key can flush
BATCH = 100
FLUSH_INTERVAL_S = 2.0
def user_key(user_id: int) -> str:
return f"user:{user_id}"
# Write-Behind: ghi cache + enqueue, tra ve NGAY (khong doi DB).
def save_user_write_behind(user: dict) -> None:
key = user_key(user["id"])
r.set(key, json.dumps(user)) # can nhac KHONG TTL voi data chua flush
r.lpush(DIRTY_QUEUE, key) # danh dau "dirty"
# tra ve ngay; RUI RO: data chi o RAM cho toi khi worker flush thanh cong
# Worker nen: dinh ky gom batch va flush xuong DB.
def flush_worker() -> None:
while True:
for _ in range(BATCH):
key = r.rpop(DIRTY_QUEUE)
if key is None:
break # het dirty
raw = r.get(key)
if raw is None:
continue # key da het han/bi xoa
user = json.loads(raw)
try:
# GOP GHI: chi ghi gia tri cache moi nhat xuong DB mot lan
db.execute(
"UPDATE users SET name = %s, email = %s WHERE id = %s",
(user["name"], user["email"], user["id"]),
)
except Exception:
# flush loi: enqueue lai de thu lan sau, tranh mat data
r.lpush(DIRTY_QUEUE, key)
time.sleep(FLUSH_INTERVAL_S)
Điểm cần nhớ ở write-behind: (1) phải có cơ chế retry khi flush lỗi; (2) cửa sổ thời gian từ lúc ghi cache tới lúc flush thành công chính là khoảng có thể mất data nếu crash; (3) muốn an toàn thật sự, thay list đơn giản bằng Redis Streams hoặc message queue có ack — Module 5 sẽ phân tích.
Bảng So Sánh & Khi Nào Dùng Cái Nào
Bảng đối chiếu các pattern (gồm cả Cache-Aside ở bài 9). Lưu ý Cache-Aside và Read-Through là pattern đọc; Write-Through, Write-Behind, Write-Around là pattern ghi — bảng gộp lại để dễ so sánh đặc tính chung:
| Pattern | Write latency | Read latency | Consistency cache↔DB | Độ phức tạp | Risk mất data | Use case tiêu biểu |
|---|---|---|---|---|---|---|
| Cache-Aside (đọc) | Thấp (ghi DB + DEL cache) | HIT thấp; MISS lần đầu cao | Eventual; stale window khi DEL lỗi/TTL | Thấp | Không (DB là source of truth) | Read-heavy tổng quát; mặc định phổ biến nhất |
| Read-Through (đọc) | Phụ thuộc pattern ghi đi kèm | HIT thấp; MISS lần đầu cao | Như Cache-Aside, logic ở cache layer | Trung bình (cần abstraction/provider) | Không | Muốn app code gọn, tập trung logic load ở một nơi |
| Write-Through (ghi) | Cao (đợi cả cache + DB) | HIT thấp, luôn fresh sau ghi | Mạnh nhất (cache fresh ngay nếu cả hai OK) | Trung bình | Không (DB ghi đồng bộ) | Read-after-write; cần cache luôn tươi |
| Write-Behind (ghi) | Rất thấp (chỉ ghi cache) | HIT thấp | Yếu trong cửa sổ chưa flush; DB trễ hơn cache | Cao (worker, retry, queue tin cậy) | Có (mất nếu cache chết trước khi flush) | Write-heavy, gộp ghi: counter, view, metric |
| Write-Around (ghi) | Thấp (chỉ ghi DB) | Data mới luôn MISS lần đầu | DB là source of truth; cache lười nạp | Thấp | Không | Ghi nhiều, đọc lại thưa: log, audit, append-only |
Khi nào dùng cái nào
- Read-heavy, dữ liệu đọc lại nhiều: Cache-Aside hoặc Read-Through cho đường đọc. Nếu vừa ghi xong cần đọc lại tươi ngay thì ghép Write-Through.
- Cần read-after-write tươi, chấp nhận write chậm hơn: Write-Through. Đổi lại write latency cao và cache có thể chứa data ít đọc.
- Write-heavy, nhiều update lặp lại cùng key, chấp nhận mất một ít data mới nhất: Write-Behind để gộp ghi và giảm tải DB. Bắt buộc có persistence/queue tin cậy nếu data quan trọng.
- Ghi nhiều nhưng ít đọc lại: Write-Around để tránh làm bẩn cache; chấp nhận lần đọc đầu sau ghi sẽ miss.
Ba câu hỏi định hướng khi chọn: (1) workload thiên về đọc hay ghi? (2) mức chấp nhận mất data tới đâu? (3) yêu cầu freshness chặt hay lỏng (đọc luôn cần bản mới nhất hay chấp nhận stale)?
Pitfalls & Anti-patterns
- Write-Behind mất data khi crash: dùng list/biến nhớ tạm làm buffer rồi cache chết trước khi flush — các thay đổi chưa đẩy xuống DB mất sạch. Phải dùng hàng đợi bền vững có ack, bật persistence/replication, và có retry khi flush lỗi. Đừng dùng Write-Behind cho dữ liệu không được phép mất (giao dịch tiền, đơn hàng).
- Tưởng Write-Through atomic giữa cache & DB: cache và DB là hai hệ thống tách rời, không có transaction phủ cả hai. Ghi DB OK nhưng ghi cache lỗi (hoặc ngược lại) làm hai bên lệch. Luôn quyết định trước "nếu một bên lỗi thì sao" — thường là invalidate để đường đọc tự sửa.
- Chọn pattern sai workload: dùng Write-Through cho workload write-heavy gộp được (counter, view) làm write latency và tải DB cao vô ích — đó là chỗ của Write-Behind. Ngược lại, dùng Write-Around cho dữ liệu cần đọc-lại-ngay-sau-ghi khiến lần đọc đầu luôn miss.
- Write-Through cache mọi thứ → phình cache: vì ghi gì cũng nạp vào cache, dữ liệu ghi-một-lần-không-đọc-lại vẫn ngốn RAM. Cần TTL/eviction để dọn, hoặc dùng Write-Around cho phần dữ liệu ít đọc lại.
- Coi Read-Through là tính năng có sẵn của Redis: Redis không tự biết DB của bạn. Read-Through phải tự hiện thực bằng lớp bọc (truyền loader). Đừng kỳ vọng Redis tự đọc DB khi miss.
- Write-Behind không kiểm soát thứ tự ghi: nếu nhiều update cùng key được flush không đúng thứ tự, DB có thể nhận bản cũ ghi đè bản mới. Khi flush nên lấy giá trị mới nhất từ cache (như code mẫu) thay vì replay từng delta theo thứ tự ngẫu nhiên.
- Quên invalidate trong Write-Around: ghi thẳng DB mà không
DELkey cũ khiến cache giữ bản cũ tới khi TTL hết. Hãy invalidate ngay cạnh lệnh ghi DB.
Tổng Kết & Quiz
Tổng kết
- Read-Through: cache layer tự load DB khi miss; app chỉ nói chuyện với cache. Khác Cache-Aside ở chỗ logic đọc DB nằm trong cache layer chứ không ở application code.
- Write-Through: ghi đồng bộ cả cache và DB → cache luôn fresh; đổi lại write latency cao và cache chứa cả data ít đọc lại.
- Write-Behind (Write-Back): ghi cache trước, flush xuống DB bất đồng bộ theo batch → write rất nhanh, gộp ghi; nhưng có risk mất data nếu cache chết trước khi flush.
- Write-Around: ghi thẳng DB, không ghi cache; cache chỉ điền khi đọc → tránh làm bẩn cache, nhưng data mới miss lần đọc đầu.
- Cache và DB là hai hệ thống tách rời, không atomic mặc định → mọi pattern ghi phải xử lý lỗi từng phần.
- Chọn pattern theo workload (read-heavy/write-heavy), mức chấp nhận mất data và yêu cầu freshness.
Quiz 5 câu
- Read-Through khác Cache-Aside ở điểm cốt lõi nào? Vì sao Read-Through không phải tính năng có sẵn của Redis?
- Write-Through đảm bảo cache luôn fresh bằng cách nào, và phải trả giá gì về latency cũng như bộ nhớ cache?
- Trong Write-Behind, dữ liệu có thể mất ở thời điểm nào? Cần những cơ chế gì để giảm rủi ro mất data?
- Khi nào chọn Write-Around thay vì Write-Through? Nhược điểm của Write-Around với read-after-write là gì?
- Vì sao không thể coi việc ghi cache và ghi DB trong Write-Through là một thao tác atomic? Khi ghi cache lỗi nhưng DB đã ghi thành công thì nên xử lý ra sao?
Đáp án gợi ý
- Điểm cốt lõi: vị trí của logic "đọc DB khi miss". Cache-Aside để logic đó trong application code; Read-Through để trong cache layer/abstraction, app chỉ gọi cache. Redis là key-value store thuần, không biết về DB của bạn, nên phải tự hiện thực Read-Through bằng một lớp bọc truyền vào hàm loader.
- Write-Through ghi đồng bộ cùng giá trị vào cả DB và cache trong cùng thao tác nên ngay sau ghi cache đã có bản mới nhất (stale window gần như 0). Trả giá: write latency cao hơn (đợi cả hai hệ thống) và cache chứa cả data ghi-một-lần-không-đọc-lại, tốn RAM (cần TTL/eviction).
- Mất data trong cửa sổ từ lúc ghi cache tới khi flush xuống DB thành công: nếu cache crash/mất điện/OOM hoặc worker flush lỗi mà chưa ghi DB, thay đổi mất. Giảm rủi ro: hàng đợi bền vững có ack (Redis Streams/MQ), bật persistence/replication, retry khi flush lỗi, không dùng cho data không được phép mất.
- Chọn Write-Around khi ghi nhiều nhưng ít đọc lại (log, audit, append-only) để tránh làm bẩn cache. Nhược điểm: dữ liệu mới luôn miss ở lần đọc đầu (phải xuống DB), nên không hợp đọc-lại-ngay-sau-ghi.
- Vì cache và DB là hai hệ thống riêng, không có transaction phủ cả hai nên không atomic. Khi DB đã ghi xong nhưng ghi cache lỗi, nên invalidate (
DEL) key đó để lần đọc kế tiếp MISS và nạp lại bản đúng từ DB, đồng thời log lỗi để theo dõi.
Bài tiếp theo
Bài 11 chuyển sang một nền tảng then chốt cho mọi pattern ở trên: thiết kế cache key — cách đặt tên key có namespace, version, tránh trùng và dễ invalidate theo nhóm.
