RoaringBitMap处理海量数据内存diff

一、背景

假设mysql库中有一张近千万的客户信息表(未分表),其中有客户性别,等级(10个等级),参与某某活动等字段

1、如果要通过等级+性别+其他条件(离散度也低)筛选出客户,如何处理查询?

2、参与活动是记录活动ID,客户每参与一个活动,就要记录客户和该活动id的关联,活动记录字段如何存储?

使用mysql存储,对应上方场景,可能会面临以下问题

1、查询条件关联字段虽然建立了索引,但是由于字段值离散度较低,导致索引失效,直接变成全表扫描

2、活动记录字段,由于要存储多值,只能设计存活动ID的json串或者符号分隔存储,直接导致该字段无法建立有效的索引

可以发现,传统的关系型数据库,面对以上场景,暂时没办法设计出合理的索引,所以会导致无法查询的情况。

为了解决以上问题,一开始方向也是使用mongoDB或者ES去进行数据异构存储,借助NoSql的结构,存储频繁变更的数据,比如,活动id字段可以直接存储为数组,也可以建立索引。

但是,由于时间、资源等关系,进行数据异构存储成本较大,所以只能寻找替代方案。

在存储方式还是使用Mysql的情况下,如何进行数据检索呢,基于上面的场景,很明显直接使用mysql查询已经走不通了,需要使用标签操作数据,在内存中进行数据的交并集操作。

所以问题就转变成了,什么样的数据结构,可以以较低的内存占用,存储海量数据,还支持交差并集等逻辑操作呢?

位图结构,完美适合标签场景,位图是过bit数组来存储数据的数据结构,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

二、技术选型

接上背景,既然选择了位图作为标签场景下的查询方案,那么应该选取哪种Bitmap实现呢?

java原生的BitSet,guava的EWAHCompressedBitmap,第三方的RoaingBitmap

注:由于标签可能每日全量更新,需要考虑内存操作且能够持久化,redis的Bitmap,在一亿数据量情况下,不拆分(取余,hash等),size为12M,推算5千万数据量size为6M,属于大key( >5M ),且大数据量Bitmap初始化,可能造成redis阻塞延迟,故暂时不考虑。

巧用RoaringBitMap处理海量数据内存diff问题 - 知乎

这里借用上方引文的测试结果:

内存占用测试:往Bitmap中添加1、N+1、2N+1.....5000000数据,其中N为数据的步长(稀疏度) 来计算各个Bitmap在不同稀疏度下(N)的内存占用情况。通过下图可以看出,除了在稀疏度为1时,EWAHCompressedBitmap内存占用最低以外,其余稀疏度下的内存占用:RoaingBitmap<EWAHCompressedBitmap<BitSet。

CPU耗时测试:往各个Bitmap中添加1、N+1、2N+1.....5000000数据,其中N为数据的步长(稀疏度),然后与有5000000满数据的Bitmap分别求2000次差集并取2000次中的最大耗时,得到在每个稀疏度下每种Bitmap的耗时情况。通过下图可以看出,各个稀疏度下的cpu耗时:RoaingBitmap≈EWAHCompressedBitmap<BitSet.

基于上方的测试数据,从内存占用,CPU耗时等方面,RoaingBitmap均要优于其他类型的位图,所以,选择RoaingBitmap作为新方案的Bitmap实现。

持久化存储:Bitmap、BitSet、RoaringBitmap持久化存储_bitset持久化-CSDN博客

api文档:RoaringBitmap 1.0.1 javadoc (org.roaringbitmap)

三、RoaingBitmap简单介绍及原理

如果你只是简单使用,那么记住:RoaringBitmap是高效压缩位图。其实就够了。

RoaringBitmap的核心思想是将整数集合按照高位划分成不同的容器,每个容器内部再使用不同的存储策略。具体而言,它将一个32位的整数分成两部分:

1、高16位作为容器的索引(container index)。

2、低16位则存储在相应的容器中。

RoaringBitmap支持两种类型的容器:

1、ArrayContainer:当一个容器内的元素数量较少(通常少于4096个)时,使用一个简单的数组来存储这些元素。这种容器适合存储稀疏的数据。

2、BitmapContainer:当容器内元素数量较多时,使用位图存储。位图是一个长数组,每个位代表一个低16位的整数是否存在。这适合存储密集型数据。

传送门:RoaringBitmap的原理与应用,看这个就够了-CSDN博客

四、存储设计

使用Mysql的longtext类型存储持久化后的RoaingBitmap。

注意:

1、Mysql字段类型:text 字段类型的字节限制为 65535 字节, 约为0.06MB;longtext 字段类型的字节限制为 2147483647 字节,约为2047MB;mediumtext 字段类型的字节限制为 16777215 字节,约为16MB。

2、mysql单次最大传输数据量限制约为16M

3、RoatingBitMap 5千万全量数据,持久化存储需要 6259385 字节,约 6M;

持久化关联Java代码,以及RoaringBitMap的逻辑i操作:

java 复制代码
    /**
     * roaringBitmap 转 string
     *
     * @param roaringBitmap 入参
     * @return str
     */
    public static String roaringBitMapToString(RoaringBitmap roaringBitmap) {
        // RoaringBitMap 转为byte数组
        byte[] array = new byte[roaringBitmap.serializedSizeInBytes()];
        roaringBitmap.serialize(ByteBuffer.wrap(array));
        // byte转为字符串
        return new String(array, StandardCharsets.ISO_8859_1);
    }

    /**
     * string 转 roaringBitmap
     *
     * @param bitMapStr bitmap序列化字符串
     * @return RoaringBitmap
     */
    public static RoaringBitmap stringToRoaringBitMap(String bitMapStr) {
        RoaringBitmap roaringBitmap = new RoaringBitmap();
        try {
            // str 转为 byte数组
            byte[] redisArray = bitMapStr.getBytes(StandardCharsets.ISO_8859_1);
            roaringBitmap.deserialize(ByteBuffer.wrap(redisArray));
        } catch (IOException e) {
            log.error("stringToRoaringBitMap {} Error", bitMapStr.length(), e);
        }
        return roaringBitmap;
    }

    /**
     * bitMap操作
     *
     * @param oldBitMap    bitMap_1
     * @param newSubBitMap bitMap_2
     * @param operate      操作符
     * @return RoaringBitmap
     */
    public static RoaringBitmap roaringBitMapOperate(
            RoaringBitmap oldBitMap, RoaringBitmap newSubBitMap, String operate) {
        switch (operate) {
            case "add":
            case "or":
                oldBitMap.or(newSubBitMap);
                break;
            case "remove":
                oldBitMap.andNot(newSubBitMap);
                break;
            case "replace":
                oldBitMap = newSubBitMap;
                break;
            case "and":
                oldBitMap.and(newSubBitMap);
                break;
            default:
                throw new IllegalArgumentException("operate 异常" + operate);
        }
        return oldBitMap;
    }

五、压测数据准备

目标数据量 五千万 客户,10个标签,总数据量在一亿两千万左右,存储空间53MB

六、本地环境(4核8G机器)测试结果

5千万客户,标签覆盖率约为 50%,40%,30% ,20%,10%五个批次;客户随机离散分布在10个标签, 单标签平均客户量一千二百万左右,测试随机取交,并集耗时。

jmeter单线程循环调用

参与筛选标签数据 交/并集 取样次数 响应耗时中位数 平均数 响应耗时90% 响应耗时95% 响应耗时99% 最小 最大
2个 50 277 276 286 289 303 263 303
2个 50 273 276 286 288 308 262 308
2个 随机 50 282 283 299 302 307 265 307
5个 50 702 712 746 755 792 664 792
5个 50 701 710 727 733 761 667 761
5个 随机 50 685 690 715 729 742 665 742
8个 随机 50 1145 1140 1175 1187 1202 1075 1202
10个 随机 50 1379 1387 1433 1450 1481 1336 1481

jmeter多线程循环调用 开发环境单副本 5个线程循环20次

参与筛选标签数据 交/并集 取样次数 响应耗时中位数 平均数 响应耗时90% 响应耗时95% 响应耗时99% 最小 最大
2个 随机 100 526 525 601 609 623 299 658
5个 随机 100 1409 1448 1685 1723 1816 871 1897
8个 随机 100 2210 2219 2433 2468 2605 1650 2760
10个 随机 100 2789 2815 3094 3125 3308 2194 3440

1、任意2个标签取交、并集耗时, 各采样50次

并集测试结果

2、任意5个标签取交、并集耗时, 各采样50次

交集测试结果

3、任意8个标签随机交、并集耗时, 采样50次

4、任意10个标签随机取交、并集耗时, 各采样50次

相关推荐
代码敲上天.11 分钟前
数据库语句优化
android·数据库·adb
盒马盒马27 分钟前
Redis:zset类型
数据库·redis
静听山水41 分钟前
mysql语句执行过程
数据库·mysql
虽千万人 吾往矣1 小时前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
Q_w77421 小时前
一个真实可用的登录界面!
javascript·mysql·php·html5·网站登录
mariokkm1 小时前
Django一分钟:在Django中怎么存储树形结构的数据,DRF校验递归嵌套模型的替代方案
数据库·django·sqlite
Wang's Blog2 小时前
Redis: 集群环境搭建,集群状态检查,分析主从日志,查看集群信息
数据库·redis
容器( ु⁎ᴗ_ᴗ⁎)ु.。oO3 小时前
MySQL事务
数据库·mysql
数据龙傲天4 小时前
1688商品API接口:电商数据自动化的新引擎
java·大数据·sql·mysql
engineer-gxd4 小时前
MySQL 表的操作
mysql