1. 為什么要用「近似」?
隨著業務量爆發式增長,精確統計 的內存或 CPU 成本可能難以接受。例如:
- 統計一天內 唯一 IP 數 —— 用
SET
精確去重,百萬 IP→占用數百 MB。 - 統計海量商品銷量、實時計算 P99 延遲、獲取 TOP-N 熱門頁面……
概率型(Probabilistic)數據結構 通過犧牲可控的精度,換取極低內存與高吞吐,成為解決此類問題的利器。Redis-Bloom 模塊為 Redis 提供了一整套成熟實現,go-redis v9 已封裝全部指令,開箱即用。
2. 近似集合操作
需求 | 數據結構 | 誤差特點 | 是否可刪除 |
---|---|---|---|
是否出現過 | Bloom Filter | 假陽性(可調) | ? |
Cuckoo Filter | 假陽性(略低) | ? | |
集合基數 | HyperLogLog | 標準誤差 ≈ 0.81% | — |
2.1 Bloom Filter —— 最小內存的存在性判斷
// 批量寫入
okList, _ := rdb.BFMAdd(ctx,"recorded_users","andy", "cameron", "david", "michelle",
).Result() // [true true true true]// 判斷存在
exists, _ := rdb.BFExists(ctx, "recorded_users", "cameron") // true
absent, _ := rdb.BFExists(ctx, "recorded_users", "kaitlyn") // false
應用場景
- 去重寫日志、判重爬蟲 URL、垃圾郵件過濾。
- 典型誤判率 1%~0.01%,可通過
BF.RESERVE
自定義。
2.2 Cuckoo Filter —— 支持刪除
rdb.CFAdd(ctx, "other_users", "paolo") // true
rdb.CFDel(ctx, "other_users", "paolo") // true
rdb.CFExists(ctx, "other_users", "paolo") // false
選型要點
對比項 | Bloom | Cuckoo |
---|---|---|
寫入速度 | 更快 | 略慢 |
內存 | 更省 | 稍高 |
DEL | 不支持 | 支持 |
查詢性能 | 略慢 | 更快 |
結論:需要刪除就選 Cuckoo;極端節省內存或寫密集則用 Bloom。
2.3 HyperLogLog —— 基數統計王者
// group:1 共 3 個不同成員
rdb.PFAdd(ctx, "group:1", "andy", "cameron", "david")
// group:2 共 4 個不同成員
rdb.PFAdd(ctx, "group:2", "kaitlyn", "michelle", "paolo", "rachel")cnt1, _ := rdb.PFCount(ctx, "group:1") // 3
cnt2, _ := rdb.PFCount(ctx, "group:2") // 4// 合并后去重
rdb.PFMerge(ctx, "both_groups", "group:1", "group:2")
total, _ := rdb.PFCount(ctx, "both_groups") // 7
- 固定 12 KB 即可統計 2?? 去重計數。
- 誤差 ≈ 0.81%。
- 典型場景:UV、去重后計數、唯一用戶量。
3. 近似統計運算
需求 | 數據結構 | 誤差可調 | 典型場景 |
---|---|---|---|
頻率估計 | Count-min Sketch | ? | 商品銷量、PV 計數 |
分位數/百分位 | t-digest | ? | 延遲 P99、身高分布 |
TOP-K 排名 | Top-K | ? | 最熱商品/頁面 |
3.1 Count-min Sketch —— 近似頻率查詢
// 誤差≤0.1%,錯誤概率≤0.05%
rdb.CMSInitByProb(ctx, "items_sold", 0.01, 0.005)rdb.CMSIncrBy(ctx, "items_sold","bread", 300, "tea", 200, "coffee", 200, "beer", 100)rdb.CMSIncrBy(ctx, "items_sold", "bread", 100, "coffee", 150)freq, _ := rdb.CMSQuery(ctx, "items_sold", "bread", "coffee") // [400 350]
- 內存固定(取決于誤差參數),無需隨 item 增長。
- 「流式」場景比
ZINCRBY
?ZRANGE
省內存高效。
3.2 t-digest —— 分位數利器
rdb.TDigestCreate(ctx, "male_heights")
rdb.TDigestAdd(ctx, "male_heights", 175.5, 181, 160.8, 152, 177, 196, 164)p75, _ := rdb.TDigestQuantile(ctx, "male_heights", 0.75) // [181]
cdf, _ := rdb.TDigestCDF(ctx, "male_heights", 181) // ≈0.7857
min, _ := rdb.TDigestMin(ctx, "male_heights") // 152
max, _ := rdb.TDigestMax(ctx, "male_heights") // 196
- 采樣點多時仍保持 O(1) 內存。
- 適合 P95、P99 時延監控、A/B 實驗指標。
3.3 Top-K —— 熱門榜單實時統計
// 創建「Top3」榜單
rdb.TopKReserve(ctx, "top_3_songs", 3)// 批量增加播放量
rdb.TopKIncrBy(ctx, "top_3_songs","Starfish Trooper", 3000,"Only one more time", 1850,"Rock me, Handel", 1325,"How will anyone know?", 3890,"Average lover", 4098,"Road to everywhere", 770)// 列出前 3
top3, _ := rdb.TopKList(ctx, "top_3_songs")
// [Average lover, How will anyone know?, Starfish Trooper]// 查詢某歌曲是否在榜
hit, _ := rdb.TopKQuery(ctx, "top_3_songs", "Starfish Trooper") // [true]
- 內部基于 Count-min + 堆,內存固定。
- 適合實時 TOP-N 榜單,如熱搜、熱賣、熱文章。
4. 選型與實踐指南
場景 | 建議數據結構 | 備注 |
---|---|---|
唯一訪問 IP / UV | HyperLogLog | 固定 12 KB / 0.81% 誤差 |
日志去重、注冊判重 | Bloom / Cuckoo | 需刪除 → Cuckoo;否則 Bloom |
商品銷量、頁面 PV | Count-min Sketch | 更關注趨勢而非精確值 |
延遲分布監控 | t-digest | 秒級更新 P95/P99 |
最熱商品/話題榜單 | Top-K | 高并發流式排名 |
拼寫/命名黑名單 | Bloom Filter | 快速 filtration |
提示
所有模塊均屬于 RedisBloom,需加載模塊或使用 Redis Stack。
go-redis v9 命令位于
github.com/redis/go-redis/v9
,大寫前綴如BFMAdd
、CMSInitByProb
。針對誤差/容量調優:
- Bloom:
BF.RESERVE key errorRate capacity
- CMS:
CMS.INITBYPROB key error prob
- t-digest:可選壓縮率
TDIGEST.CREATE key compression
5. 踩坑 & 性能建議
問題 | 解決方案 |
---|---|
誤差過大 | 調大容量 / 調小 errorRate;但會增內存 |
Top-K 大量并發 INCRBY 壓力 | 采用 Pipeline 批量上報 |
同一 Key 頻繁刪除(Cuckoo 滿) | 提前預估容量,使用 CF.RESERVE |
CMS 超過容量后誤差逐漸增大 | 按天/小時拆分 Key 或定期快照重建 |
t-digest 估算極端分位不準 (P99.9) | 增大 compression、增加樣本數 |
HyperLogLog 需要合并太多 Key | 兩層:先分片,再定期 PFMERGE |
6. 生產 Checklist
- 模塊加載:
redis-stack-server
或--loadmodule redisbloom.so
。 - 版本:Redis ≥ 6.2,RedisBloom ≥ 2.6。
- 監控:結合
INFO modules
觀察 BF/CMS 內部 stats;或自定義 Metrics。 - 備份:RDB/AOF 包含概率結構,但恢復后誤差不變;無須額外處理。
- 容量預估:使用統計學公式或壓測,寧可稍大,不可過小。
- 代碼封裝:為每種結構寫 DAO,隱藏底層命令,方便替換與調參。
7. 總結
概率型數據結構 = 低成本 + 可接受誤差。
在 高 QPS / 大數據量 / 對精度容忍度高 的場景,它們能顯著減少內存與 IO,提升系統整體吞吐。合理選型、正確調參,再配合 go-redis 的高效封裝,你就能輕松構建 高性能、低成本 的統計與去重服務。
推薦閱讀
- RedisBloom 官方案例
- ACM 論文《Less Hashing, Same Performance: Building a Better Bloom Filter》
- Dunning & Ertl《A Comprehensive Evaluation of Approximate Cardinality Estimation Algorithms》
Happy Coding,愿你的 Redis 永不爆表!