1.bitmap
bitmap是大数据处理中常用的一种数据结构,可以用来做去重与基数统计。
考虑如下场景:
有1千万个整数,整数的范围在1到1亿之间。如何快速判断某个整数是否在这1千万个整数中?
如果用散列这种常见的数据结构,内存的占用肯定是非常大,基本不可用。
如果开辟一个长度为1亿的整形数组,一个int型占用4个字节,最后算下来内存占用也很大。
有的同学说可以将数组换成布尔数组,可以减小内存占用。对比整形,布尔型内存占用确实较小,但是很多语言中布尔型变量大小是1个字节,内存占用还是相当可观。
实际上,表示true/false只需要一个bit就够了,一个字节有8个bit,将布尔数组换成二进制,就是我们的bitmap,可以将内存空间再压缩到原来的1/8。
2.RoaringBitmap
bitmap的问题在于,不管业务中实际的元素基数有多少,它占用的内存空间都恒定不变。还是以上面的例子来说明,整数的范围仍然是1到1亿之间,但是数字只有10000个,需要判断某个数字在不在这10000个数字中?
如果还是使用原始的bitmap结构,事先需要开辟的内存空间还是1亿个bit,但是只有1w个bit位是1,其余都是0。数据越稀疏,空间浪费越严重。
为了解决位图稀疏存储浪费空间的问题,出现了很多稀疏位图的压缩算法,RoaringBitmap就是其中的优秀代表。
RoaringBitmap的主要思路如下:
将32位无符号整数按照高16位分桶,即最多可能有216=65536个桶,论文内称为container。存储数据时,按照数据的高16位找到container(找不到就会新建一个),再将低16位放入container中。也就是说,一个RoaringBitmap就是很多container的集合。
论文中的原图如下
图中示出了三个container:
第一个container是高16位为0000H,container中基数为1000,具体数值为1000个62的倍数。
第二个container是高16位为0001H,container中基数为100,具体数值为[2^16, 2^16+100)
第三个container是高16位为0001H,container中基数为2^15,存储有[2×2^16, 3×2^16)
区间内的所有偶数
3.container种类
在roaringbitmap中主要有以下几种小桶:arraycontainer(数组容器),bitmapcontainer(位图容器),runcontainer(行程步长容器)
ArrayContainer
在创建一个新container时,如果只插入一个元素,roaringbitmap默认会用ArrayContainer来存储。当ArrayContainer的容量超过4096,即8k后,会自动转成BitmapContainer(这个所占空间始终都是8k)存储。
bitmapcontainer
这个容器就是第一部分讲的普通位图,只不过这里位图的位数为2^16=65536
个,也就是66536个bit。计算下来起所占内存就是8kb。然后每一位用0,1表示这个数不存在或者存在。
runcontainer
它使用可变长度的unsigned short数组存储用行程长度编码(RLE)压缩后的数据。举个例子,连续的整数序列11, 12, 13, 14, 15, 27, 28, 29会被RLE压缩为两个二元组11, 4, 27, 2,表示11后面紧跟着4个连续递增的值,27后面跟着2个连续递增的值。
4.在java中的实现与调用
java中已有相关的类库实现了RoaringBitmap相关功能,使用前我们引入如下依赖:
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.9.10</version>
</dependency>
并使用如下代码测试
import org.roaringbitmap.RoaringBitmap;
public class RoaringBitMapDemo {
public static void t1() {
RoaringBitmap rr = RoaringBitmap.bitmapOf(1, 2, 3, 1000);
RoaringBitmap rr2 = new RoaringBitmap();
rr2.add(4000L, 4005L);
// 第三个数值,索引从0开始
int thirdvalue = rr.select(3);
// 2这个值的排序,排序索引从1开始,如果不在是0
int indexoftwo = rr.rank(2);
boolean c1 = rr.contains(1000);
boolean c2 = rr.contains(7);
System.out.println("bofore or, rr is: " + rr);
System.out.println("thirdvalue is: " + thirdvalue);
System.out.println("indexoftwo is: " + indexoftwo);
System.out.println("c1 is: " + c1);
System.out.println("c2 is: " + c2);
System.out.println();
// 做并集
RoaringBitmap rror = RoaringBitmap.or(rr, rr2);
rr.or(rr2);
System.out.println("rr is: " + rr);
System.out.println("rr2 is: " + rr2);
System.out.println("rror is: " + rror);
boolean equals = rror.equals(rr);
System.out.println("is equals: " + equals);
// 获取位图中元素个数
long cardinality = rr.getLongCardinality();
System.out.println("cardinality is: " + cardinality);
}
public static void main(String[] args) {
t1();
}
}
最后输出的结果
bofore or, rr is: {1,2,3,1000}
thirdvalue is: 1000
indexoftwo is: 2
c1 is: true
c2 is: false
rr is: {1,2,3,1000,4000,4001,4002,4003,4004}
rr2 is: {4000,4001,4002,4003,4004}
rror is: {1,2,3,1000,4000,4001,4002,4003,4004}
is equals: true
cardinality is: 9
5.总结
1.原始bitmap适用于数据分布比较稠密场景,RoaringBitMap适用于数据分布比较稀疏的场景。
2.roaringbitmap做交并差的速度也比原始的bitmap快,主要原因是roaringbitmap将原始大块的bitmap分成了各个小块,数据分布稀疏的情况下小块container数量较少,大大减少了需要处理的bitmap大小。