Kiểu mới trong Redis 8 — JSON, Bloom/Cuckoo, Count-min, Top-K, t-digest, TimeSeries, Vector set, Query Engine
Redis 8.0 (5/2025) gộp vào bản chính những thứ trước đây là module của "Redis Stack" phải cài riêng: JSON, năm cấu trúc xác suất (Bloom, Cuckoo, Count-min sketch, Top-K, t-digest), TimeSeries và Query Engine; thêm vector set — kiểu mới do antirez viết. Câu hỏi phỏng vấn không còn là "Redis có những gì" mà là "khi nào dùng cái này thay vì PostgreSQL/Elasticsearch/Prometheus/vector DB, và nó sai theo hướng nào". Bài này đo từng cấu trúc với đáp án chính xác làm mốc: sai bao nhiêu, tốn bao nhiêu RAM, gãy ở đâu. Ví dụ: catalog sản phẩm, chống cache penetration, sản phẩm hot (e-commerce); độ trễ API, số người online, tìm khoá học tương tự (LMS).
Thuật ngữ dùng trong bài
JSONPath (cú pháp chỉ đường trong JSON:$.items[0].price,$..price) · sketch (cấu trúc tóm tắt dữ liệu trong bộ nhớ cố định, trả lời gần đúng) · false positive / sai dương (nói "có" khi thật ra không) · false negative / sai âm (nói "không" khi thật ra có) · Bloom filter (mảng bit + k hàm băm; chỉ sai dương) · Cuckoo filter (lưu "dấu vân tay" ngắn trong bảng; xoá được) · Count-min sketch (ma trận bộ đếm; ước lượng tần suất, chỉ đếm dư) · Top-K (giữ k phần tử xuất hiện nhiều nhất) · t-digest (tóm tắt phân phối bằng các "centroid"; ước lượng phân vị như p99) · quantile / phân vị (p99 = 99% giá trị nhỏ hơn nó) · retention (giữ dữ liệu bao lâu) · downsample / compaction (gộp dữ liệu thô thành điểm theo phút/giờ) · embedding (vector số thể hiện ý nghĩa của văn bản/ảnh) · HNSW (đồ thị nhiều tầng để tìm láng giềng gần đúng) · recall@10 (tỉ lệ 10 kết quả đúng nhất mà hệ thống tìm được) · quantization (lượng tử hoá: lưu số thực bằng ít bit hơn — Q8 = 8 bit, BIN = 1 bit) · inverted index (chỉ mục từ → danh sách tài liệu chứa từ đó).
Kiểm chứng: tài liệu Redis qua Context7 ngày 03/10/2026 (release notes 8.0 — danh sách thành phần gộp vào;BF.RESERVEcông thức số bit/hàm băm;CMS.INITBYPROB;TOPK.RESERVE;TS.CREATE,TS.ADD,TS.CREATERULE; vector set:VADD,VSIM, trang "Quantization modes";JSON.SETtham sốFPHAmới ở 8.8;JSON.DEBUG MEMORY). Cấu hình mặc định đọc bằngCONFIG GET bf-*/cf-*/ts-*vàVINFOtrên Redis 8.8.0. Lab chạy thật: Redis 8.8.0, PHP 8.3.33 + phpredis 6.3.0 (+ Laravel 12.64 cho phần code ứng dụng); đáp án chính xác tính trong PHP để so. Output:labs/redis/out/07*.txt.
Trả lời 30 giây
Hai nhóm. Nhóm có cấu trúc/chỉ mục — JSON (sửa từng nhánh nguyên tử; RAM ~3,7 lần văn bản), TimeSeries (nén: 1,3–7,5 byte/mẫu so với 96 của zset), vector set (HNSW, Q8 mặc định: 819 byte/vector 128 chiều, recall 95% so với đáp án thật), Query Engine (index phụ cho hash/JSON: ~560 byte/doc). Nhóm phác thảo xác suất — RAM cố định, trả lời gần đúng, và phải biết sai theo hướng nào: Bloom/Cuckoo chỉ sai dương (lab 0,5–1,2%), Count-min chỉ đếm dư (phần tử hiếm bị đếm dư trung bình +489%), Top-K đúng 8–10/10, t-digest lệch ≤ 1,2% ở p50 và < 0,2% ở đuôi. Bẫy hay dính:CF.DELphần tử chưa từng thêm xoá nhầm phần tử khác (sai âm);BINtụt recall còn 25%; filter hẹp làm vector search chậm 6 lần và recall 70%; số JSON vượt u64 thành số thực; phpredis đổi giá trị TimeSeries thànhtrue; Query Engine không tìm "ao" ra "áo". Và tất cả không có trên Valkey (bài 1) — chọn chúng là chọn Redis Ltd.
1. Bài toán gốc và bối cảnh ra đời
| Cấu trúc | Bài toán | Trước đó làm sao, giới hạn gì |
|---|---|---|
| JSON | Document lồng nhau, sửa một nhánh mà không đọc–ghi cả document | String JSON: đọc–sửa–ghi, mất cập nhật (bài 1); hash: chỉ phẳng |
| Bloom / Cuckoo | "Chắc chắn chưa có" với RAM rất nhỏ: chống cache penetration, email đã đăng ký, URL đã crawl | Set: chính xác nhưng 42 byte/phần tử (bài 3) |
| Count-min / Top-K | Tần suất và "top k" trên luồng rất lớn (sản phẩm hot, IP gọi nhiều) | Hash/zset đếm từng phần tử: RAM tỉ lệ với số phần tử khác nhau |
| t-digest | p50/p95/p99 trên hàng triệu mẫu, gộp được giữa các máy | Lưu hết mẫu rồi sắp xếp; trung bình của p99 các máy thì sai về toán |
| TimeSeries | Số liệu theo thời gian: nén, retention, gộp theo phút/giờ, lọc theo label | Zset score = thời gian: ~96 byte/mẫu, tự gộp, tự dọn |
| Vector set | Tìm "giống nhất" theo embedding: gợi ý khoá học, tìm kiếm ngữ nghĩa, RAG | Quét hết (O(N)) hoặc thêm vector DB riêng |
| Query Engine | Lọc/tìm theo nội dung field của hash/JSON (tag, khoảng số, full-text) | Tự duy trì set/zset làm index bằng tay |
Trước 8.0 chúng là module (RediSearch, RedisJSON, RedisBloom, RedisTimeSeries) gói trong "Redis Stack". Redis 8.0 đưa tất cả vào mọi bản phân phối, cùng lúc đổi license thành RSALv2/SSPLv1/AGPLv3 (bài 1 mục 8). Vector set ra đời ở 8.0 dưới dạng preview. Tối ưu cho: thêm khả năng mà không thêm hệ thống. Không tối ưu cho: dữ liệu lớn hơn RAM, nhu cầu chuyên sâu mà hệ thống chuyên dụng làm tốt hơn (search phức tạp, metrics dài hạn, vector hàng chục triệu).
2. Mô hình tư duy cốt lõi
- Nhóm "có cấu trúc": dữ liệu đầy đủ + chỉ mục trong RAM. JSON là cây; TimeSeries là chuỗi chunk nén; vector set là đồ thị HNSW; Query Engine là inverted index cạnh hash/JSON. Câu trả lời chính xác (vector set: gần đúng do HNSW + lượng tử hoá), giá là RAM và CPU duy trì chỉ mục.
- Nhóm "phác thảo": RAM cố định, đổi độ chính xác lấy bộ nhớ, sai số có hướng. Luật để nhớ: Bloom/Cuckoo nói "không" là chắc, nói "có" là "có thể"; Count-min nói số là "không ít hơn"; Top-K giữ phần tử nặng, bỏ phần đuôi; t-digest chính xác nhất ở hai đầu phân phối. Không xoá được (Bloom, CMS) hoặc xoá nguy hiểm (Cuckoo). Không liệt kê được phần tử.
3. Internals và quyết định thiết kế
3.1 JSON: cây trong RAM, sửa từng nhánh
### sửa từng phần, ngay trên server (07a-json.txt)
JSON.NUMINCRBY p:1 $.stock -1 → [56]
JSON.ARRAPPEND p:1 $.tags '"sale"' → [2]
JSON.GET p:1 $..price → [199000,199000,219000]
JSON.NUMINCRBY p:1 $..price 1000 → [200000,200000,220000] (một lệnh sửa mọi chỗ khớp)
JSON.SET p:1 $.color '"blue"' NX → true; lần 2 → false
JSON.SET p:1 $.a.b 1 (cha chưa có) → false
JSON.MERGE p:1 $ {stock:null, brand} → true (null = xoá field)
### 20 process × 50 lần JSON.NUMINCRBY $.stock 1 (bài 1: GET/SET JSON còn 101/1000)
JSON.GET p:2 $.stock → [1000]
### số lớn
ghi {"id":9007199254740993,"order_id":12345678901234567890,"trace":123456789012345678901234567,"price":0.1}
JSON.GET n → {"id":9007199254740993,"order_id":12345678901234567890,"trace":1.2345678901234568e26,"price":0.1}
JSON.NUMINCRBY n $.price 0.2 → [0.30000000000000004]
### document lớn: 10 000 phần tử trong $.items
MEMORY USAGE big = 1331232 byte (JSON text 356741 byte)
JSON.GET big $.items[5000].price 83 µs
JSON.GET big $..price 7280 µs
JSON.GET big $.items[?(@.price>499)].sku 4210 µs
JSON.ARRINSERT big $.items 0 {"sku":"new","price":1,"qty":1} 99 µs
JSON.NUMINCRBY big $.items[5000].qty 1 62 µs
JSON.GET big 2817 µs
### FPHA (8.8): mảng 768 số thực (embedding)
mặc định JSON.DEBUG MEMORY= 8208 byte, phần tử [0]: ghi -0.165956 đọc [-0.165956]
FPHA FP32 JSON.DEBUG MEMORY= 4112 byte, phần tử [0]: ghi -0.165956 đọc [-0.16595600545406342]
FPHA BF16 JSON.DEBUG MEMORY= 2064 byte, phần tử [0]: ghi -0.165956 đọc [-0.166015625]
FPHA FP16 với 70000.5 → value out of range for F16 at line 1 column 10
JSON được parse thành cây, mỗi nút một giá trị có kiểu: nhờ vậy sửa một nhánh là nguyên tử và rẻ (62–99 µs trên document 10 000 phần tử), và 20 process cùng cộng vẫn ra đúng 1000.
Cái giá: RAM ~3,7 lần văn bản (1,33 MB cho 357 KB JSON; bài 3: 528 byte/sản phẩm so với 305 của string, 241 của hash). Đường dẫn quét đệ quy ($..price) hay có
điều kiện ([?(@.price>499)]) là O(kích thước document): 4–7 ms chặn server. Số: nguyên tới u64 giữ nguyên, lớn hơn thành số thực — mất chữ số im lặng; số thực cộng dồn lệch
như mọi double. FPHA (8.8) ép mảng số thực về FP32/BF16/FP16: RAM giảm 2–4 lần, đổi lại mất chính xác và lỗi khi vượt miền của kiểu nhỏ.
3.2 Bloom và Cuckoo: "chắc chắn không" với vài bit mỗi phần tử
### Bloom: BF.RESERVE bf 0.01 100000 (sai dương 1%, sức chứa 100 000) (07b-prob.txt)
100 000 phần tử: MEMORY USAGE=137968 byte (11.0 bit/phần tử) | sai dương trên 100 000 phần tử lạ: 506 (0.51%) | sai âm: 0
nạp quá sức chứa lên 1 000 000: số sub-filter=4, MEMORY USAGE=2681176 | sai dương: 885 / 100 000 (0.89%)
NONSCALING (sức chứa 1 000), BF.ADD 1 100 phần tử khác nhau → {"thêm mới (1)":1000,"coi như đã có (0)":2,"lỗi":98} | lỗi: ERR non scaling filter is full
so sánh: set chứa cùng 100 000 phần tử: MEMORY USAGE=4237554 byte
### Cuckoo: CF.RESERVE cf 100000
100 000 phần tử: MEMORY USAGE=262240 byte | sai dương: 1177 / 100 000 (1.18%)
CF.EXISTS cf x25 (chưa từng thêm) → 1 (sai dương); CF.DEL cf x25 → 1; số phần tử ĐÃ thêm còn được thấy: 100000 → 99999 ← sai âm
CF.ADD dup 2 lần, CF.DEL dup 1 lần → CF.EXISTS dup = 1, CF.COUNT dup = 1 (cuckoo đếm như multiset)
Nạp quá sức chứa, Bloom mặc định tự thêm sub-filter (gấp đôi cỡ mỗi lần — bf-expansion-factor 2): không lỗi, nhưng RAM tăng và sai dương nhích lên (0,89%), mỗi lần hỏi
phải dò mọi sub-filter. NONSCALING thì đầy là lỗi — và trước khi lỗi, có 2 phần tử mới bị coi là "đã có" (trả 0) mà không báo gì. Cuckoo lưu dấu vân tay ngắn nên
CF.DEL được, nhưng chỉ an toàn với phần tử chắc chắn đã thêm: xoá một phần tử chưa thêm mà filter "tưởng" là có (sai dương) sẽ xoá nhầm dấu vân tay của phần tử
khác → lần đầu tiên có sai âm (lab: 100 000 → 99 999).
3.3 Count-min sketch và Top-K: tần suất trên luồng lớn
### luồng 1 000 000 lượt xem trên 10 000 sản phẩm, phân phối Zipf (s = 1.1) (07b-prob.txt)
CMS (sai số 0.1% tổng, xác suất 99%): MEMORY USAGE=56056 byte | top 10: lệch tối đa +123 lượt | 5 000 sản phẩm ít xem nhất (trung bình thật 8.6 lượt):
lệch trung bình +34.1, tối đa +182, lệch tương đối trung bình +489%
CMS có bao giờ đếm THIẾU? không (chỉ đếm dư)
Top-K (k=10): MEMORY USAGE=2800 byte | trùng với top 10 thật: 10/10 | p1=151146/151147 p2=70877/70889 p3=45153/45159 p4=32606/32753 … (ước lượng/thật)
(lần chạy trước: trùng 8/10)
CMS hợp để hỏi "p1 có bị xem quá 100 000 lần không" (hot), vô dụng để hỏi chính xác "p9999 được xem mấy lần" (đuôi). Top-K (HeavyKeeper) 2,8 KB giữ đúng nhóm đầu (8–10/10 qua hai lần chạy, có ngẫu nhiên bên trong) với số đếm sát (thiếu tối đa 0,5% ở nhóm đầu) — đủ cho "sản phẩm đang hot" trên trang chủ, không đủ cho bảng xếp hạng có thưởng.
3.4 t-digest: phân vị trên hàng triệu mẫu trong 10 KB
### t-digest: 1 000 000 độ trễ (log-normal, trung vị ~80 ms) (07b-prob.txt)
p50 thật 79.94 ms | t-digest 80.85 ms | lệch +1.14%
p90 thật 172.72 ms | t-digest 172.85 ms | lệch +0.07%
p99 thật 322.71 ms | t-digest 322.08 ms | lệch -0.19%
p99.9 thật 511.24 ms | t-digest 511.69 ms | lệch +0.09%
MEMORY USAGE td = 9864 byte (1 000 000 mẫu) | mảng số thực thô ~ 8 000 000 byte
t-digest gom mẫu thành các centroid (trung bình + số lượng), cho phép centroid lớn ở giữa phân phối và bắt centroid nhỏ ở hai đầu — vì thế chính xác nhất ở đuôi (p99.9 lệch 0,09%), kém
nhất ở trung vị (1,14%). Gộp được (TDIGEST.MERGE) — đó là cách đúng để có p99 của cả cụm từ nhiều máy, thay vì lấy trung bình p99 từng máy (sai về toán).
3.5 TimeSeries: nén, retention, downsample
### trùng timestamp (ts-duplicate-policy mặc định = block) (07c-ts.txt)
TS.ADD cùng timestamp lần 2 → phpredis trả false, lỗi: ERR TSDB: Error at upsert, update is not supported when DUPLICATE_POLICY is set to BLOCK mode, …
TS.ADD … ON_DUPLICATE LAST → 1790000000000
TS.GET online:r1 (phpredis mặc định) → [1790000000000,true] ← giá trị là simple string, phpredis đổi thành true
TS.GET online:r1 (OPT_REPLY_LITERAL) → [1790000000000,"125"]
TS.ADD mẫu cũ hơn retention (đến muộn) → false ERR TSDB: Timestamp is older than retention
TS.ADD mẫu đến muộn nhưng trong retention → 1790089000000 (nhận, chèn đúng chỗ)
### RAM: 1 000 000 mẫu, mỗi giây 1 mẫu (≈ 11,6 ngày)
smooth số nguyên đi chậm (số người online) MEMORY USAGE= 1275088 byte (1.28 byte/mẫu), chunk=302
noisy số thực ngẫu nhiên (độ trễ ms) MEMORY USAGE= 7531232 byte (7.53 byte/mẫu), chunk=1785
so sánh: zset score = ms, member = "ms:giá trị" → MEMORY USAGE=95726976 byte (95.7 byte/mẫu)
### downsample
TS.RANGE ts:smooth - + AGGREGATION max 3600000 (1 giờ) → 278 điểm, 13890 µs
TS.RANGE ts:smooth - + (thô, 1 000 000 điểm) → 207009 µs
compaction avg 1 phút: cpu:raw 3600 mẫu → cpu:avg1m 60 mẫu (bucket cuối chưa đóng), mẫu đầu [[1789999980000,"52"]]
TS.MRANGE - + FILTER kind=cpu → 2 series: cpu2:raw, cpu:raw
Mẫu được nén theo chunk (timestamp đều nhau → gần như miễn phí; giá trị ít đổi → vài bit): 1,3 byte/mẫu cho số nguyên đi chậm, 7,5 byte/mẫu cho số thực nhiễu — 13 tới 75 lần gọn hơn
zset. Mặc định DUPLICATE_POLICY là block: client gửi lại (retry) cùng timestamp nhận lỗi — chọn LAST/SUM theo ý nghĩa dữ liệu. Compaction rule
gộp tự động vào series khác (bucket căn theo mốc epoch, nên bucket đầu có thể bắt đầu trước dữ liệu); đọc thô 1 triệu điểm mất 0,2 giây chặn server — dashboard luôn đọc series đã gộp.
3.6 Vector set: HNSW + lượng tử hoá
EF lớn chỉ cứu được phần sai của đồ thị, không cứu được
phần sai do lượng tử hoá (lab: Q8 95,1% với EF mặc định lẫn EF 500).### 10 000 vector 128 chiều, 100 cụm (07d-vec.txt)
Q8 nạp 10 000 vector: 8494 ms | MEMORY USAGE= 8188474 byte ( 819 byte/vector) | recall@10 so với TRUTH cùng set: 100.0% | VSIM ~755 µs
NOQUANT nạp 10 000 vector: 8527 ms | MEMORY USAGE= 12028482 byte ( 1203 byte/vector) | recall@10 so với TRUTH cùng set: 100.0% | VSIM ~740 µs
BIN nạp 10 000 vector: 6757 ms | MEMORY USAGE= 7068482 byte ( 707 byte/vector) | recall@10 so với TRUTH cùng set: 89.6% | VSIM ~717 µs
### recall thật: so với 10 láng giềng đúng tính trên vector gốc (NOQUANT + TRUTH)
Q8 mặc định recall@10 = 95.1%
Q8 EF 500 recall@10 = 95.1%
BIN mặc định recall@10 = 24.7%
BIN EF 500 recall@10 = 24.5%
### FILTER: thuộc tính JSON cho từng phần tử (20 danh mục, giá 10k–1tr)
MEMORY USAGE sau khi gắn thuộc tính: 8668474 byte
FILTER '.cat == 7' (khớp 5%): trả trung bình 10.0/10 kết quả, recall 83.4% so với TRUTH cùng filter, ~4830 µs
FILTER '.cat == 7 and .price < 60000' (khớp ~0,25%): trả trung bình 9.6/10 kết quả, recall 70.4% so với TRUTH cùng filter, ~6369 µs
Ba điều đọc ra: (1) "recall 100%" có thể là ảo — TRUTH trên chính set đã lượng tử hoá so sánh với vector đã méo; so với đáp án trên vector gốc thì Q8 còn 95%,
BIN chỉ 25% (dữ liệu 128 chiều có cụm; BIN hợp hơn với embedding chiều cao và tìm thô — docs gọi là "coarse search"). (2) Phần lớn RAM là đồ thị, không phải vector: ở 128
chiều, Q8 tốn 819 byte trong đó vector chỉ 128 byte (hnsw-m 16 liên kết mỗi tầng). (3) Filter hẹp đắt và sót: HNSW phải duyệt thêm để tìm đủ phần tử khớp —
chậm 6–8 lần, recall 70–83%, có lúc trả ít hơn COUNT; FILTER-EF tăng công sức lọc. Nạp ~0,7–0,9 ms/vector qua mạng.
3.7 Query Engine: index phụ cho hash/JSON
10 000 sản phẩm JSON; FT.CREATE idx:p xong: num_docs=10000, used_memory tăng 5639056 byte cho index (~563 byte/doc) (07f-search.txt)
FT.SEARCH '@brand:{Coolmate} @price:[100000 300000] @stock:[1 +inf]' SORTBY price → tổng 177, 3 đầu: … (1482 µs)
FT.SEARCH idx:p '@name:áo' → 3750 kết quả
FT.SEARCH idx:p '@name:ao' → 0 kết quả
FT.SEARCH idx:p '@name:thoại' → 1250 kết quả
FT.SEARCH idx:p '@name:thoai' → 0 kết quả
FT.SEARCH '@brand:{Biti's}' (dấu nháy trong tag, không escape) → 0 kết quả
FT.SEARCH '@brand:{Biti\'s}' (escape) → 2000 kết quả
sửa JSON.NUMINCRBY p:5 → index cập nhật ngay: FT.SEARCH '@price:[89596 89596]' → 1 kết quả
Query Engine giữ inverted index (chữ, tag) và cây số (numeric) cạnh dữ liệu; mỗi lệnh ghi vào key khớp prefix được index đồng bộ — đọc thấy ngay. Giá: ~560 byte/doc cho 4 field, CPU mỗi lần ghi, và các bẫy của search engine "thô": không bỏ dấu tiếng Việt mặc định ("ao" không ra "áo"), ký tự đặc biệt trong tag phải escape (sai thì 0 kết quả, không lỗi). Search tiếng Việt nghiêm túc cần chuẩn hoá (lưu thêm field bỏ dấu) hoặc search engine có analyzer tiếng Việt.
4. Đảm bảo và giới hạn
| Cấu trúc | Hướng sai số / đảm bảo | Lab | Giới hạn chính |
|---|---|---|---|
| JSON | Chính xác; lệnh nguyên tử trên document | 1000/1000 khi 20 process cùng cộng | RAM ~3,7 × văn bản; số > u64 thành double; path đệ quy O(N) |
| Bloom | Chỉ sai dương, ≤ tham số (khi chưa quá sức chứa) | 0,51% (đặt 1%); quá sức chứa 0,89% | Không xoá; nạp quá → sub-filter hoặc lỗi |
| Cuckoo | Sai dương; xoá được phần tử đã thêm | 1,18%; xoá nhầm → sai âm | Xoá phần tử chưa thêm làm hỏng filter |
| Count-min | Chỉ đếm dư, ≤ ε × tổng với xác suất 1 − δ | Không đếm thiếu; đuôi +489% | Vô dụng cho phần tử hiếm; không liệt kê |
| Top-K | Gần đúng, có ngẫu nhiên | 8–10/10 đúng | Phần tử ở biên vào/ra thất thường |
| t-digest | Sai tương đối nhỏ, tốt nhất ở đuôi | ≤ 1,14% (p50), ≤ 0,19% (p90–p99.9) | Không cho giá trị chính xác từng mẫu |
| TimeSeries | Chính xác; trùng timestamp theo policy | 1,3–7,5 byte/mẫu | Mẫu cũ hơn retention bị từ chối; đọc thô nhiều điểm chặn server |
| Vector set | Gần đúng (HNSW + lượng tử) | Q8 95%, BIN 25%, filter hẹp 70% | RAM chủ yếu là đồ thị; một key = một node |
| Query Engine | Index cập nhật đồng bộ khi ghi | ~560 byte/doc | Analyzer mặc định không hợp tiếng Việt; Cluster cần cấu hình riêng (chưa verify) |
Khung lý thuyết: các sketch là ví dụ của đánh đổi không gian–độ chính xác có cận chặt: Bloom cần ~1,44·log₂(1/ε) bit/phần tử; Count-min cần O((1/ε)·log(1/δ)) ô;
t-digest có sai số tương đối nhỏ dần về phía đuôi. HNSW là tìm kiếm gần đúng với độ phức tạp ~O(log N) mỗi truy vấn, đánh đổi recall lấy tốc độ qua EF và M.
5. Failure modes
| # | Tình huống | Hậu quả (đo) | Phát hiện | Chữa / phòng |
|---|---|---|---|---|
| 1 | Bloom chống cache penetration nhưng dữ liệu bị xoá ở DB | Không xoá được khỏi Bloom → vẫn "có thể có", request vẫn xuống DB (đúng nhưng lọt) | Tỉ lệ lọt tăng dần | Dựng lại filter định kỳ; Cuckoo nếu cần xoá |
| 2 | Bloom nạp quá sức chứa | Sub-filter: RAM ×19 (138 KB → 2,7 MB), sai dương 0,89%; NONSCALING: lỗi + 2 phần tử bị coi là "đã có" | BF.INFO số filter, items | Ước lượng sức chứa; theo dõi items/capacity |
| 3 | CF.DEL phần tử chưa chắc đã thêm | Xoá nhầm → sai âm (100 000 → 99 999) | Gần như không thể | Chỉ xoá khi nguồn sự thật xác nhận đã thêm |
| 4 | Dùng CMS cho số liệu đuôi (thống kê từng sản phẩm) | Lệch +489% trung bình | Số liệu sản phẩm ít người xem "đẹp" bất thường | CMS chỉ cho câu hỏi về phần tử hot; đuôi đếm chính xác ở nơi khác |
| 5 | Retry gửi lại mẫu TimeSeries cùng timestamp | Lỗi DUPLICATE_POLICY … BLOCK | Log lỗi ghi metric | ON_DUPLICATE LAST/SUM theo ý nghĩa |
| 6 | Đọc TimeSeries qua phpredis/Laravel | Giá trị thành true | Biểu đồ toàn 1 | Redis::OPT_REPLY_LITERAL |
| 7 | Vector set dạng BIN cho embedding chiều thấp có cụm | Recall 25% | Đo recall với tập kiểm tra | Q8 mặc định; đo recall trên đáp án float trước khi chọn |
| 8 | Vector search + filter rất hẹp | Chậm 6–8 lần, recall 70%, trả thiếu kết quả | p99 tăng, số kết quả < COUNT | Tách set theo nhóm lớn (mỗi danh mục một key), FILTER-EF |
| 9 | ID dài > u64 trong JSON | Thành số thực, mất chữ số im lặng | ID sai khi đọc lại | Lưu ID dạng chuỗi |
| 10 | Search tiếng Việt không chuẩn hoá | "ao" → 0 kết quả | Người dùng báo "không tìm thấy" | Field bỏ dấu song song, hoặc search engine chuyên dụng |
| 11 | Chuyển sang Valkey / Redis cũ / managed service không hỗ trợ | ERR unknown command; RDB 14 không nạp được | Lúc migrate | Dữ liệu dựng lại được; adapter |
6. Vận hành trong production
- RAM — bảng nhanh để ước lượng (lab): Bloom 1%: ~11 bit/phần tử; Cuckoo: ~21 bit/phần tử; CMS (0,001; 0,01): 56 KB cố định; Top-K 10: 2,8 KB; t-digest: ~10 KB; TimeSeries: 1,3–7,5 byte/mẫu; vector 128 chiều Q8: ~820 byte (cộng thêm nếu chiều cao: ~1 byte/chiều); Query Engine: ~560 byte/doc cho 4 field; JSON: ~3,7 × văn bản.
- Cấu hình mặc định (8.8):
bf-error-rate 0.01,bf-initial-size 100(BF.ADD lên key chưa có sẽ tạo filter 100 phần tử rồi phình liên tục — luônBF.RESERVEtrước),cf-initial-size 1024,ts-duplicate-policy block,ts-chunk-size-bytes 4096, vector setquant-type int8,hnsw-m 16. - ACL: Redis 8 thêm category
@json,@bloom,@cuckoo,@cms,@topk,@tdigest,@timeseries,@search(release notes 8.0) — kiểm lại ACL hiện có khi nâng cấp. - Persistence/upgrade: các kiểu này lưu trong RDB dưới dạng module type; RDB 8.8 là format 14, không nạp vào Valkey/Redis 7.4 (bài 1). Nâng từ Redis Stack lên 8.x: dữ liệu module tương thích theo tài liệu, chưa verify trong lab.
- Laravel/phpredis 6.3: JSON, Bloom, CMS, Top-K, t-digest, TimeSeries, Query Engine đều phải
executeRaw(tự thêm prefix); vector set có method (vadd,vsim… — có prefix); giá trị simple string (TimeSeries) thànhtruetrừ khi bậtOPT_REPLY_LITERAL; có lỗi phpredis trảfalse(ERR, WRONGTYPE), có lỗi lại némRedisException(lỗiFPHAcủa JSON) — xử lý cả hai. - Debug lúc 3 giờ sáng: "Redis chậm" → SLOWLOG có
JSON.GET … $..,TS.RANGEthô,VSIM … FILTER,FT.SEARCHkhông giới hạn?; "RAM tăng" →BF.INFOsố sub-filter, Query Engine index (FT.INFO), stream/TS không retention.
6.1 Code Laravel (đã chạy thật)
use Illuminate\Support\Facades\Redis;
$prefix = Redis::connection()->client()->getOption(\Redis::OPT_PREFIX);
$k = fn (string $name) => $prefix . $name; // executeRaw KHÔNG tự thêm prefix
Redis::executeRaw(['JSON.SET', $k('product:9'), '$', json_encode($product, JSON_UNESCAPED_UNICODE)]);
Redis::executeRaw(['JSON.NUMINCRBY', $k('product:9'), '$.stock', '-1']);
Redis::executeRaw(['BF.RESERVE', $k('bf:email'), '0.001', '100000']); // luôn RESERVE trước
$maybe = Redis::executeRaw(['BF.EXISTS', $k('bf:email'), $email]) === 1; // 0 = chắc chắn chưa có
Redis::connection()->client()->setOption(\Redis::OPT_REPLY_LITERAL, true); // để TS.GET trả "3" chứ không phải true
Redis::executeRaw(['TS.ADD', $k('ts:orders'), '*', '3', 'ON_DUPLICATE', 'SUM']);
Redis::vadd('emb:courses', $embedding, 'laravel-nang-cao'); // vector set: có method, có prefix
$similar = Redis::vsim('emb:courses', $queryVector, ['COUNT' => 5, 'WITHSCORES' => true]);
### output (07e-laravel.txt)
JSON.NUMINCRBY qua executeRaw → [4]
Redis::get('product:9') trên key JSON → NULL (key là JSON: WRONGTYPE → phpredis false → Laravel đổi thành null, giống key không tồn tại)
BF.EXISTS a@x.vn / b@x.vn → 1 / 0
TS.GET qua executeRaw → [1791016071803,true] ← giá trị thành true (simple string)
Redis::vsim([0.1,0.85,0.25]) → {"laravel-nang-cao":0.9986881017684937,"laravel-co-ban":0.9983288049697876}
key thật: lab:bf:email, lab:emb:courses, lab:product:9, lab:ts:orders
Bẫy của đoạn code: bật OPT_REPLY_LITERAL trên connection dùng chung làm đổi cả kết quả các lệnh khác trả "OK" (thành chuỗi "OK" thay vì true) — nên dùng một
connection riêng cho TimeSeries; Redis::get() trên key sai kiểu trả null như cache miss (bài 1).
7. So sánh và ma trận quyết định
| Nhu cầu | Redis 8 | Thay thế | Chọn Redis khi | Chọn thay thế khi |
|---|---|---|---|---|
| Document lồng nhau, sửa từng phần | JSON | PostgreSQL jsonb, MongoDB | Dữ liệu nóng, cần < 1 ms, dựng lại được | Là nguồn sự thật, cần truy vấn/transaction |
| "Chắc chắn chưa có" | Bloom / Cuckoo | Set (chính xác), unique index DB | Tập rất lớn, chấp nhận sai dương | Cần chính xác hoặc cần xoá thường xuyên |
| Tần suất / hot items trên luồng | CMS / Top-K | Hash/zset đếm thật, ClickHouse | Chỉ quan tâm phần tử hot, RAM cố định | Cần số chính xác cho mọi phần tử |
| Phân vị độ trễ | t-digest | Prometheus histogram, HDR histogram | Cần phân vị realtime trong app, gộp được | Đã có hệ thống metrics chuẩn |
| Số liệu theo thời gian | TimeSeries | Prometheus, TimescaleDB, InfluxDB | Vài nghìn series, giữ ngắn, đọc nhanh trong app | Metrics hạ tầng, giữ dài, nhiều series |
| Tìm vector tương tự | Vector set (hoặc vector index của Query Engine) | pgvector, Qdrant, Milvus | Vài trăm nghìn vector, cần độ trễ thấp, đã có Redis | Hàng chục triệu vector (RAM), cần lọc phức tạp, cần bền |
| Lọc/tìm theo nội dung | Query Engine | PostgreSQL index, Elasticsearch/OpenSearch | Lọc tag/khoảng số trên dữ liệu đã ở Redis | Full-text tiếng Việt, facet phức tạp, dữ liệu lớn |
Dùng khi: dữ liệu đã/nên ở Redis, cần độ trễ thấp, quy mô vừa RAM, chấp nhận (và hiểu) sai số của sketch. Tránh khi: cần là nguồn sự thật, cần chạy trên Valkey/managed service không hỗ trợ, hoặc hệ thống chuyên dụng đã có sẵn trong công ty — thêm một "Elasticsearch nhỏ" trong Redis là thêm một thứ phải vận hành mà ít người hiểu.
8. Góc nhìn tech lead: tác động tổ chức
- Lock-in rõ nhất của series: lab bài 1 — Valkey 9.1.2 trả
ERR unknown commandchoJSON.SET,BF.ADD,TS.ADD,VADD. Chọn các kiểu này = chọn Redis (license RSALv2/SSPLv1/AGPLv3) hoặc dịch vụ của Redis Ltd; managed service khác hỗ trợ đến đâu phải kiểm từng nơi (chưa verify). - Exit strategy: giữ nguồn sự thật ngoài Redis (catalog trong DB, sự kiện thô trong log) để dựng lại sketch/index/vector ở hệ thống khác. Sketch (Bloom, HLL, CMS) không chuyển đổi được sang định dạng khác — chỉ dựng lại được từ dữ liệu gốc.
- Số xấp xỉ cần chủ: mọi con số từ CMS/Top-K/t-digest/HLL trên dashboard phải ghi "xấp xỉ" và có người hiểu hướng sai số; không dùng cho tính tiền, xếp hạng có thưởng.
- Kỹ năng team: vector search và search engine có nghề riêng (đo recall, analyzer, đánh giá chất lượng). "Có sẵn trong Redis" không có nghĩa là không cần người hiểu.
- ADR mẫu: "Dùng Bloom (1%, sức chứa X) để chặn tra cứu email chưa đăng ký; dựng lại hằng đêm từ bảng users; sai dương chấp nhận được vì chỉ dẫn tới một truy vấn DB; không dùng Cuckoo vì không có nguồn xác nhận khi xoá."
9. Anti-patterns và bài học thực tế
- Thay PostgreSQL bằng RedisJSON "cho nhanh": mất bền vững, mất truy vấn, RAM gấp 3,7 lần văn bản.
BF.ADDkhôngBF.RESERVE: filter mặc định 100 phần tử, phình thành hàng chục sub-filter.- Xoá trong Cuckoo theo sự kiện không chắc chắn: sai âm — phá đúng lời hứa của filter.
- Báo cáo lượt xem từng sản phẩm bằng CMS: đuôi lệch hàng trăm phần trăm.
- Chọn BIN để tiết kiệm RAM mà không đo recall: 25%.
- Lưu snowflake/ID lớn dạng số trong JSON: vượt u64 là mất chữ số; nhiều client JS còn mất từ 2⁵³.
Nguồn công khai: release notes Redis 8.0 (gộp module, license); docs BF.RESERVE (công thức bit/hàm băm), trang vector set "Quantization modes" (Q8/BIN/NOQUANT).
Chưa tìm được postmortem công khai về các kiểu này — số liệu là lab tự dựng trên dữ liệu tổng hợp (Zipf, log-normal, vector có cụm).
Điều docs không nói rõ: "recall 100%" khi so TRUTH trên set đã lượng tử hoá; NONSCALING Bloom trả 0 cho vài phần tử mới trước khi báo đầy; phpredis đổi giá trị
TimeSeries thành true; Query Engine mặc định không hợp tiếng Việt có dấu.
10. Câu hỏi phụ interviewer hay hỏi tiếp
- Bloom filter sai theo hướng nào? Dùng chống cache penetration thế nào?
- Chỉ sai dương. Request cho id:
BF.EXISTS= 0 → chắc chắn không có → trả 404 ngay, không chạm DB; = 1 → đi cache/DB như thường. Không xoá được → dựng lại định kỳ. - Bloom và Cuckoo khác gì?
- Cuckoo lưu dấu vân tay nên xoá được và thường tốt hơn ở sai số thấp, nhưng tốn RAM hơn (lab 262 KB vs 138 KB) và xoá phần tử chưa thêm gây sai âm.
- Count-min sketch đếm thiếu được không?
- Không — va chạm chỉ cộng thêm, lấy min qua các hàng. Sai số tỉ lệ với tổng lượt, nên phần tử hiếm lệch tương đối rất lớn.
- Lấy p99 độ trễ của cả cụm từ 10 server?
- Mỗi server một t-digest,
TDIGEST.MERGErồi hỏi p99. Không lấy trung bình p99 của từng server. - TimeSeries khác zset theo thời gian thế nào?
- Nén (1,3–7,5 vs 96 byte/mẫu), retention, compaction, lọc theo label, policy trùng timestamp. Zset hợp khi cần xoá/sửa theo member và đếm theo khoảng.
- Vector set đủ cho RAG/gợi ý không?
- Với vài trăm nghìn vector, đã có Redis: được. Đo recall trên đáp án float (Q8 ~95%); cẩn thận BIN và filter hẹp; RAM chủ yếu là đồ thị; một key một node; dữ liệu phải dựng lại được.
- Khi nào không dùng các kiểu mới này?
- Khi phải chạy trên Valkey/managed service không hỗ trợ, khi cần là nguồn sự thật, khi quy mô vượt RAM, hoặc khi hệ thống chuyên dụng (Elasticsearch, Prometheus, pgvector) đã có và team đã quen.
Phụ lục: lab đã chạy
Môi trường: Redis 8.8.0 (redis:alpine, có sẵn module search 8.8.0, ReJSON 8.8.0, bf 8.8.0, timeseries 8.8.0, vectorset). Phần đo viết bằng PHP 8.3.33 +
phpredis 6.3.0 (kết nối thẳng, không prefix), đáp án chính xác tính trong PHP; phần code ứng dụng qua Laravel 12.64 (REDIS_PREFIX=lab:). Dữ liệu tổng hợp có seed cố định.
cd labs/redis
./07-redis8.sh # chạy hết → out/07a … 07f
./07-redis8.sh vec # 1 phần: json | prob | ts | vec | search | laravel
Chưa verify: Query Engine trên Redis Cluster; vector index của Query Engine (HNSW/SVS-VAMANA) so với vector set; embedding thật (OpenAI 1 536 chiều) — lab dùng vector tổng hợp 128 chiều; nâng cấp dữ liệu module từ Redis Stack lên 8.x; hỗ trợ trên các managed service.