热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何基于BitMap来作为底层的数据模型

作者:陈凯 GrowingIO 数据开发工程师,主要负责 SaaS 和 OP 产品数据平台的开发和设计,目前专攻于微服务、数仓建设方向。GrowingIO 每天需要处理近千亿的用户行为数据,平台的「事

作者:陈凯
GrowingIO 数据开发工程师,主要负责 SaaS 和 OP 产品数据平台的开发和设计,目前专攻于微服务、数仓建设方向。

GrowingIO 每天需要处理近千亿的用户行为数据,平台的「事件分析」模块是使用比较频繁的功能,简单且强大。在事件分析中,客户可以很灵活地使用多种维度组合去查看某个指标,并且查询的速度也十分可观。

本文抽取 GrowingIO 在事件分析中的通用数据模型,揭晓该功能背后的存储模型和实现原理。

在用户行为的数据分析中,无论是无埋点,还是埋点,对于某一条行为数据的表达形式往往是:「某人」于「某个时间」在「某个维度」下做了「某个动作」「多少次」。

所以在数据统计中,这种表达形式可以拆解成「指标量」和「维度」,指标量可以是用户量、页面浏览量、某个埋点的次数等,维度可以是时间、城市、浏览器、用户属性等。

在海量数据的背景下,如何比较高效地完成指标+维度的计算,一直是大数据分析领域比较热门的话题,下面将讲述在 GrowingIO ,我们是如何高效解决的。

1. 从一个数据需求说起?

假设给定如下一组用户行为的原始数据:数据含义: 表示某个用户的某次访问记录。(这里仅列举了地区和设备维度,当然还会存在浏览器、平台、版本等维度,这里不一一列举了。)

1.1 使用 SQL 分析统计

? 现在业务想计算「过去7天」在「地区」维度下,「设备: Mac」的人数是多少?So Easy,一个 SQL 搞定在这里插入图片描述

使用 GrowingIO 平台的分析工具可以表示如下:
在这里插入图片描述

但是通过 SQL 这种现查的方式,随着数据量的越来越大,几十亿或上百亿的时候,对计算所需要的资源和响应时间也会线性地增长,此时客户在使用平台工具最直观的感受就是“菊花”转转转,图表一直加载不出来。

1.2 如何使查询更加高效

1.2.1 堆机器,加资源

最直接粗暴的方式,就是增加更多的计算资源,或者对查询的结果进行缓存、预热。但是对于 SaaS 产品来讲,在查询并发比较高的时候,再多的计算资源也会因为查询排队而遇到性能瓶颈。

1.2.2 数据分层

? 在数仓的分层架构中,对于经常使用的查询结果,我们可以通过离线计算的方式生成了一个结果表「过去7天-地区-设备-指标表」,示例如下:在这里插入图片描述

这样在特性的查询场景中,只需要查询结果表就行,很大的减少了计算量,响应时间也短了不少。

? 但是业务那边的需求往往是千变万化的,然后一堆统计的需求砸到了你的脑袋上

「过去 30 天」的「总用户的量」和「总访问量」是多少?
「昨天」「设备: Windows」的「用户量」是多少?
「获取 30 天」按「平台」拆分的「用户量」是多少?
「11 月- 11 日」来了哪些用户?请导出这个用户列表
其他更多漏斗、留存、画像等数不尽的需求让人头晕目眩。

? 你看了看生成结果表,发现并不能完全解决这些问题,你觉得需要生成更多的结果表来满足更多的需求。然而到最后你发现有些表居然仅仅只使用了一次,数仓里面堆了一堆垃圾。

1.2.3 数据预聚合

和数仓分层的理念类似,对数据进行预计算、预聚合,使用空间换时间的思想加快计算。这也是目前一些主流开源框架的解决思路,比如 SparkSQL 的物化视图、Kylin的 Cube、Druid 的 Segment 、Carbondata 的 MV 等。

下面使用一张图展示主要区别:
在这里插入图片描述

基于我们所追求的方式,我们首先需要寻找一种高效并且灵活的存储模型。

1.3 优化存储模型

基于上节中数据预聚合的思路,从预聚合的结果中,我们不难发现,其中有几个没有摆脱的点:
多个维度依然是横向存储在一起
维度和指标绑定在一条记录中
因为存在这种局限性,刚好限制了我们灵活进行「维度+指标」的任意组装和扩展

? 如何才能更好的让维度和指标随心所欲地组合呢?我们在预聚合结果的基础之上做了一些改进:
将每个维度纵向存储
将维度和指标分开存储

2. 基于 BitMap 的存储模型 ?

2.1 纵向存储维度(人数)

依然以开篇的那组数据为例,此时将维度进行纵向存储:在这里插入图片描述

此时想取「地区: 北京」和「设备: Mac」的「用户量」

在这里插入图片描述

OK!这样可以很灵活的解决各种维度组合起来的问题了,而且连用户的群体也能直接获取。

? 但是从表格中发现,用户存储「用户集合」的数据结构还没有解决。那么既能以类似数组的方式存储整数值,还能使用交集(and)操作,还需要达到更好的数据压缩和计算。

此时你应该想到了 BitMap 这种数据结构

至于为何选用 BitMap 的数据结构,以及 BitMap 的功能和基本使用,这里不再探讨。可以参考 java 的实现 BitSet 以及优化的库 RoaringBitmap。

2.2 存储指标量(次数)

为了解决存储指标次数的存储问题,你需要用一个Map 的结构来存储「总的次数」: Map (其中key为次数,value为符合访问次数的人)

在这里插入图片描述
访问量表示: 总共访问「1次」有哪些人,访问「2次」的有哪些人等等。

此时计算「地区: 北京」和「设备: Mac」的「访问量」

2.3 使用更优雅的方式存储次数

在 Map 这个结构中,key 存储的是 10 进制的数字。这就会导致 Map 的 key 变得特别特别多,所以需要有一种方式来优化一下结构。

方式就是用将 10 进制转化为 2 进制的方式去存储次数,此时 Map 的 key 存储是 二进制为 1 的位置:

比如 2 的二进制是: 「10」,从右向左分别表示(下标i从0开始)「第0位是0」,「第1位是1」。所以将key为1的 bitmap 中存储这个人。

比如5的二进制是: 「101」,从右向左分别表示「第0位是1」,「第1位是0」,「第2位是1」。所以将 key 为 0 和 2 的 bitmap 中存储这个人。

然后将上节 2.2 中结果表示如下:

在这里插入图片描述
此时计算「地区: 北京」和「设备: Mac」的「访问量」

在这里插入图片描述

3. 多维度交叉的问题 ⚔️

理想是美好的,但是现实很残酷。在 2.1 小节的例子中,每个用户的维度组合只有一种,但是现实中往往一个用户行为可能会存在多种维度组合的情况。

那么什么是维度组合: 一条数据中唯一的所有维度值,称为一个组合。

PS: 如果你的系统中某个 ID 的维度组合只有一种。比如某个订单,一旦生成了,他的价格,商品,物流等信息基本都是固定的。那么之前的模型基本都能满足大多场景了。

3.1 面临的问题

? 那么会导致什么问题呢?此时回到起点,又来了一批用户行为的数据如下:

在这里插入图片描述
此时多了一个「用户1」在「杭州」使用了「Windows」。如果按照之前的模型存储如下:在这里插入图片描述

此时计算「地区: 北京」的用户:直接返回 [ 1 , 2 , 3 ],问题不大

此时计算「地区: 北京」和「设备: Windows」的用户
在这里插入图片描述
❌ 你会发现,得出的结果是错的,应该只有「用户 3 」满足才对。

3.2 使用维度组合编号的方式解决

其实问题出在将维度分开进行存储的时候,丢失了「维度组合关系」这个重要的衡量条件。「用户 1 」虽然在「北京」待过,也使用过「Windows」,但是他却没有同时满足这个条件,这就是问题所在。

所以需要一种方式来存储「维度组合关系」这一重要信息。

将每个人维度组合进行顺序编号,得到如下结果:
在这里插入图片描述

注意:编号是对应到每个人的,相同的维度组合,编号是一样的。

此时对应的存储结构也发生了变化:Map( key 代表编号,value 代表人的集合在这里插入图片描述

此时我们再来计算「地区: 北京」和 「设备: Windows」维度的用户在这里插入图片描述

最后得到的结果就是「用户3」,算出来的数据就变准确了。

3.3 多维度情况下计算次数

其实稍微想一下就是两层的 Map 结构:Map>,比如刚刚的那组数据表示如下:在这里插入图片描述

比如「用户1」这个用户,在「编号0」发生的 2 次,在「编号1」发生了 1 次
在这里插入图片描述
此时我们来计算「地区: 北京」和 「设备: Mac」维度的访问量

在这里插入图片描述

4. 简单的性能对比

环境准备:SparkSQL(local[16], 内存4G), BitMap 单线程计算(内存4G)

场景:简单的 2 ~ 3 个维度组合求人数、次数,按照值的降序取 Top 1000在这里插入图片描述

x轴含义: 数据量/用户量。
y轴含义: 计算时间, 单位毫秒。

可以看到随着数据量的不断递增,SparkSQL 的计算时间也在不断递增,但是 BitMap 的计算时间却相对比较稳定。

5. 总结

BitMap 是一个兼并计算和存储优】势的数据结构,在存储上百万甚至上千万的 ID 时,也能得到很好的计算效果。

并且当你使用 BitMap 存储的时候,就已经天然支持很多的业务场景,比如分群计算、标签计算、漏斗分析、留存分析、用户触达等,因为无需再重新计算人群。

本篇主要揭晓我们是如何基于 BitMap 来作为底层的数据模型,当然在实际应用过程中还有很多的挑战,由于篇幅原因,这里就不展开讲述了。

以下列出一些我们后续的工作内容和攻克方向:
bitmap 是以 int 值进行存储的,但是在实际生产中,你的 ID 数据可能是类似 UUID 的这种字符串,那么需要解决 string 转唯一 int 的问题。
如何使 bitmap 在分布式环境下达到更好的计算效果
如何解决高基维度所带来的挑战
如何在实时、图表分析、分群画像以及更多的业务场景中进行使用
如何设计一个类 SQL 语言来兼容这种数据模型

关于GrowingIO

GrowingIO 是基于用户行为数据的增长平台,国内领先的数据运营解决方案供应商。为产品、运营、市场、数据团队及管理者提供客户数据平台、获客分析、产品分析、智能运营等产品和咨询服务,帮助企业在数据化升级的路上,提升数据驱动能力,实现更好的增长。

如果对我们的产品有兴趣,欢迎点击此处,获取15天免费试用!


推荐阅读
  • 本文由编程笔记#小编整理,主要介绍了关于数论相关的知识,包括数论的算法和百度百科的链接。文章还介绍了欧几里得算法、辗转相除法、gcd、lcm和扩展欧几里得算法的使用方法。此外,文章还提到了数论在求解不定方程、模线性方程和乘法逆元方面的应用。摘要长度:184字。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 判断数组是否全为0_连续子数组的最大和的解题思路及代码方法一_动态规划
    本文介绍了判断数组是否全为0以及求解连续子数组的最大和的解题思路及代码方法一,即动态规划。通过动态规划的方法,可以找出连续子数组的最大和,具体思路是尽量选择正数的部分,遇到负数则不选择进去,遇到正数则保留并继续考察。本文给出了状态定义和状态转移方程,并提供了具体的代码实现。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • SpringBoot整合阿里云OSS实现单个图片上传和批量上传
    SpringBoot整合阿里云OSS实现单个图片上传和批量上传 ... [详细]
author-avatar
生死有命富贵在天999_275
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有