RoaringBitMap学习和实践

学习内容:

  • 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)

参考:
高效压缩位图RoaringBitmap的原理与应用1
高效压缩位图RoaringBitmap的原理及使用2

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值