咆哮位图
学习内容:
- RoaringBitMap解决痛点
- RoaringBitMap原理
- 应用实践
RoaringBitMap解决痛点
- 场景
给定40亿不重复数据在[0 - 2^32 -1 ]区间内,快速判断某位数是否在该集合内
- 解决方式
40亿数据直接存的话需要大约15G内存(4000000000*4/1024/1024/1024)
我们可以使用位图进行优化,这样一个字节可以表示8位数,大大节省内存,这样算下来只使用(2^32/8/1024/1024 = 512M)内存
-
痛点
当上述只存储了0这个元素,只有0的位置是1 其他全是0.但是仍然占用了512M内存,这样就很浪费
为了解决位图不适合稀疏储存的缺点,roaringbitmap的出现就是为了压缩稀疏位图
RoaringBitMap原理
RBM的主要思路是:将32位无符号整数按照高16位分桶,即最多可能有216=65536个桶,论文内称为container。存储数据时,按照数据的高16位找到container(找不到就会新建一个),再将低16位放入container中。也就是说,一个RBM就是很多container的集合
container原理
ArrayContainer
public final class ArrayContainer extends Container implements Cloneable {
private static final int DEFAULT_INIT_SIZE = 4;
private static final int ARRAY_LAZY_LOWERBOUND = 1024;
static final int DEFAULT_MAX_SIZE = 4096;
private static final long serialVersionUID = 1L;
protected int cardinality;
short[] content;
结构很简单,只有一个short[] content,将16位value直接存储(如上图)
short[] content始终保持有序,方便使用二分查找,且不会存储重复数值。
因为这种Container存储数据没有任何压缩,因此只适合存储少量数据。
ArrayContainer占用的空间大小与存储的数据量为线性关系,每个short为2字节,因此存储了N个数据的ArrayContainer占用空间大致为2N字节。存储一个数据占用2字节,存储4096个数据占用8kb。
根据源码可以看出,常量DEFAULT_MAX_SIZE
值为4096,当容量超过这个值的时候会将当前Container替换为BitmapContainer。
BitmapContainer
public final class BitmapContainer extends Container implements Cloneable {
protected static final int MAX_CAPACITY = 65536;
private static final long serialVersionUID = 2L;
private static final int BLOCKSIZE = 128;
public static final boolean USE_BRANCHLESS = true;
final long[] bitmap;
int cardinality;
这种Container使用long[]存储位图数据。我们知道,每个Container处理16位整形的数据,也就是0~65535,因此根据位图的原理,需要65536个比特来存储数据,每个比特位用1来表示有,0来表示无。每个long有64位,因此需要1024个long来提供65536个比特。
因此,每个BitmapContainer在构建时就会初始化长度为1024的long[]。这就意味着,不管一个BitmapContainer中只存储了1个数据还是存储了65536个数据,占用的空间都是同样的8kb。
RunContainer
public final class RunContainer extends Container implements Cloneable {
private static final int DEFAULT_INIT_SIZE = 4;
private static final boolean ENABLE_GALLOPING_AND = false;
private static final long serialVersionUID = 1L;
private short[] valueslength;
int nbrruns;
RunContainer中的Run指的是行程长度压缩算法(Run Length Encoding),对连续数据有比较好的压缩效果。
它的原理是,对于连续出现的数字,只记录初始数字和后续数量。即:
对于数列11,它会压缩为11,0;
对于数列11,12,13,14,15,它会压缩为11,4;
对于数列11,12,13,14,15,21,22,它会压缩为11,4,21,1;
源码中的short[] valueslength中存储的就是压缩后的数据。
container性能
- 读取
只有BitmapContainer可根据下标直接寻址,复杂度为O(1),ArrayContainer和RunContainer都需要二分查找,复杂度O(log n)
- 内存
ArrayContainer一直线性增长,在达到4096后就完全比不上BitmapContainer了,适合小数据量的存储
BitmapContainer是一条横线,始终占用8kb,当大于4096会自动会转为 BitmapContainer
实践:
spark海量数据count(distinct)优化
object BitMapUtil {
//序列化 bitmap
def serBitMap(bm:RoaringBitmap): Array[Byte] ={
val stream = new ByteArrayOutputStream()
val dataOutputStream: DataOutputStream = new DataOutputStream(stream)
bm.serialize(dataOutputStream)
stream.toByteArray
}
//反序列化bitmap
def deSerBitMap(bytes:Array[Byte]): RoaringBitmap ={
val bm: RoaringBitmap = RoaringBitmap.bitmapOf()
val stream = new ByteArrayInputStream(bytes)
val inputStream = new DataInputStream(stream)
bm.deserialize(inputStream)
bm
}
}
package count_distinct
import org.apache.spark.sql.{Encoder, Encoders}
import org.apache.spark.sql.expressions.Aggregator
import org.roaringbitmap.RoaringBitmap
// in buffer out
class BitMapGenUDAF extends Aggregator[Int,Array[Byte],Array[Byte]] {
override def zero: Array[Byte] = {
//构造一个空的 bitmap
val bm: RoaringBitmap = RoaringBitmap.bitmapOf()
//将bitmap序列化为字节数组
BitMapUtil.serBitMap(bm)
}
override def reduce(b: Array[Byte], a: Int): Array[Byte] = {
//将buffer反序列化为bitmap
val bm: RoaringBitmap = BitMapUtil.deSerBitMap(b)
bm.add(a)
//将bitmap序列化为字节数组
BitMapUtil.serBitMap(bm)
}
override def merge(b1: Array[Byte], b2: Array[Byte]): Array[Byte] = {
val bitmap1: RoaringBitmap = BitMapUtil.deSerBitMap(b1)
val bitmap2: RoaringBitmap = BitMapUtil.deSerBitMap(b2)
bitmap1.or(bitmap2)
BitMapUtil.serBitMap(bitmap1)
}
override def finish(reduction: Array[Byte]): Array[Byte] = reduction
override def bufferEncoder: Encoder[Array[Byte]] = Encoders.BINARY
override def outputEncoder: Encoder[Array[Byte]] = Encoders.BINARY
}
//使用 bitmap
//1.注册udaf函数
session.udf.register("gen_bitmap",udaf(new BitMapGenUDAF)) // 这个函数出来的是字节数组,如果要计算具体的基数得写一个udf
//2.计算基数函数
def card(byteArray:Array[Byte]): Int ={
val bitmap: RoaringBitmap = BitMapUtil.deSerBitMap(byteArray)
bitmap.getCardinality
}
//3.注册函数
session.udf.register("get_card",card _)
session.sql(
s"""
|select
| gen_bitmap(courseid) as cnt_arr,
| get_card(gen_bitmap(courseid)) as cnt
|from sparktuning.course_shopping_cart
|""".stripMargin
).show(false)