布隆过滤器与 HyperLogLog:用误差换内存的概率数据结构

举报
yd_231768394 发表于 2026/11/04 22:56:19 2026/11/04
【摘要】 布隆过滤器与 HyperLogLog:用误差换内存的概率数据结构 前言“这个 URL 爬过没有”“今天有多少独立访客”——精确答案可能要几百 MB,而 1% 的误差只要几 MB。概率数据结构就是这笔交易的产物,用可控的误差换内存和速度。 一、布隆过滤器:只能确定"一定不存在"结构极简:一个 m 位的位数组 + k 个独立哈希函数。插入:对元素算 k 个哈希,把对应位置全置 1;查询:任一位...

布隆过滤器与 HyperLogLog:用误差换内存的概率数据结构

前言

“这个 URL 爬过没有”“今天有多少独立访客”——精确答案可能要几百 MB,而 1% 的误差只要几 MB。概率数据结构就是这笔交易的产物,用可控的误差换内存和速度。

一、布隆过滤器:只能确定"一定不存在"

结构极简:一个 m 位的位数组 + k 个独立哈希函数。

  • 插入:对元素算 k 个哈希,把对应位置全置 1;
  • 查询:任一位置为 0 → 一定不存在;全为 1 → 可能存在。

所以它只有假阳性,没有假阴性。误判率近似:

[ p \approx \left(1 - e^{-kn/m}\right)^k ]

实用参数:控制 1% 误判率约需 m/n ≈ 9.6 bit/元素、k ≈ 7。100 万元素约 1.2MB——这就是它最大的价值。

不支持删除:多个元素共享位,清一个会误伤其他。需要删除时用计数布隆(每位一个计数器,内存约 4 倍)或布谷鸟过滤器(支持删除、空间利用率更高)。

二、工程落地:缓存穿透防护

async function getUser(id) {
  // 第一道闸:一定不存在,直接返回
  if (!(await redis.call('BF.EXISTS', 'user:bf', `user:${id}`))) return null;

  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const row = await db.findUser(id);
  if (row) await redis.set(`user:${id}`, JSON.stringify(row), 'EX', 300);
  return row;   // 不存在时也建议写空值缓存,防单点打穿
}
BF.RESERVE user:bf 0.01 1000000   # 误判率 1%,容量 100 万
BF.ADD     user:bf user:1001
BF.EXISTS  user:bf user:1001

四个必须提前想清楚的点:

  1. 容量要预估并留余量:超过 capacity 后误判率飙升(RedisBloom 会自动扩容子过滤器,但精度和查询成本都变差),一般留 2 倍;
  2. 数据变更只能"加",不能"减":用户注销后过滤器里仍存在,表现为多查一次库——业务上可接受;
  3. 重建要有切换方案:全量重建期间会漏判,用"双过滤器 + 影子写入"平滑切换;
  4. 它替代不了空值缓存:两道闸叠加才对,布隆挡的是"明显不存在",空值缓存挡的是"刚查过不存在"。

三、HyperLogLog:基数统计的内存奇迹

原理一句话:哈希值的前导零长度,能粗略反映集合规模。单个估计方差太大,于是分成 ( m = 2^{14} = 16384 ) 个桶,取调和平均,再做小基数与大基数偏差修正。

指标 数值
标准误差 ( \approx 1.04/\sqrt{m} \approx 0.81% )
固定内存 约 12KB(稀疏编码时更小)
与基数关系 无关,10 万和 10 亿都占 12KB
能力边界 只能估算数量,不能取回元素,不能做减法
PFADD   uv:2026-11-04 user:1 user:2 user:3
PFCOUNT uv:2026-11-04
PFMERGE uv:week uv:2026-11-03 uv:2026-11-04

坑在这三处:

  • PFCOUNT 是 O(n) 的合并计算,热点接口每次调用会吃掉大量 CPU,应定期算好结果缓存;
  • PFMERGE 要求精度一致,不同实现(Redis / ClickHouse / 自研)不能混着合并;
  • 单 key 适合百万到千万级;十亿级 UV 建议分桶(按日期/业务拆 key)或用 ClickHouse 的 uniqCombined(底层同为 HLL,精度更高且能并行)。

四、还有哪些概率结构

结构 解决的问题 特点
Bloom Filter 存在性判断 最省内存,不支持删除
Counting Bloom 可删除的存在性判断 内存 ×4 左右
Cuckoo Filter 可删除 + 更快查询 负载因子高,实现复杂
HyperLogLog 去重计数 12KB 估任意基数
MinHash 集合相似度(Jaccard) 推荐去重、文档查重
Count-Min Sketch 频次估算 识别热点 key、重流量攻击

五、选型速查

需求 结构 100 万元素内存 误差
判断"可能存在/一定不存在" 布隆过滤器 ~1.2MB @1% 假阳性 1%
同上且需删除 计数布隆 / 布谷鸟 4~5MB 同上
独立访客、去重计数 HyperLogLog ~12KB 0.81%
两个集合有多像 MinHash 取决于签名长度 可调

六、绝对不要用错的地方

  1. 权限判断:布隆的假阳性意味着"没权限的人被放行",这是事故;
  2. 唯一性约束:注册去重必须精确,用数据库唯一索引;
  3. 计费与对账:HLL 的 0.81% 误差会被放大成真金白银的差额;
  4. 需要删除的场景:标准布隆做不到,别硬改业务去凑。

判断标准只有一句:误差会不会转化为资损、越权或数据错误。会,就不能用。

总结

分工记牢:布隆过滤器回答"有没有可能出现",HyperLogLog 回答"大概有多少"。

它们的经济学是"用 1% 的误差,换掉 99% 的内存和延迟"。在爬虫 URL 去重、缓存穿透防护、UV 统计、热点识别这些场景里,这笔买卖几乎总是划算的——前提是你清楚误差落在哪里,并且它落下的地方不重要。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。