精确去重和Roaring BitMap

精确去重和Roaring BitMap

互联网行业常见的一个业务需求就是求UV(日活)和N日留存,这就涉及到去重计数(COUNT DISTINCT)的计算.

BitMap概述

精确去重算法主要通过BitMap来实现,它本质上是定义了一个很大的 bit 数组,每个元素对应到 bit 数组的其中一位

一个Integer是32-bit, 一共有Integer.MAX_VALUE = 2 ^ 32个值,对于原始的Bitmap来说,这就需要2 ^ 32长度的bit数组
通过计算可以发现(2 ^ 32 / 8 bytes = 512MB), 一个普通的Bitmap需要耗费512MB的存储空间
不管业务值的基数有多大,这个存储空间的消耗都是恒定不变的,这显然是不能接受的
而Roaring Bitmap作为压缩性能更好的位图索引,广泛应用于众多成熟的开源大数据平台(Kylin、Druid、ES等)

对于非Integer类型的数据(比如String类型),可以通过数据字典映射成Integer

Roaring Bitmap的数据结构

  • Roaring bitmap用来表示所有32-bit的unsigned integer的集合(共2 ^ 32 = 42 9496 7296个)
  • 这个数足够覆盖一款产品的用户量了

Roaring bitmap的数据结构为Key-Value的键值对:

  • Key: 根据业务值整数的high 16-bits进行分桶(共2 ^ 16 = 65536个), 每一个Roaring Bitmap的Key值(first-level)都存放在short(16-bit)类型的有序数组中: short[] keys
  • Value: 用于存放业务值整数的low 16-bits的一个container. 每一个Roaring Bitmap有一个Container数组: Container[] values

三种不同类型的Container

Roaring bitmap共有三种不同类型的Container, 分别是Array Container, Bitmap Container, Run Container.

Array Container

Array Container通过16-bit unsigned integer的有序数组存放(short in Java)业务值

数组的初始长度为DEFAULT_INIT_SIZE = 4, 扩容的规则为:

  • 若 size < 64, 则capacity * 2;
  • 若 64 < size < 1067, 则capacity * 1.5;
  • 若 1067 < size, 则capacity * 1.25;
  • 不会分配超过DEFAULT_MAX_SIZE = 4096大小的capacity;
  • 特别处理: 若size离最大值(4096)只差1/16时, 直接升舱到最大值(4096)

Array Container有一个counter用来追踪基数
序列化之后Array Container消耗的存储空间为2c+2bytes, c表示基数

Bitmap Container

Bitmap Container通过固定size为1024的64-bit(long in Java)的数组存放业务值, 这个数组刚好能存下2 ^ 16个数字
有一个counter用来存放1的个数
序列化之后Bitmap Container消耗的存储空间固定为为8192bytes

Run Container

Run Container通过存放pair<start_value, length>于16-bit integer(short in Java)的数组中来表示业务值, 例如11、12、13、14、15可以被优化成11, 4
其中start_value表示起始值, length为该起始值后连续1的长度
这个short数组的capacity也是动态分配

Run Container的基数可以通过SUM(length)计算出来, 没有用另外的counter进行追踪
序列化之后Run Container消耗的存储空间为2+4rbytes, r表示pair<start_value, length>的数目

Contrainer之间的转换

  • 如果开始的时候是一个空的Roaring bitmap, 那么当添加一个业务值时,Array Container就会被创建
  • 当插入新的业务值时, 基数超过4096, Array Container会转化成Bitmap Container
  • 通过runOptimize function触发原container的扫描, 决定是否转换为Run Container, 只有当存储消耗比Array Container或Bitmap Container小的时候, 才会发生转换
  • 根据计算可以得出,基数大于4096的时候,2 + 4r < 8192, 那么不应超过2047个pair;
  • 基数小于4096的时候,pair的个数应该小于基数的一半

Roaring Bitmap逻辑操作的简单分析

在Roaring Bitmap中, 一个bitmap包含一个RoaringArray类型的成员变量highLowContainer, 用于存储数据: RoaringArray highLowContainer
RoaringArray包含两个数组, 分别是short[] keysContainer[] values

Key值(first-level)的处理

每一个Roaring Bitmap的Key值(first-level)都存放在short(16-bit)类型的有序数组中

对两个bitmap做逻辑运算时,先做Key值的比较

Highlevel计算框架

  1. 依次迭代两个bitmap的key数组

  2. 若key值相同, 则对value的container进行逻辑计算, 不同类型的container有不同的操作, 计算结束后, 两个key数组迭代器步长+1

  3. 如果key值不同, key值较小的迭代器的步长(index), 会一直加到自己的key值恰好等于较大的key值(二分查找), 找不到则返回较小的数组的size

    Find the smallest integer index larger than pos such that array[index].key=x. If none can be found, return size. Based on code by O. Kaser.

  4. 如果是Union计算, 在第3步较小的迭代器移动步长的过程中, 会把每次经过的key值和value值添加到作为结果的bitmap中

  5. 对于Union计算, 必须把两个数组都迭代完全; 对于Intersection计算, 只要有一个数组迭代完就可以结束了

Bitmap Container vs Bitmap Container的操作

对于Union操作,输入为两个bitmap container, 迭代两个container中的1024个long值(long[] bitmap)

  • 分别做位运算中的|操作, 得到一个新的long[] bitmap数组
  • 同时利用Long.bitCount()计算新的bitmap container的基数

对于Intersection操作, 同样输入两个bitmap container

  • 先迭代两个container中的1024个long值(long[] bitmap)一次,计算交集的基数
  • 基数大于4096, 则再次迭代两个container,分别做位运算中的&操作,得到一个新的long[] bitmap数组
  • 基数不大于4096,也再次迭代两个container,并将long值&操作的结果转换成short类型,存放在最终的结果Array Container中

Bitmap Container vs Array Container的操作

对于Union操作, 输入为一个bitmap container和一个array container

  • 先把这个bitmap container复制一遍
  • 遍历array container的数组, 将每个值v换算成复制后的bitmap container中long[] bitmap数组的对应下标i, 取出该bitmap的值bitmap[i]和v做相关的|操作,写入这个bitmap[i]
  • 遍历完成后这个复制的bitmap container即为结果bitmap container

对于Intersection操作, 输入为一个bitmap container和一个array container

  • 先创建一个和array container一样大小的新的array container
  • 遍历旧的array container, 查找里面的值是否存在于bitmap container中
  • 存在则写入新的array container的short[]数组中
  • 这个新的array container即为交集结果

Array Container vs Array Container的操作

对于Union操作, 输入为两个array container

  • 直接相加两个container的基数,若基数和 > 4096, 则把两个container的值置于一个新的bitmap container中计算,结果的基数如果不大于4096, 则把结果转换成array container返回,否则直接返回bitmap container
  • 若基数和不大于4096, 则遍历两个数组,通过数组进行相关的Union计算

对于Intersection操作, 输入为两个array container

  • 通过数组操作进行相关的Insection计算

参考文献

  1. Consistently faster and smaller compressed bitmaps with Roaring
  2. Better bitmap performance with Roaring bitmaps# 、
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值