IIWAB Roaring Bitmap - IIWAB

Roaring Bitmap

IIWAB 2月前 ⋅ 96 阅读

Roaring Bitmap——稀疏场景下的Bitmap最优解

Bitmap(位图)是“用二进制位表示元素存在性”的高效结构,但遇到稀疏数据(比如ID是雪花ID、UUID这类不连续值)时,普通Bitmap会浪费99%的空间(比如ID是100000,但要从位0存到100000)。

Roaring Bitmap就是为解决“稀疏Bitmap空间浪费”而生的优化方案——它通过“分段+压缩”,把稀疏Bitmap的空间利用率提升了10~100倍,同时保持高效的集合运算(交、并、差)。

一、什么是Roaring Bitmap?

Roaring Bitmap(简称RB)是一种优化型Bitmap,核心思想是:

把整数ID分成“高16位”和“低16位”,高16位作为“容器编号”,低16位存在对应容器中;不同容器根据数据密度,用不同的压缩方式存储。

它的目标是:在稀疏场景下省空间,在密集场景下不损失效率

二、核心原理:“分段+多容器”的压缩逻辑

Roaring Bitmap把32位整数(用户ID常用类型)分成两部分:

  • 高16位:表示“容器组”(共2^16=65536个容器);
  • 低16位:表示“容器内的元素”(每个容器最多存2^16=65536个元素)。

对每个容器,RB会根据“元素数量”自动选择3种存储方式(按需切换,空间最优):

1. ArrayContainer(数组容器)——元素少(≤4096个)

当容器内元素数量≤4096时,用有序短整型数组存储低16位值。

  • 举例:容器内只有3个元素(低16位是100、200、300),直接存成[100,200,300]
  • 优势:空间占用=元素数×2字节(比如3个元素占6字节),比Bitmap(65536位=8KB)省很多。

2. BitmapContainer(位图容器)——元素多(>4096个)

当容器内元素数量>4096时,直接用普通Bitmap存储低16位值。

  • 举例:容器内有5000个元素,用8KB的Bitmap(2^16位=65536位=8KB)存储;
  • 优势:元素多的时候,Bitmap的空间效率比数组高(5000个元素用数组存要10KB,Bitmap只要8KB)。

3. RunContainer(行程编码容器)——元素连续(可选)

当容器内元素是连续区间时(比如100-2000),用“起始值+长度”的行程编码存储。

  • 举例:元素是100-2000,存成(100, 1901)(100是起始,1901是长度);
  • 优势:连续元素的空间占用极省(不管多长,只存2个整数)。

三、Roaring Bitmap的核心优势

对比普通Bitmap和其他压缩Bitmap(比如EWAH Bitmap),RB的优势很明显:

1. 空间效率极高(尤其是稀疏场景)

  • 普通Bitmap:不管元素多稀疏,都要占“最大ID对应的位数”空间;
  • Roaring Bitmap:只存“有元素的容器”,且容器按需用数组/位图/行程编码,稀疏场景下空间是普通Bitmap的1/10~1/100。

2. 集合运算速度快

交、并、差等运算直接在“容器级别”做:

  • 相同容器组的元素直接运算;
  • 不同容器组的元素自动归并;
  • 数组/位图容器的运算都做了优化(比如数组用二分查找,位图用位运算)。

3. 兼容性好

支持32位/64位整数,能无缝替换普通Bitmap(很多库都提供了Bitmap的接口封装)。

四、Roaring Bitmap的典型应用场景

正因为“省空间+运算快”,RB在这些场景里是首选:

  • 海量用户去重统计:比如DAU/WAU/MAU(配合ClickHouse等数仓);
  • 权限控制:用RB存储“用户拥有的权限ID”,快速判断权限交集;
  • 日志去重:存储“已处理的日志ID”,避免重复消费;
  • 推荐系统:存储“用户看过的商品ID”,快速计算用户兴趣交集。

五、实战:Roaring Bitmap的基本用法(以Java库为例)

常用的Roaring Bitmap库是org.roaringbitmap:RoaringBitmap,核心操作很简单:

1. 初始化与添加元素

import org.roaringbitmap.RoaringBitmap;

public class RBDemo {
    public static void main(String[] args) {
        // 初始化RoaringBitmap
        RoaringBitmap rb = new RoaringBitmap();
        
        // 添加元素(用户ID)
        rb.add(1001L);
        rb.add(1002L);
        rb.add(1003L);
    }
}

2. 统计基数(去重数)

// 统计元素个数(对应DAU)
long count = rb.getCardinality();
System.out.println("去重数:" + count); // 输出3

3. 集合运算(比如合并两日DAU)

// 次日的RoaringBitmap
RoaringBitmap rb2 = new RoaringBitmap();
rb2.add(1001L);
rb2.add(1004L);

// 并集(两日总去重用户)
RoaringBitmap union = RoaringBitmap.or(rb, rb2);
System.out.println("总去重数:" + union.getCardinality()); // 输出4(1001、1002、1003、1004)

六、关键细节:使用Roaring Bitmap的注意事项

  1. ID尽量用整数类型:RB只支持整数(32/64位),非整数ID(比如UUID)要先转成整数(比如用哈希函数);
  2. 避免频繁增删(部分库不支持删除):部分RB实现是“只增”的,删除操作可能效率低;
  3. 注意容器切换的开销:当容器内元素数量跨越“4096”阈值时,会触发数组→位图的切换,频繁切换会有性能损耗。

总结

Roaring Bitmap是**“稀疏场景下Bitmap的最优替代方案”**——它用“分段+多容器压缩”解决了普通Bitmap的空间浪费问题,同时保持了高效的集合运算,是海量数据去重、统计场景的“利器”。


全部评论: 0

    我有话说: