大量数据检索问题
10亿数据存储检索
数据检索问题:
给定一个长度为10 的int数组,例如:{1,2,3,4,5,6,7,8,9,10} 查找数字“9” 是否存在给定数组中。对于这样的问题直接进行数组遍历或者二分查找很容易就找到对应数字是否存在,但这是数据量很少的情况。计算一下数组占用存储空间:10个int数字4字节=40字节。
同样的问题数组长度换成10亿int数组,占用空间10亿4字节=40亿字节=4000000000/1024/1024/1024=3.73GB,这样的空间占用内存无法支持只能存盘进行分批读取,不可行
bitmap进行大量数据存储检索
bitmap:使用位来存储true/false 来表示这个位上是否存在数字
这样就表示数组:{1,3,7} 此时空间占用只是8bit,可以使用1bit表示1个数字,相对于int类型数据使用4byte=4*8bit=32bit 表示1个int类型数据来说,空间减少32倍。
可得:10亿数据=4000000000/1024/1024/32=119MB,可以使用一个10亿个bit的数组进行存储检索,切检索复杂度为常数。占用空间减少32倍
非连续数据空间占用问题
上面讲的10亿数据可以理解为全部非bit位都被占用,无空间浪费。但是bit数组是根据存储数据的最大值进行空间划分,存在如下情况:数组{1,2,999999999} 只存在三个数字,如果直接使用int数组存储空间占用:4*3=12字节,但是如果使用bitmap存储,根据最大数值划分空间,依然占用119MB。这种情况就造成空间极大浪费。
针对这种情况:
1、业务上根据情况决定使用那种存储方式
2、技术上还可以针对这种空间浪费情况进行优化,压缩存储。使用压缩位图(RoaringBitmap)的方式。
RoaringBitmap
简介
RBM 其实就是相对于bitmap的优化版本,能提供更优秀的压缩性能和更快的查询效率。
基于其优秀的压缩方式和查询效率,已经有很多开源项目在使用
- Apache Lucene
- Apache Druid
- Apache Spark
- Apache CarbonData
- LinkedIn Pinot
- Apache Kylin
并且提供了多种语言的实现方式
Java 使用示例
// 添加依赖
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.9.10</version>
</dependency>
public static void main(String[] args) {
RoaringBitmap roaringBitmap = RoaringBitmap.bitmapOf(1,2,3,1000);
RoaringBitmap roaringBitmap1 = new RoaringBitmap();
roaringBitmap1.add(4000L,4005L);
System.out.println("third num:"+roaringBitmap.select(3));
System.out.println("num 2 sort key:"+roaringBitmap.rank(2));
System.out.println("is contains 1000 num:"+roaringBitmap.contains(1000));
System.out.println("is contains 1001 num:"+roaringBitmap.contains(1001));
//对比占用空间大小
RoaringBitmap roaringBitmap2 = new RoaringBitmap();
byte[] bits = new byte[1000000];
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 1000000; i++) {
roaringBitmap2.add(i);
bits[i] = (byte) i;
list.add(i);
}
System.out.println((byte)100);
System.out.println("roaringbitmap size:"+ ObjectSizeCalculator.getObjectSize(roaringBitmap2));
System.out.println("byte size:"+ObjectSizeCalculator.getObjectSize(bits));
System.out.println("list size:"+ObjectSizeCalculator.getObjectSize(list));
}
结果:
一百万数据不同方式占用存储空间
RoaringBitmap 原理
为什么存储空间能相对节约这么多?主要看数据存储结构
每个RoaringBitmap 包含一个RoaringArray,叫做highLowContainer 包含了所有数据
hightLowContainer 包含三个参数
keys、values、size:
每个32位的整形,高16位会被作为key存储到char[] keys中,低16位则被看做value,存储到Container[] values中的某个Container中。keys和values通过下标一一对应。size则标示了当前包含的key-value pair的数量,即keys和values中有效数据的数量。
针对这样的结构,数据插入逻辑如下:
抽象展示:
1、821697800 对应的 16 进制数为 30FA1D08, 其中高 16 位为 30FA, 低16位为 1D08。
2、我们先用二分查找从一级索引(即 Container Array)中找到数值为 30FA 的容器(如果该容器不存在,则新建一个),从图中我们可以看到,该容器是一个 Bitmap 容器。
3、找到了相应的容器后,看一下低 16 位的数值 1D08,它相当于是 7432,因此在 Bitmap 中找到相应的位置,将其置为 1 即可。
三种存储container
ArrayContainer
Array Container 是 Roaring Bitmap 初始化默认的 Container。Array Container 适合存放稀疏的数据,其内部数据结构是一个有序的 Short 数组。数组初始容量为 4,数组最大容量为 4096,所以 Array Container 是动态变化的,当容量不够时,需要扩容,并且当超过最大容量 4096 时,就会转换为 Bitmap Container。由于数组是有序的,存储和查询时都可以通过二分查找快速定位其在数组中的位置。
bitmapContainer
第二种 Container 是 Bitmap Container。它的数据结构是一个 Long 数组,数组容量恒定为 1024,和上文的 Array Container 不同,Array Container 是一个动态扩容的数组。Bitmap Container 不用像 Array Container 那样需要二分查找定位位置,而是可以直接通过下标直接寻址。
由于每个 Bitmap Container 需要处理低 16 位数据,也就是需要使用 Bitmap 来存储需要 8192 B(2^16/8), 而一个 Long 值占 8 个 B,所以数组大小为 1024。因此一个 Bitmap Container 固定占用内存 8 KB。
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,