概述

所谓的BitMap算法就是位图算法,简单说就是用一个bit位来标记某个元素所对应的value,而key即是该元素,由于BitMap使用了bit位来存储数据,因此可以大大节省存储空间,这是很常用的数据结构,比如用于Bloom Filter中、用于无重复整数的排序等等。bitmap通常基于数组来实现,数组中每个元素可以看成是一系列二进制数,所有元素组成更大的二进制集合。

基本思想

我用一个简单的例子来详细介绍BitMap算法的原理。假设我们要对0-7内的5个元素(4,7,2,5,3)进行排序(这里假设元素没有重复)。我们可以使用BitMap算法达到排序目的。要表示8个数,我们需要8个byte,

  1.首先我们开辟一个字节(8byte)的空间,将这些空间的所有的byte位都设置为0

  2.然后便利这5个元素,第一个元素是4,因为下边从0开始,因此我们把第五个字节的值设置为1

  3.然后再处理剩下的四个元素,最终8个字节的状态如下图

4.现在我们遍历一次bytes区域,把值为1的byte的位置输出(2,3,4,5,7),这样便达到了排序的目的

从上面的例子我们可以看出,BitMap算法的思想还是比较简单的,关键的问题是如何确定10进制的数到2进制的映射图。

MAP映射:

假设需要排序或则查找的数的总数N=100000000,BitMap中1bit代表一个数字,1个int = 4Bytes = 4*8bit = 32 bit,那么N个数需要N/32 int空间。所以我们需要申请内存空间的大小为int a[1 + N/32],其中:a[0]在内存中占32为可以对应十进制数0-31,依次类推:

  a[0]-----------------------------> 0-31

  a[1]------------------------------> 32-63

  a[2]-------------------------------> 64-95

  a[3]--------------------------------> 96-127

那么十进制数如何转换为对应的bit位,下面介绍用位移将十进制数转换为对应的bit位:

  1.求十进制数在对应数组a中的下标

  十进制数0-31,对应在数组a[0]中,32-63对应在数组a[1]中,64-95对应在数组a[2]中………,使用数学归纳分析得出结论:对于一个十进制数n,其在数组a中的下标为:a[n/32]

  2.求出十进制数在对应数a[i]中的下标

  例如十进制数1在a[0]的下标为1,十进制数31在a[0]中下标为31,十进制数32在a[1]中下标为0。 在十进制0-31就对应0-31,而32-63则对应也是0-31,即给定一个数n可以通过模32求得在对应数组a[i]中的下标。

  3.位移

  对于一个十进制数n,对应在数组a[n/32][n%32]中,但数组a毕竟不是一个二维数组,我们通过移位操作实现置1

  a[n/32] |= 1 << n % 32

  移位操作:

  a[n>>5] |= 1 << (n & 0x1F)

  n & 0x1F 保留n的后五位 相当于 n % 32 求十进制数在数组a[i]中的下标。

BitMap简单使用示例

用户标签使用BitMap的数据结构来存储,比如表示用户对应的标签表如下所示:

如果使用标签,也就是一个标签对应多个用户,如下所示,比较简单一看就会:

让每一个标签存储包含此标签的所有用户 ID,每一个标签都是一个独立的 Bitmap。这样,实现用户的去重和查询统计,就变得一目了然:

对上面例子的使用:

在用户群做交集和并集运算的时候,例如:

1,如何查找使用苹果手机的程序员用户?

2.如何查找所有男性或者00后的用户?

3,同样是刚才的例子,我们给定 90 后用户的 Bitmap,再给定一个全量用户的 Bitmap。最终要求出的是存在于全量用户,但又不存在于 90 后用户的部分。

如何求出呢?我们可以使用异或操作,即相同位为 0,不同位为 1。 (1^1)

BitMap的代码实现

java实现:

 1 /**
2 * ClassName BitMap4.java
3 * author Rhett.wang
4 * version 1.0.0
5 * Description TODO
6 * createTime 2020年01月24日 07:53:00
7 */
8 public class BitMap4 {
9 //保存数据的
10 private byte[] bits;
11
12 //能够存储多少数据
13 private int capacity;
14
15
16 public BitMap4(int capacity){
17 this.capacity = capacity;
18
19 //1bit能存储8个数据,那么capacity数据需要多少个bit呢,capacity/8+1,右移3位相当于除以8
20 bits = new byte[(capacity >>3 )+1];
21 }
22
23 public void add(int num){
24 // num/8得到byte[]的index
25 int arrayIndex = num >> 3;
26
27 // num%8得到在byte[index]的位置
28 int position = num & 0x07;
29
30 //将1左移position后,那个位置自然就是1,然后和以前的数据做|,这样,那个位置就替换成1了。
31 bits[arrayIndex] |= 1 << position;
32 }
33
34 public boolean contain(int num){
35 // num/8得到byte[]的index
36 int arrayIndex = num >> 3;
37
38 // num%8得到在byte[index]的位置
39 int position = num & 0x07;
40
41 //将1左移position后,那个位置自然就是1,然后和以前的数据做&,判断是否为0即可
42 return (bits[arrayIndex] & (1 << position)) !=0;
43 }
44
45 public void clear(int num){
46 // num/8得到byte[]的index
47 int arrayIndex = num >> 3;
48
49 // num%8得到在byte[index]的位置
50 int position = num & 0x07;
51
52 //将1左移position后,那个位置自然就是1,然后对取反,再与当前值做&,即可清除当前的位置了.
53 bits[arrayIndex] &= ~(1 << position);
54
55 }
56
57 public static void main(String[] args) {
58 BitMap4 bitmap = new BitMap4(100);
59 bitmap.add(7);
60 System.out.println("插入7成功");
61
62 boolean isexsit = bitmap.contain(7);
63 System.out.println("7是否存在:"+isexsit);
64
65 bitmap.clear(7);
66 isexsit = bitmap.contain(7);
67 System.out.println("7是否存在:"+isexsit);
68 }
69 }

PYTHON代码实现:

 1 class BitMap():
2 def __init__(self,max):
3 self.size=int((max +31 -1)/31)
4 self.array=[0 for i in range(self.size)]
5
6 def bitindex(self,num):
7 return num%31
8
9 def set_1(self,num):
10 elemindex=(num//31)
11 byteindex=self.bitindex(num)
12 ele=self.array[elemindex]
13 self.array[elemindex] = ele| (1<< byteindex)
14
15
16 def test_1(self,i):
17 elemindex=(i//31)
18 bytearray= self.bitindex(i)
19 if self.array[elemindex] & (1 << bytearray):
20 return True
21 return False
22
23 if __name__ =="__main__":
24 Max = ord('z')
25 shuffle_array=[x for x in 'qwelajkda']
26 ret =[]
27 bitmap =BitMap(Max)
28 for c in shuffle_array:
29 bitmap.set_1(ord(c))
30
31 for i in range(Max+1):
32 if bitmap.test_1(i):
33 ret.append(chr(i))
34
35 print(u'原始数组是:%s' % shuffle_array)
36 print(u'排序以后的数组是:%s' % ret)

scala代码实现

 1 /**
2 * ClassName BitMap.java
3 * author Rhett.wang
4 * version 1.0.0
5 * Description TODO
6 * createTime 2020年01月24日 10:30:00
7 */
8 class BitMap(bitmap:Array[Byte], length:Int) {
9
10 }
11 object BitMap {
12 var bitmap:Array[Int]=Array()
13 def main(args: Array[String]) {
14
15 var bitmaps=TestBit(100)
16 setBit(32)
17 println(getBit(32))
18 println(getBit(11))
19 }
20 def TestBit(length:Int):Unit={
21 bitmap= new Array[Int]((length >> 5).toInt + (if ((length & 31) > 0) 1
22 else 0))
23 }
24
25 def getBit(index: Long): Int = {
26 var intData: Int = bitmap(((index - 1) >> 5).toInt)
27 var offset: Int = ((index - 1) & 31).toInt
28 return intData >> offset & 0x01
29 }
30
31 def setBit(index: Long) {
32 var belowIndex: Int = ((index - 1) >> 5).toInt
33 var offset: Int = ((index - 1) & 31).toInt
34 var inData: Int = bitmap(belowIndex)
35 bitmap(belowIndex) = inData | (0x01 << offset)
36 }
37
38 /* def clear(num:Int):Unit={
39 var arrayIndex: Int = num >> 5
40 var position: Int = num & 0x1F
41 bitmap(arrayIndex) =(bitmap(arrayIndex) & ~(1 << position)).toByte
42 }*/
43
44
45 }

Roaring BitMap的原理和使用

位图索引被广泛用于数据库和搜索引擎中,通过利用位级并行,它们可以显著加快查询速度。但是,位图索引会占用大量的内存,因此我们会更喜欢压缩位图索引。 Roaring Bitmaps 就是一种十分优秀的压缩位图索引,后文统称 RBM。压缩位图索引有很多种,比如基于 RLE(Run-Length Encoding,运行长度编码)的WAH (Word Aligned Hybrid Compression Scheme) 和 Concise (Compressed ‘n’ Composable Integer Set)。相比较前者, RBM 能提供更优秀的压缩性能和更快的查询效率。

RBM 的用途和 Bitmap 很差不多(比如说索引),只是说从性能、空间利用率各方面更优秀了。目前 RBM 已经在很多成熟的开源大数据平台中使用,简单列几个作为参考。

1 Apache Lucene and derivative systems such as Solr and Elasticsearch,
2 Metamarkets’ Druid,
3 Apache Spark,
4 Apache Hive,
5 eBay’s Apache Kylin,
6 ……

RBM 的主要思想并不复杂,简单来讲,有如下三条:

1,我们将 32-bit 的范围 ([0, n)) 划分为 2^16 个桶,每一个桶有一个 Container 来存放一个数值的低16位;

2,在存储和查询数值的时候,我们将一个数值 k 划分为高 16 位(k % 2^16)和低 16 位(k mod 2^16),取高 16 位找到对应的桶,然后在低 16 位存放在相应的 Container 中;

3,容器的话, RBM 使用两种容器结构: Array Container 和 Bitmap Container。Array Container 存放稀疏的数据,Bitmap Container 存放稠密的数据。即,若一个 Container 里面的 Integer 数量小于 4096,就用 Short 类型的有序数组来存储值。若大于 4096,就用 Bitmap 来存储值。

如下图,就是官网给出的一个例子,三个容器分别代表了三个数据集:

the list of the first 1000 multiples of 62

all integers [216, 216 + 100)

all even numbers in [2216, 3216)

举例说明:

看完前面的还不知道在说什么?没关系,举个栗子说明就好了。现在我们要将 821697800 这个 32 bit 的整数插入 RBM 中,整个算法流程是这样的:

821697800 对应的 16 进制数为 30FA1D08, 其中高 16 位为 30FA, 低16位为 1D08。

我们先用二分查找从一级索引(即 Container Array)中找到数值为 30FA 的容器(如果该容器不存在,则新建一个),从图中我们可以看到,该容器是一个 Bitmap 容器。

找到了相应的容器后,看一下低 16 位的数值 1D08,它相当于是 7432,因此在 Bitmap 中找到相应的位置,将其置为 1 即可。

下面介绍到的是RoaringBitmap的核心,三种Container。

通过上面的介绍我们知道,每个32位整形的高16位已经作为key存储在RoaringArray中了,那么Container只需要处理低16位的数据。

ArrayContainer

结构很简单,只有一个short[] content,将16位value直接存储。

short[] content始终保持有序,方便使用二分查找,且不会存储重复数值。

因为这种Container存储数据没有任何压缩,因此只适合存储少量数据。

ArrayContainer占用的空间大小与存储的数据量为线性关系,每个short为2字节,因此存储了N个数据的ArrayContainer占用空间大致为2N字节。存储一个数据占用2字节,存储4096个数据占用8kb。

根据源码可以看出,常量DEFAULT_MAX_SIZE值为4096,当容量超过这个值的时候会将当前Container替换为BitmapContainer。

BitmapContainer

这种Container使用long[]存储位图数据。我们知道,每个Container处理16位整形的数据,也就是0~65535,因此根据位 图的原理,需要65536个比特来存储数据,每个比特位用1来表示有,0来表示无。每个long有64位,因此需要1024个long来提供65536个 比特。

因此,每个BitmapContainer在构建时就会初始化长度为1024的long[]。这就意味着,不管一个BitmapContainer中只存储了1个数据还是存储了65536个数据,占用的空间都是同样的8kb。

解释一下为什么这里用的 4096 这个阈值?因为一个 Integer 的低 16 位是 2Byte,因此对应到 Arrary Container 中的话就是 2Byte * 4096 = 8KB;同样,对于 Bitmap Container 来讲,2^16 个 bit 也相当于是 8KB。

RunContainer

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中存储的就是压缩后的数据。

这种压缩算法的性能和数据的连续性(紧凑性)关系极为密切,对于连续的100个short,它能从200字节压缩为4字节,但对于完全不连续的100个short,编码完之后反而会从200字节变为400字节。

如果要分析RunContainer的容量,我们可以做下面两种极端的假设:

最好情况,即只存在一个数据或只存在一串连续数字,那么只会存储2个short,占用4字节

最坏情况,0~65535的范围内填充所有的奇数位(或所有偶数位),需要存储65536个short,128kb

代码测试示例:

 1 import org.roaringbitmap.RoaringBitmap;
2
3 import java.util.function.Consumer;
4
5 /**
6 * ClassName RBitMap.java
7 * author Rhett.wang
8 * version 1.0.0
9 * Description TODO
10 * createTime 2020年01月25日 21:09:00
11 */
12 public class RBitMap {
13 public static void main(String[] args) {
14 test1();
15 }
16 private static void test1(){
17 //向rr中添加1、2、3、1000四个数字
18 RoaringBitmap rr = RoaringBitmap.bitmapOf(1,2,3,1000);
19 //创建RoaringBitmap rr2
20 RoaringBitmap rr2 = new RoaringBitmap();
21 //向rr2中添加10000-12000共2000个数字
22 rr2.add(10000L,12000L);
23 //返回第3个数字是1000,第0个数字是1,第1个数字是2,则第3个数字是1000
24 rr.select(3);
25 //返回value = 2 时的索引为 1。value = 1 时,索引是 0 ,value=3的索引为2
26 rr.rank(2);
27 //判断是否包含1000
28 rr.contains(1000); // will return true
29 //判断是否包含7
30 rr.contains(7); // will return false
31
32 //两个RoaringBitmap进行or操作,数值进行合并,合并后产生新的RoaringBitmap叫rror
33 RoaringBitmap rror = RoaringBitmap.or(rr, rr2);
34 //rr与rr2进行位运算,并将值赋值给rr
35 rr.or(rr2);
36 //判断rror与rr是否相等,显然是相等的
37 boolean equals = rror.equals(rr);
38 if(!equals) throw new RuntimeException("bug");
39 // 查看rr中存储了多少个值,1,2,3,1000和10000-12000,共2004个数字
40 long cardinality = rr.getLongCardinality();
41 System.out.println(cardinality);
42 //遍历rr中的value
43 for(int i : rr) {
44 System.out.println(i);
45 }
46 //这种方式的遍历比上面的方式更快
47 rr.forEach((Consumer<? super Integer>) i -> {
48 System.out.println(i.intValue());
49 });
50 }
51 }

在RoaringBitmap中,32位整数被分成了2^16个块。任何一个32位整数的前16位决定放在哪个块里。后16位就是放在这个块里的内容。比如0xFFFF0000和0xFFFF0001,前16位都是FFFF,表明这两个数应该放在一个块里。后16位分别是0和1。在这个块中指保存0和1就可以了,不需要保存完整的整数。

在最开始的时候,一个块中包含一个长度为4的short数组,后16位所对应的值就存在这个short数组里。注意在插入的时候要保持顺序性,这里就需要用到二分查找来加快速度了。如果当块中的元素大于short数组的长度时,就需要重新分配更大的数组,把当前数组copy过去,并把新值插入对应的位置。扩展数组大小和STL中vector的方式类似,不过并不是完全的加倍,而且上限是4096,也就是说最多只保存4096个元素。那么问题来了,超过了4096怎么办呢?

一个块里最多可能需要存放2^16个元素,那么如果是用short来存放,最多需要65536个short,那么就是131072个byte。如果换一种方式,用位来存储元素,那么就需要65536个bit,相当于1024个long型数组,即2048个int,也就是4096个short。

所以,当一个块中元素数量小于等于4096的时候,用有序short数组来保存元素,而当元素数量大于4096的时候,用长度为1024的long数组来按位表示元素是否存在。

当bitmap中有多个块的时候,块的信息是用数组来保存的。这个数组同样需要保持顺序性,也是用二分查找找到一个块的位置。所以,当一个整数过来之后,首先根据前16位计算出块的key,然后在块的数组中二分查找。找到的话,就把后16位保存在这个块中。找不到,就创建一个新块,把后16位保存在块中,再把块插入对应的位置。

大数据分析常用去重算法分析-Kylin

首先,请大家思考一个问题:在大数据处理领域中,什么环节是你最不希望见到的?以我的观点来看,shuffle 是我最不愿意见到的环节,因为一旦出现了非常多的 shuffle,就会占用大量的磁盘和网络 IO,从而导致任务进行得非常缓慢。而今天我们所讨论的去重分析,就是一个会产生非常多 shuffle 的场景,先大概介绍一下shuffle原理:

Shuffle描述着数据从map task输出到reduce task输入的这段过程。在分布式情况下,reduce task需要跨节点去拉取其它节点上的map task结果。这一过程将会产生网络资源消耗和内存,磁盘IO的消耗。

先来看以下场景:

我们有一张商品访问表,表上有 item 和 user_id 两个列,我们希望求商品的 UV,这是去重非常典型的一个场景。我们的数据是存储在分布式平台上的,分别在数据节点 1 和 2 上。

我们从物理执行层面上想一下这句 SQL 背后会发生什么故事:首先分布式计算框架启动任务, 从两个节点上去拿数据, 因为 SQL group by 了 item 列, 所以需要以 item 为 key 对两个表中的原始数据进行一次 shuffle。我们来看看需要 shuffle 哪些数据:因为 select/group by了 item,所以 item 需要 shuffle 。但是,user_id  我们只需要它的一个统计值,能不能不 shuffle 整个 user_id 的原始值呢?

如果只是简单的求 count 的话, 每个数据节点分别求出对应 item 的 user_id 的 count, 然后只要 shuffle 这个 count 就行了,因为count 只是一个数字, 所以 shuffle 的量非常小。但是由于分析的指标是 count distinct,我们不能简单相加两个节点user_id 的 count distinct 值,我们只有得到一个 key 对应的所有 user_id 才能统计出正确的 count distinct值,而这些值原先可能分布在不同的节点上,所以我们只能通过 shuffle 把这些值收集到同一个节点上再做去重。而当 user_id 这一列的数据量非常大的时候,需要 shuffle 的数据量也会非常大。我们其实最后只需要一个 count 值,那么有办法可以不 shuffle 整个列的原始值吗?我下面要介绍的两种算法就提供了这样的一种思路,使用更少的信息位,同样能够求出该列不重复元素的个数(基数)

第一种要介绍的算法是一种精确的去重算法,主要利用了 Bitmap 的原理。Bitmap 也称之为 Bitset,它本质上是定义了一个很大的 bit 数组,每个元素对应到 bit 数组的其中一位。例如有一个集合[2,3,5,8]对应的 Bitmap 数组是[001101001],集合中的 2 对应到数组 index 为 2 的位置,3 对应到 index 为 3 的位置,下同,得到的这样一个数组,我们就称之为 Bitmap。很直观的,数组中 1 的数量就是集合的基数。追本溯源,我们的目的是用更小的存储去表示更多的信息,而在计算机最小的信息单位是 bit,如果能够用一个 bit 来表示集合中的一个元素,比起原始元素,可以节省非常多的存储。

这就是最基础的 Bitmap,我们可以把 Bitmap 想象成一个容器,我们知道一个 Integer 是32位的,如果一个 Bitmap 可以存放最多 Integer.MAX_VALUE 个值,那么这个 Bitmap 最少需要 32 的长度。一个 32 位长度的 Bitmap 占用的空间是512 M (2^32/8/1024/1024),这种 Bitmap 存在着非常明显的问题:这种 Bitmap 中不论只有 1 个元素或者有 40 亿个元素,它都需要占据 512 M 的空间。回到刚才求 UV 的场景,不是每一个商品都会有那么多的访问,一些爆款可能会有上亿的访问,但是一些比较冷门的商品可能只有几个用户浏览,如果都用这种 Bitmap,它们占用的空间都是一样大的,这显然是不可接受的。

对于上节说的问题,有一种设计的非常的精巧 Bitmap,叫做 Roaring Bitmap,能够很好地解决上面说的这个问题。我们还是以存放 Integer 值的 Bitmap 来举例,Roaring Bitmap 把一个 32 位的 Integer 划分为高 16 位和低 16 位,取高 16 位找到该条数据所对应的 key,每个 key 都有自己的一个 Container。我们把剩余的低 16 位放入该 Container 中。依据不同的场景,有 3 种不同的 Container,分别是 Array Container、Bitmap Container 和 Run Container,下文将一一介绍。

首先第一种,是 Roaring Bitmap 初始化时默认的 Container,叫做 Array Container。Array Container 适合存放稀疏的数据,Array Container 内部的数据结构是一个 short array,这个 array 是有序的,方便查找。数组初始容量为 4,数组最大容量为 4096。超过最大容量 4096 时,会转换为 Bitmap Container。这边举例来说明数据放入一个 Array Container 的过程:有 0xFFFF0000 和 0xFFFF0001 两个数需要放到 Bitmap 中, 它们的前 16 位都是 FFFF,所以他们是同一个 key,它们的后 16 位存放在同一个 Container 中; 它们的后 16 位分别是 0 和 1, 在 Array Container 的数组中分别保存 0 和 1 就可以了,相较于原始的 Bitmap 需要占用 512M 内存来存储这两个数,这种存放实际只占用了 2+4=6 个字节(key 占 2 Bytes,两个 value 占 4 Bytes,不考虑数组的初始容量)。

第二种 Container 是 Bitmap Container,其原理就是上文说的 Bitmap。它的数据结构是一个 long 的数组,数组容量固定为 1024,和上文的 Array Container 不同,Array Container 是一个动态扩容的数组。这边推导下 1024 这个值:由于每个 Container 还需处理剩余的后 16 位数据,使用 Bitmap 来存储需要 8192 Bytes(2^16/8), 而一个 long 值占 8 个 Bytes,所以一共需要 1024(8192/8)个 long 值。所以一个 Bitmap container 固定占用内存 8 KB(1024 * 8 Byte)。当 Array Container 中元素到 4096 个时,也恰好占用 8 k(4096*2Bytes)的空间,正好等于 Bitmap 所占用的 8 KB。而当你存放的元素个数超过 4096 的时候,Array Container 的大小占用还是会线性的增长,但是 Bitmap Container 的内存空间并不会增长,始终还是占用 8 K,所以当 Array Container 超过最大容量(DEFAULT_MAX_SIZE)会转换为 Bitmap Container。

我们自己在 Kylin 中实践使用 Roaring Bitmap 时,我们发现 Array Container 随着数据量的增加会不停地 resize 自己的数组,而 Java 数组的 resize 其实非常消耗性能,因为它会不停地申请新的内存,同时老的内存在复制完成前也不会释放,导致内存占用变高,所以我们建议把 DEFAULT_MAX_SIZE 调得低一点,调成 1024 或者 2048,减少 Array Container 后期 reszie 数组的次数和开销。

最后一种 Container 叫做Run Container,这种 Container 适用于存放连续的数据。比如说 1 到 100,一共 100 个数,这种类型的数据称为连续的数据。这边的Run指的是Run Length Encoding(RLE),它对连续数据有比较好的压缩效果。原理是对于连续出现的数字, 只记录初始数字和后续数量。例如: 对于 [11, 12, 13, 14, 15, 21, 22],会被记录为 11, 4, 21, 1。很显然,该 Container 的存储占用与数据的分布紧密相关。最好情况是如果数据是连续分布的,就算是存放 65536 个元素,也只会占用 2 个 short。而最坏的情况就是当数据全部不连续的时候,会占用 128 KB 内存。

总结:用一张图来总结3种 Container 所占的存储空间,可以看到元素个数达到 4096 之前,选用 Array Container 的收益是最好的,当元素个数超过了 4096 时,Array Container 所占用的空间还是线性的增长,而 Bitmap Container 的存储占用则与数据量无关,这个时候 Bitmap Container 的收益就会更好。而 Run Container 占用的存储大小完全看数据的连续性, 因此只能画出一个上下限范围 [4 Bytes, 128 KB]。

我们再来看一下Bitmap 在 Kylin 中的应用,Kylin 中编辑 measure 的时候,可以选择 Count Distinct,且Return Type 选为 Precisely,点保存就可以了。但是事情没有那么简单,刚才上文在讲 Bitmap 时,一直都有一个前提,放入的值都是数值类型,但是如果不是数值类型的值,它们不能够直接放入 Bitmap,这时需要构建一个全区字典,做一个值到数值的映射,然后再放入 Bitmap 中。

在 Kylin 中构建全局字典,当列的基数非常高的时候,全局字典会成为一个性能的瓶颈。针对这种情况,社区也一直在努力做优化,这边简单介绍几种优化的策略,更详细的优化策略可以见文末的参考链接。

1)当一个列的值完全被另外一个列包含,而另一个列有全局字典,可以复用另一个列的全局字典

2)当精确去重指标不需要跨 Segment 聚合的时候,可以使用这个列的 Segment 字典代替(这个列需要字典编码)。在 Kylin 中,Segment 就相当于时间分片的概念。当不会发生跨 Segments 的分析时,这个列的 Segment 字典就可以代替这个全局字典。

3)如果你的 cube 包含很多的精确去重指标,可以考虑将这些指标放到不同的列族上。不止是精确去重,像一些复杂 measure,我们都建议使用多个列族去存储,可以提升查询的性能。

快手BitBase数据库

如上图所示,首先将原始数据的一列的某个值抽象成 bitmap(比特数组),举例:city=bj,city 是维度,bj (北京) 是维度值,抽象成 bitmap 值就是10100,表示第0个用户在 bj,第1个用户不在北京,依次类推。然后将多维度之间的组合转换为 bitmap 计算:bitmap 之间做与、或、非、异或,举例:比如在北京的用户,且兴趣是篮球,这样的用户有多少个,就转换为图中所示的两个 bitmap 做与运算,得到橙色的 bitmap,最后,再对 bitmap 做 count 运算。count 表示统计“1”的个数,list 是列举“1”所在的数组 index,业务上表示 userId。

SparkSQL自定义聚合函数(UDAF)实现bitmap函数

创建表:使用phoenix在HBase中创建测试表,字段使用VARBINARY类型

1 CREATE TABLE IF NOT EXISTS test_binary (
2 date VARCHAR NOT NULL,
3 dist_mem VARBINARY
4 CONSTRAINT test_binary_pk PRIMARY KEY (date)
5 ) SALT_BUCKETS=6;

创建完成后使用RoaringBitmap序列化数据存入数据库:

自定义代码:

  1 import org.apache.spark.sql.Row;
2 import org.apache.spark.sql.expressions.MutableAggregationBuffer;
3 import org.apache.spark.sql.expressions.UserDefinedAggregateFunction;
4 import org.apache.spark.sql.types.DataType;
5 import org.apache.spark.sql.types.DataTypes;
6 import org.apache.spark.sql.types.StructField;
7 import org.apache.spark.sql.types.StructType;
8 import org.roaringbitmap.RoaringBitmap;
9
10 import java.io.*;
11 import java.util.ArrayList;
12 import java.util.List;
13
14 /**
15 * 实现自定义聚合函数Bitmap
16 */
17 public class UdafBitMap extends UserDefinedAggregateFunction {
18 @Override
19 public StructType inputSchema() {
20 List<StructField> structFields = new ArrayList<>();
21 structFields.add(DataTypes.createStructField("field", DataTypes.BinaryType, true));
22 return DataTypes.createStructType(structFields);
23 }
24
25 @Override
26 public StructType bufferSchema() {
27 List<StructField> structFields = new ArrayList<>();
28 structFields.add(DataTypes.createStructField("field", DataTypes.BinaryType, true));
29 return DataTypes.createStructType(structFields);
30 }
31
32 @Override
33 public DataType dataType() {
34 return DataTypes.LongType;
35 }
36
37 @Override
38 public boolean deterministic() {
39 //是否强制每次执行的结果相同
40 return false;
41 }
42
43 @Override
44 public void initialize(MutableAggregationBuffer buffer) {
45 //初始化
46 buffer.update(0, null);
47 }
48
49 @Override
50 public void update(MutableAggregationBuffer buffer, Row input) {
51 // 相同的executor间的数据合并
52 // 1. 输入为空直接返回不更新
53 Object in = input.get(0);
54 if(in == null){
55 return ;
56 }
57 // 2. 源为空则直接更新值为输入
58 byte[] inBytes = (byte[]) in;
59 Object out = buffer.get(0);
60 if(out == null){
61 buffer.update(0, inBytes);
62 return ;
63 }
64 // 3. 源和输入都不为空使用bitmap去重合并
65 byte[] outBytes = (byte[]) out;
66 byte[] result = outBytes;
67 RoaringBitmap outRR = new RoaringBitmap();
68 RoaringBitmap inRR = new RoaringBitmap();
69 try {
70 outRR.deserialize(new DataInputStream(new ByteArrayInputStream(outBytes)));
71 inRR.deserialize(new DataInputStream(new ByteArrayInputStream(inBytes)));
72 outRR.or(inRR);
73 ByteArrayOutputStream bos = new ByteArrayOutputStream();
74 outRR.serialize(new DataOutputStream(bos));
75 result = bos.toByteArray();
76 } catch (IOException e) {
77 e.printStackTrace();
78 }
79 buffer.update(0, result);
80 }
81
82 @Override
83 public void merge(MutableAggregationBuffer buffer1, Row buffer2) {
84 //不同excutor间的数据合并
85 update(buffer1, buffer2);
86 }
87
88 @Override
89 public Object evaluate(Row buffer) {
90 //根据Buffer计算结果
91 long r = 0l;
92 Object val = buffer.get(0);
93 if (val != null) {
94 RoaringBitmap rr = new RoaringBitmap();
95 try {
96 rr.deserialize(new DataInputStream(new ByteArrayInputStream((byte[]) val)));
97 r = rr.getLongCardinality();
98 } catch (IOException e) {
99 e.printStackTrace();
100 }
101 }
102 return r;
103 }
104 }

调用例子:

 1  /**
2 * 使用自定义函数解析bitmap
3 *
4 * @param sparkSession
5 * @return
6 */
7 private static void udafBitmap(SparkSession sparkSession) {
8 try {
9 Properties prop = PropUtil.loadProp(DB_PHOENIX_CONF_FILE);
10 // JDBC连接属性
11 Properties connProp = new Properties();
12 connProp.put("driver", prop.getProperty(DB_PHOENIX_DRIVER));
13 connProp.put("user", prop.getProperty(DB_PHOENIX_USER));
14 connProp.put("password", prop.getProperty(DB_PHOENIX_PASS));
15 connProp.put("fetchsize", prop.getProperty(DB_PHOENIX_FETCHSIZE));
16 // 注册自定义聚合函数
17 sparkSession.udf().register("bitmap",new UdafBitMap());
18 sparkSession
19 .read()
20 .jdbc(prop.getProperty(DB_PHOENIX_URL), "test_binary", connProp)
21 // sql中必须使用global_temp.表名,否则找不到
22 .createOrReplaceGlobalTempView("test_binary");
23 //sparkSession.sql("select YEAR(TO_DATE(date)) year,bitmap(dist_mem) memNum from global_temp.test_binary group by YEAR(TO_DATE(date))").show();
24 sparkSession.sql("select date,bitmap(dist_mem) memNum from global_temp.test_binary group by date").show();
25 } catch (Exception e) {
26 e.printStackTrace();
27 }
28 }  

总结

感谢网络大神的分享:技术共勉

https://kyligence.io/zh/blog/count-distinct-bitmap/

https://www.jianshu.com/p/ded6e8ecd0d1

https://blog.csdn.net/xiongbingcool/article/details/81282118

http://www.sohu.com/a/327627247_315839

https://blog.csdn.net/xywtalk/article/details/52590275

https://blog.csdn.net/qq_22520215/article/details/77893326

BitMap算法知识笔记以及在大数据方向的使用的更多相关文章

  1. http://edu.manew.com/ ,蛮牛教育(很少免费),主要是unty3D和大数据方向。适合扫盲

    http://edu.manew.com/ ,蛮牛教育(很少免费),主要是unty3D和大数据方向.

  2. 大数据笔记01:大数据之Hadoop简介

    1. 背景 随着大数据时代来临,人们发现数据越来越多.但是如何对大数据进行存储与分析呢?   单机PC存储和分析数据存在很多瓶颈,包括存储容量.读写速率.计算效率等等,这些单机PC无法满足要求. 2. ...

  3. Oracle大数据解决方案》学习笔记5——Oracle大数据机的配置、部署架构和监控-1(BDA Config, Deployment Arch, and Monitoring)

    原创预见未来to50 发布于2018-12-05 16:18:48 阅读数 146  收藏 展开 这章的内容很多,有的学了. 1. Oracle大数据机——灵活和可扩展的架构 2. Hadoop集群的 ...

  4. 分析 BAT 互联网巨头在大数据方向布局及大数据未来发展趋势

    > 风起云涌的大数据战场上,早已迎百花齐放繁荣盛景,各大企业加速跑向"大数据时代".而我们作为大数据的践行者,在这个"多智时代"如何才能跟上大数据的潮流, ...

  5. 大数据笔记07:大数据之Hadoop的HDFS(特点)

    1. HDFS的特点: (1)数据冗余,硬件容错 (2)流式的数据访问(写一次读多次,不能直接修改已写入的数据,只能删除之后再去写入) (3)存储大文件 2. HDFS适用性和局限性 适用性:(1)适 ...

  6. 大数据笔记10:大数据之Hadoop的MapReduce的原理

    1. MapReduce(并行处理的框架) 思想:分而治之,一个大任务分解成多个小的子任务(map),并行执行后,合并结果(Reduce) (1)大任务分解成多个小任务,这个过程就是map: (2)多 ...

  7. 大数据笔记06:大数据之Hadoop的HDFS(文件的读写操作)

    1. 首先我们看一看文件读取: (1)客户端(java程序.命令行等等)向NameNode发送文件读取请求,请求中包含文件名和文件路径,让NameNode查询元数据. (2)接着,NameNode返回 ...

  8. 大数据笔记05:大数据之Hadoop的HDFS(数据管理策略)

            HDFS中数据管理与容错 1.数据块的放置       每个数据块3个副本,就像上面的数据库A一样,这是因为数据在传输过程中任何一个节点都有可能出现故障(没有办法,廉价机器就是这样的) ...

  9. 大数据笔记04:大数据之Hadoop的HDFS(基本概念)

    1.HDFS是什么? Hadoop分布式文件系统(HDFS),被设计成适合运行在通用硬件(commodity hardware)上的分布式文件系统.它和现有的分布式文件系统有很多共同点. 2.HDFS ...

随机推荐

  1. DDS协议解读及测试开发实践

    DDS概述 DDS是OMG在2004年发布的中间件协议和应用程序接口(API)标准,它为分布式系统提供了低延迟.高可靠性.可扩展的通信架构标准.DDS目前在工业.医疗.交通.能源.国防领域都有广泛的应 ...

  2. HSPICE 电平触发D触发器仿真

    一. HSPICE的基本操作过程 打开HSPICE程序,通过OPEN打开编写好的网表文件. 按下SIMULATE进行网表文件的仿真. 按下AVANWAVES查看波形图(仿真结果). 二. 网表文件结构 ...

  3. CF253A Boys and Girls 题解

    Content 有 \(n\) 个男生.\(m\) 个女生坐在一排,请求出这样一种方案,使得相邻两个座位之间的人的性别不同的次数最多. 数据范围:\(1\leqslant n,m\leqslant 1 ...

  4. java 图形化小工具Abstract Window Toolit 常用组件:对话框Dialog FileDialog

    对话框 Dialog是Window类的子类,是1个容器类,属于特殊组件,对话框是可以独立存在的顶级窗口,因此用法与普通窗口的用法几乎完全一样.但对话框有如下两点需要注意. (1),对话框通常依赖于其他 ...

  5. 客户通过新型网络访问Web服务

  6. Linux(centos)使用docker安装pdf2htmlEX

    pdf2htmlEX是一款可以将pdf文档转换成html文件的插件,但是Linux系统安装起来很麻烦,所以我们使用docker进行安装 首先要安装docker 因为国外镜像很慢,所以我们这边修改使用国 ...

  7. js中的jQuery Validate增加手机号码验证

    $.validator.addMethod("isPhone", function(value,element) { var length = value.length; var ...

  8. 【九度OJ】题目1018:统计同成绩学生人数 解题报告

    [九度OJ]题目1018:统计同成绩学生人数 解题报告 标签(空格分隔): 九度OJ [LeetCode] http://ac.jobdu.com/problem.php?pid=1018 题目描述: ...

  9. 【LeetCode】949. Largest Time for Given Digits 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  10. B. Petya and Exam

    B. Petya and Exam 题目链接 题意 给你一串字符,在这个串中所有出现的字符都是\(good\)字符,未出现的都是\(bad\)字符, 然后给你另一串字符,这个字符串中有两个特殊的字符, ...