• 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结构很简单,只有一个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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这种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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    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
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
        //使用 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

  • 相关阅读:
    python爬虫-某政府网站加速乐(简单版)实例小记
    华为云HECS云服务器docker环境下安装nginx
    Cannot read properties of undefined (reading ‘prototype‘)
    【深度学习】最大熵马尔科夫、CRF、条件随机场、最大匹配法
    数据库三范式
    对称(镜像)二叉树
    webpack打包vue项目添加混淆方式,解决缓存问题
    本地websocket服务端暴露至公网访问【内网穿透】
    linux usb驱动移植(1)
    Spring Cloud Gateway的使用总结
  • 原文地址:https://blog.csdn.net/Lzx116/article/details/126283692