BitSet能扛十亿级标签筛选,因纯内存位运算、10亿用户仅需约125MB内存、操作耗时微秒级;但要求用户ID为连续紧凑整数,需通过本地缓存的双写映射转换业务UID,推荐用RoaringBitmap替代原生BitSet。

BitSet 为什么能扛住十亿级标签筛选?
因为它是纯内存位运算,不查磁盘、不走索引、不建倒排表——10 亿用户只需约 125MB 内存(10⁹ ÷ 8 ÷ 1024²),一次 AND 或 OR 操作平均耗时在微秒级。它不是“快一点”,而是把“集合计算”降维成 CPU 寄存器级别的位操作。
但注意:BitSet 本质是「用户 ID → bit 位置」的严格映射,所以它要求用户 ID 是连续、紧凑、从 0 开始的整数。现实系统中直接用业务 UID(如字符串或带空洞的自增 ID)会崩——这是第一个也是最常踩的坑。
如何把分散的业务 UID 映射到 BitSet 的 bit 位?
必须做一层「UID → 紧凑序号」的双写映射,且该映射本身要支持低延迟查询。常见错误是用 MySQL 或 Redis 存映射表,结果过滤前先查 ID 转换,反而拖慢整体响应。
- 推荐用
RoaringBitmap替代原生BitSet:它内部自动分块 + 稀疏压缩,对稀疏 ID(比如只用了 1% 的 0~10⁹ 范围)内存节省超 90%,且支持rank()/select()快速反查原始 UID - 映射服务必须本地缓存 + 定期全量同步:例如用
ConcurrentHashMap<long integer></long>缓存最近 100 万 UID 的序号,后台线程每 5 分钟从离线宽表拉取最新映射快照并 reload - 禁止在实时筛选路径上触发任何远程调用或磁盘 IO:所有 bit 位计算必须在内存中完成,否则“秒级”就变成“秒+”
多标签组合筛选时,AND/OR 顺序怎么安排才不爆内存?
BitSet 的 AND 操作会逐步缩小结果集,OR 则可能急剧膨胀。如果先算 A OR B OR C(三个千万级标签),中间结果可能撑到几亿 bit,再跟一个 AND D 就容易 OOM。
实操策略:
- 永远先执行
AND:把强约束标签(如city = '北京'、age_group = '25-30')优先计算,快速收窄基数 - 对
OR分组预聚合:把语义相近的 OR 条件(如多个兴趣标签interest IN ('AI', 'ML', 'DL'))提前合并为一个预计算 BitSet,存在本地 LRU 缓存中 - 设置硬性内存阈值:例如单次筛选不允许中间 BitSet 超过
50_000_000Lbits(≈6MB),超限则退化为分批处理或提示“条件过于宽泛”
和布隆过滤器、RoaringBitmap、ClickHouse 的适用边界在哪?
BitSet 不是万能的,它和其它方案是互补而非替代关系:
-
BitSet适合:已知全量用户池、标签可预生成、需精确交并补、QPS 高、延迟敏感(如实时广告人群圈选) -
BloomFilter适合:存在性判断(“这个 UID 是否在黑名单里?”),但不能用于计数或取用户列表 -
RoaringBitmap是 BitSet 的工业增强版:支持 64 位 UID、自动压缩、高效 rank/select,生产环境应默认用它而非原生 BitSet -
ClickHouse适合:标签条件动态拼接、涉及多表 JOIN、需要 SQL 接口或历史回溯——但它做不到 sub-100ms 的纯内存位运算
真正难的从来不是“怎么用 BitSet”,而是怎么让业务侧定义的“30 岁女性+近 7 天下单+偏好美妆”这种自然语言条件,稳定、低开销地翻译成一串 RoaringBitmap 的 AND/OR 调用链——这背后依赖的是标签元数据管理、计算 DAG 编译、以及失败时的优雅降级能力。

















