Caching thực chiến cho backend

Sê-ri nâng cao về caching cho lập trình viên backend: cache-aside, TTL, eviction, thundering herd, stampede, cache invalidation, write-through/back, hit ratio, phân tầng, và các bẫy thực tế — mỗi bài demo THẬT bằng Redis + Go/PostgreSQL trong Docker, đo số liệu thật (độ trễ, hit ratio, số query DB tiết kiệm).

12/12 phần đã đăng Lập trình
1 Vì sao cache: đo thật Redis nhanh hơn query database 30 đến 300 lần Cache nghe như giải pháp vạn năng cho hiệu năng, nhưng nhanh hơn bao nhiêu và khi nào mới đáng? Bài này đo thật: một query gộp 100.000 dòng trên PostgreSQL tốn 8.2ms mỗi lần tính; cache kết quả vào Redis thì GET chỉ 0.028ms (pipelined) hoặc 0.26ms (round-trip đơn) — nhanh hơn 30 đến 300 lần. Nhưng nói thẳng: với query đơn giản lấy một dòng theo khoá chính, chênh lệch nhỏ hơn nhiều. Cache thắng rõ khi tính toán gốc đắt. 22/09/2026 · 7 phút đọc 2 Cache-aside: mẫu cache phổ biến nhất, đo thật hit ratio leo từ 37,6% lên 95% Cache-aside là mẫu cache gần như mọi hệ thống đều dùng: đọc cache trước, miss thì lấy từ database rồi điền vào. Bài này dựng nó bằng Go + Redis + PostgreSQL và đo thật: 20.000 request trên tập 1.000 sản phẩm, hit ratio leo từ 37,6% (mốc 1.000 request) lên 95%, và số truy vấn database đứng yên ở 1.000 — tiết kiệm 95% lượt chạm DB. Nói thẳng: query có index nên MISS chỉ chậm hơn HIT khoảng 4 lần, cái thắng lớn không nằm ở độ trễ một request. Kèm đo lỗi xuyên thủng cache: 200 id không tồn tại khiến 5.000/5.000 request đều chạm database. 22/09/2026 · 9 phút đọc 3 TTL và hết hạn: đo thật độ cũ của cache bằng TTL/2, và cơn bão miss do TTL đồng loạt TTL không phải là 'cache bao lâu' — nó là lời hứa về mức độ cũ tối đa của dữ liệu bạn phục vụ. Bài này đo thật trên Go + Redis + PostgreSQL: cho giá đổi trong DB mỗi 200ms rồi đọc qua cache với bốn TTL khác nhau. Kết quả: TTL 10s cho độ lệch trung bình 5,0 giây dữ liệu và chỉ 1 lần chạm DB; TTL 100ms cho 0,2 giây lệch nhưng 97 lần chạm DB — tăng TTL 100 lần đổi được 97 lần ít tải DB bằng đúng 25 lần cũ hơn. Và đo thêm cái bẫy TTL đồng loạt: 1.000 key nạp cùng lúc hết hạn dồn vào đúng một giây, thêm nhiễu ±20% thì đỉnh giảm còn 266. 22/09/2026 · 9 phút đọc 4 Eviction: đo thật một job quét xoá sạch key nóng dưới LRU, còn LFU giữ 199/200 Khi Redis chạm maxmemory, key bị đá ra trước cả khi TTL hết — và chính sách chọn key để đá quyết định cache còn dùng được hay không. Bài này đo thật trên Redis 7: làm nóng 200 key, cho một job quét tuần tự đi qua toàn bộ 2.000 key đúng một lượt, rồi đếm xem key nóng còn lại bao nhiêu. allkeys-lru giữ 18/200, allkeys-random giữ 11/200, allkeys-lfu giữ 199/200. Và đo luôn noeviction: 1.392/1.400 lệnh ghi bị từ chối với lỗi OOM command not allowed, tức bước điền cache của cache-aside hỏng hoàn toàn trong khi đọc vẫn chạy. 22/09/2026 · 9 phút đọc 5 Cache stampede: đo thật 500 request cùng miss một key, cả 500 lao thẳng xuống database Khi một key nóng biến mất — hết TTL hoặc bị đá — mọi request đang hỏi nó đều miss cùng lúc và đều đi tiếp xuống database. Đo thật trên Go + Redis + PostgreSQL: truy vấn gốc chạy một mình hết 6,2ms; cho 500 request cùng xuất phát thì cả 500 đều chạm database và request xấu nhất mất 1119,1ms — chậm hơn 180 lần, dù pool database chỉ có 50 kết nối. Kèm luật R × D tính trước quy mô stampede, và một cái bẫy khi đo khiến phép đo đầu tiên che mất gần một nửa hiện tượng. 22/09/2026 · 9 phút đọc 6 Chống stampede: singleflight và khoá Redis, đo thật 500 lượt chạm database rút xuống còn 1 Phần trước đo được 500 request cùng miss một key kéo theo đúng 500 truy vấn database và 1128,4ms cho request xấu nhất. Bài này chặn nó bằng hai cách và đo lại cùng kịch bản: singleflight gom lời gọi trùng trong một tiến trình — còn 1 truy vấn, trễ xấu nhất 7,2ms (nhanh hơn 157 lần); khoá Redis SET NX cũng còn 1 truy vấn nhưng chậm hơn chút (13,9ms). Rồi chạy 4 instance để lộ ra khác biệt thật sự: singleflight cho 4 truy vấn, khoá Redis vẫn 1. 22/09/2026 · 8 phút đọc 7 Cache invalidation: đo thật đảo thứ tự hai dòng code làm tỉ lệ đọc sai nhảy từ 7,26% lên 66,82% Cache-aside chỉ dựa vào TTL thì dữ liệu cũ tới cả phút. Xoá cache khi ghi là cách chữa — nhưng xoá vào lúc nào thì quan trọng hơn người ta tưởng. Đo thật trên Go + Redis + PostgreSQL với TTL 300s để mọi tính mới phải đến từ invalidation: ghi DB rồi xoá cache cho 7,26% lượt đọc sai và cache luôn khớp lại sau đó; đảo lại thành xoá cache rồi ghi DB cho 66,82% và cache KẸT ở giá trị sai cho tới khi TTL hết. Hai con số đó còn là hai loại lỗi khác hẳn nhau. 22/09/2026 · 8 phút đọc 8 Write-through vs write-back: đo thật write-back nhanh gấp 3, ghi DB ít đi 5 lần, và mất 19,4% dữ liệu khi chết Bảy phần trước đều để cache đứng ngoài đường ghi. Bài này đưa nó vào thẳng đường ghi và đo trên Go + Redis + PostgreSQL với 20.000 lượt tăng bộ đếm: write-through tốn 0,125ms mỗi lượt và vẫn 20.000 lệnh ghi xuống database, nhưng đổi lại 100% hit khi đọc ngay sau đó (write-around chỉ 80,2%). Write-back gộp lô cho 0,038ms và chỉ 3.926 lệnh ghi — nhanh gấp 3, ít đi 5 lần. Cái giá đo được: cho tiến trình chết trước lúc dồn nốt thì 3.882 lượt mất trắng, 19,41%. 22/09/2026 · 8 phút đọc 9 Hit ratio: đo thật vì sao cửa sổ 44,8% mà tích luỹ vẫn báo 95,8%, và vì sao 98% tệ hơn 90% Hit ratio là con số đầu tiên ai cũng đo, và là con số dễ hiểu sai nhất. Đo thật trên Go + Redis + PostgreSQL: cho một bot quét id lạ trong 3 giây, hit ratio cửa sổ 1 giây rơi xuống 44,82% ngay lập tức trong khi hit ratio tích luỹ chỉ chạm đáy 90,21% — cảnh báo đặt ở ngưỡng 90% không hề kêu. Và đo hai dịch vụ: A hit 98,01% đặt lên database 375 truy vấn mỗi giây, B hit 90,20% chỉ 80 — hit ratio là tỉ lệ, còn database chịu con số tuyệt đối. 22/09/2026 · 7 phút đọc 10 Hot key: đo thật một key hút 20,8% lưu lượng và dồn 4,44× tải lên đúng một shard Hit ratio 100%, mọi biểu đồ xanh, mà một node Redis nóng rực còn các node khác ngồi chơi. Bài này đo thật trên 1 triệu request phân bố Zipf qua 10.000 key, dùng chính thuật toán CRC16 của Redis Cluster: key nóng nhất chiếm 20,81% toàn bộ lưu lượng, top 10 key chiếm 51,46%, và shard nặng nhất trong 16 shard gánh 4,44 lần mức trung bình. Thêm shard không chữa được vì một key luôn thuộc đúng một slot. Tách key nóng thành 16 bản sao kéo độ lệch xuống 2,44× và tách 100 key nóng nhất xuống 1,63×. 22/09/2026 · 8 phút đọc 11 Cache phân tầng: đo thật L1 trong tiến trình cắt 97,6% lưu lượng Redis và nhanh hơn 800 lần Mười phần trước đều coi Redis là tầng cache duy nhất. Bài này thêm một tầng L1 ngay trong bộ nhớ tiến trình và đo trên 300.000 request Zipf qua 10.000 key: L1 với TTL 1 giây kéo lưu lượng tới Redis từ 300.000 xuống 7.438 — cắt 97,6% — và độ trễ mỗi request từ 39,2µs xuống 2,95µs. Đo riêng từng tầng còn lộ ra chuyện bất ngờ: Redis 36,9µs chỉ nhanh hơn PostgreSQL 1,26 lần cho query điểm có index, còn L1 thì nhanh hơn Redis 800 lần. Cái giá đo được: mỗi instance giữ bản riêng và trả dữ liệu cũ đúng bằng TTL của L1. 22/09/2026 · 8 phút đọc 12 Tổng kết Caching: gom 11 phép đo thành một hàm, DB giảm 19 lần và Redis giảm 60 lần Phần cuối loạt Caching gom mọi thứ đã đo thành một hàm đọc duy nhất — L1 trong tiến trình, singleflight, khoá SET NX, dấu vắng mặt, TTL có nhiễu — rồi đo nó cạnh cache-aside ngây thơ dưới cùng một lưu lượng: 32 người đọc song song, Zipf trên 5000 key, 5% là bot quét id không tồn tại. Kết quả: truy vấn database từ 101.739 xuống 5.300 (đúng sàn lý thuyết), lưu lượng Redis từ 100% xuống 0,1%, p99 từ 0,283ms xuống 0,001ms — đổi lại đuôi khởi động nguội xấu hơn, 388,7ms so với 21,5ms. Kèm khung quyết định và checklist rà soát trước khi lên production. 22/09/2026 · 9 phút đọc