bitset 不适合亿级用户去重或权限过滤,因其底层为 long[] 数组,要求索引非负且稠密;真实用户 id 多为字符串或稀疏 long,直接映射会导致 oom 或海量空洞,且不支持并发、序列化与持久化。

BitSet 本身不适合直接用于亿级用户去重或权限过滤——它在 JVM 堆内线性扩展,1 亿个用户 ID(假设为 int)需要约 12.5MB 内存,但实际中用户 ID 多为字符串、UUID 或稀疏 long,直接映射到 BitSet 索引会崩溃或产生海量空洞。
为什么直接 new BitSet(100_000_000) 在生产环境会失败
BitSet 底层是 long[] 数组,索引必须是非负整数且尽量稠密。真实业务中:
- 用户 ID 往往是
uuid、phone_number、email等字符串,无法直接作为 BitSet 下标 - 即使转成
long,ID 分布极稀疏(比如最大值是9223372036854775807),BitSet 会分配超大数组,触发OutOfMemoryError: Java heap space - BitSet 不支持并发写入,多线程 put 操作需手动加锁,吞吐骤降
- 它不提供序列化/持久化能力,服务重启后状态全丢,无法用于长期权限过滤
RoaringBitmap 是生产可用的 BitSet 替代方案
RoaringBitmap 把整数集合按高 16 位分桶,每桶用 bitmap、array 或 run 编码,兼顾内存与速度。它才是大数据去重和权限过滤的实际载体:
- 对 1 千万个 32 位用户 ID,压缩后内存通常
- 支持高效
or、and、xor、cardinality运算,UV 计算就是一次roaringBitmap.cardinality() - 可序列化为字节数组,存 Redis、Hologres
roaringbitmap字段或本地磁盘 - Hologres、Druid、ClickHouse 均原生支持 RoaringBitmap 类型,查询时直接下推计算
示例(Java):
import org.roaringbitmap.RoaringBitmap; RoaringBitmap usersInBeijing = RoaringBitmap.bitmapOf(1001, 1002, 1005, 1010); RoaringBitmap usersOnIOS = RoaringBitmap.bitmapOf(1002, 1005, 1008, 1012); RoaringBitmap iosInBeijing = RoaringBitmap.and(usersInBeijing, usersOnIOS); System.out.println(iosInBeijing.getCardinality()); // 输出:2
权限过滤场景下,Bitmap 必须配合 UID 映射表使用
权限系统要判断“用户 A 是否有某资源访问权”,本质是查 user_id → [resource_id] 的集合关系。但 resource_id 同样可能是字符串或稀疏 long,不能直接塞进 Bitmap。正确路径是:
- 建立一张
uid_mapping表,把原始user_id(如"u_8a3f9b2e")映射为紧凑的uid_int32(如12045) - 权限规则存储为
role_id → RoaringBitmap,其中 bitmap 存的是uid_int32值 - 运行时通过
SELECT uid_int32 FROM uid_mapping WHERE user_id = ?查出整数 ID,再查 bitmap 是否包含该值(contains()) - 务必给
uid_mapping.user_id加唯一索引 + clustering key,否则映射查询变瓶颈
跳过映射、强行用 hashCode() % N 构造 bitmap 下标,会导致哈希冲突、权限误判,线上事故高发。
Druid 和 Hologres 中 Bitmap 索引的真实生效条件
不是建了维度就自动启用 Bitmap 索引——它只对满足以下条件的查询生效:
- Druid:维度字段类型为
string且基数适中(IN、NOT IN、SELECT COUNT(DISTINCT ...)且无复杂表达式(如upper(user_id)) - Hologres:列类型为
int或bigint,且显式声明USING BITMAP索引(CREATE INDEX idx ON tbl USING BITMAP (user_id);) - 两者均要求过滤条件能下推到 segment/tablet 级别;若 SQL 中有
JOIN后再过滤,Bitmap 索引大概率失效 - Druid 的
HyperLogLog聚合虽快(亚秒),但结果是近似值;真要精确 UV,必须用RoaringBitmap+ 预聚合表
最容易被忽略的一点:Bitmap 索引对写入有额外开销。Druid 每个 segment 生成时都要构建位图,segment 越小、数量越多,构建时间越长,实时摄入延迟上升。建议单个 segment 控制在 500MB–2GB,避免过度切分。










