bitset过滤需系统设计:用户id映射为连续整数、按标签建bitset、位运算组合筛选、批量用long[]操作;千万级适用,十亿级须升级roaringbitmap或olap引擎。

BitSet 过滤不是“用个工具类就完事”,而是围绕用户 ID 映射、标签组织、批量计算和内存控制四点系统设计。它在千万级场景下效果显著,在十亿级必须升级为 RoaringBitmap 或嵌入 OLAP 引擎,否则会因内存膨胀和稀疏浪费直接失效。
用户 ID 必须映射为连续紧凑整数
BitSet 的位索引只能是 从 0 开始的非负整数,业务中常见的字符串 UID、雪花 ID 或带空洞的自增 ID 无法直接使用。
- 若用户注册有序(如 MySQL AUTO_INCREMENT),可用
uid - minUid得到 bitIndex - 若 UID 是 UUID 或不规则长整型,需构建只读双写映射表:用
ConcurrentHashMap<string integer></string>缓存热 ID,后台线程定期加载全量id2index快照 - 禁止在筛选路径中触发远程查映射——否则毫秒级运算会被一次 Redis 查询拖成百毫秒
按标签建 BitSet,而非按用户建
每个标签维护一个 BitSet,bitIndex 对应该用户是否命中该标签。这是节省内存的关键逻辑。
- 1000 万用户 → 单个 BitSet 约占 1.25 MB;100 个标签共约 125 MB,远低于 HashMap(常超 100 MB/标签)
- 组合筛选靠位运算:
bitSetVip.and(bitSetActive7d)求交集,bitSetIOS.or(bitSetAndroid)求并集 - 避免反模式:不要为每个用户分配一个 BitSet(内存爆炸),也不要将标签存在用户对象里(丧失集合计算能力)
批量操作用 long[] 替代逐位访问
单次 set(i) 或 get(i) 有方法调用与边界检查开销;对千万级数据导出、统计等场景,应绕过封装直操作底层存储。
- 读取:调用
bitSet.toLongArray()获取快照,手动遍历 long 数组 + 位移提取每一位 - 写入:构造好 long 数组后,用
BitSet.valueOf(longArr)重建,比循环 set() 快 5–10 倍 - 注意:
toLongArray()返回的是逻辑长度(含末尾 0),安全可用,无需担心越界
十亿级必须换 RoaringBitmap 或上 OLAP 引擎
原生 BitSet 在十亿用户下不再适用:单标签就要 125 MB 内存,且不压缩稀疏数据、无并行容器计算、无法分片或序列化。
- RoaringBitmap 自动按密度切分容器(Array / Bitmap / Run),稀疏标签(如仅 0.5% 用户)内存可压至 1/10
- 所有 and/or/xor 运算基于 container 并行,百亿量级交集实测毫秒内完成
- 生产推荐路径:ClickHouse 中用
AggregateFunction(groupBitmap, UInt32)实时聚合,SQL 直接调用bitmapAnd()圈选










