掘金 后端 ( ) • 2024-03-28 09:14

大家好,我是老王,前一段时间,领导想要了解下当前系统中得用户具体情况,想要搭建用户画像体系,给每个用户进行打标,方便后期精准化运营以及了解用户分层等情况。我想了下当前我们系统日活大概500w左右,如果每个用户打10个标签那就是5千万,如果成败上千个标签那么就几十亿得数据。并且这些数据还要进行各种交叉并集得计算,以及每日更新用户得标签数据,不管是存储成本还是计算成本,简单的按列存储数据库到数据库,肯定不太现实,那么有什么更好的存储方案吗?今天就来跟大家简单介绍下一种计算性能高并且存储成本低得大数据存储方案。

1.Bitmap介绍

1.1.问题思考

首先我们来看一个问题

假如有一百亿个随机整数数据,要求去重或者计算总共有多少不重复的数据?你会用什么办法呢?

最简单也最容易想到的的方法就是哈希表,通过HashMap及进行去重。但是这里面有个内存使用量的问题,如果每个数字用int存储,那就是100亿个int,因而占用的空间约为 :

(10000000000*4/1024/1024/1024)≈37.5G

即使不考虑HashMap本身实现所用到的数据结果,单单key本身,这里都需要37.5GB的内存占用,很显然是不合理的。

那么我们要如何降低内存的占用,又能满足业务要求呢。

1.2.什么是bitMap

bit即比特,是计算机系统里边数据的最小单位,8个bit即为一个Byte。一个bit的值,要么是0,要么是1。bitmap可以理解为通过一个bit数组来存储特定数据的一种数据结构;

BItMap的思想非常简单,就是用一个bit表示一个二元的状态,比如有或者没有,存在或者不存在,用bit本身的位置信息,对应不同的数据。

1.3.bitMap原理

BitMap 的基本原理就是用一个 bit 来标记某个元素对应的 Value,而 Key 即是该元素。

假如存在数字7,那就把对应的第7位的值赋为1。上图插入的数据为1,2,6,7。接着依次把所有的数据遍历然后更新这个BitMap。这样我们就可以得到最终结果。

如果使用BitMap存储,那么他的存储大小是多少呢?

如果按位存储就不一样了,20亿个数就是20亿位,占用空间约为 (10000000000/8/1024/1024/1024)≈1G.

和用int存储得37.5G相比,大约37倍。由此可见,bitMap是一种非常节省空间的数据接口

1.3.bitMap优缺点

bitMap得优缺点如下:

优点:

运算效率高,不需要进行比较和移位;

占用内存少

缺点:

天然的去重特性,不能进行重复数据的统计和非去重排序。

只有当数据比较密集时才有存储优势

2.使用ClickHouse 存储用户数据标签

2.1.ClickHouse介绍

ClickHouse是俄罗斯的Yandex于2016年开源的一个用于联机分析(OLAP:Online Analytical Processing)的列式数据库管理系统(DBMS:Database Management System),简称CK , 使用C++ 语言编写, 主要用于在线分析处理查询(OLAP),能够使用SQL查询实时生成分析数据报告。

ClickHouse是一个完全的列式数据库管理系统,允许在运行时创建表和数据库,加载数据和运行查询,而无需重新配置和重新启动服务器,支持线性扩展,简单方便,高可靠性,容错。它在大数据领域没有走 Hadoop 生态,而是采用 Local attached storage 作为存储,这样整个 IO 可能就没有 Hadoop 那一套的局限。

2.2.为什么选择Clickhouse?

2.2.1. 速度快

ClickHouse性能超过了市面上大部分的列式存储数据库,相比传统的数据ClickHouse要快100-1000倍ClickHouse还是有非常大的优势。

100Million 数据集:ClickHouse比Vertica约快5倍,比Hive快279倍,比MySQL快801倍。

1Billion 数据集:ClickHouse比Vertica约快5倍,MySQL和Hive已经无法完成任务了。

2.2.2. 功能多

ClickHouse支持数据统计分析各种场景,天然为Ad-hoc而生的,支持多种计算函数,并且本身也支持bitMap等多种数据结构存储。

  1. 支持类SQL查询;
  2. 支持繁多库函数(例如IP转化,URL分析等,预估计算/HyperLoglog等);
  3. 支持数组(Array)和嵌套数据结构(Nested Data Structure);
  4. 支持数据库异地复制部署。

2.3.如何在Clickhouse中使用bitmap?

bitmap在clickhouse中是一种AggregateFunction的数据类型,创建表结构如下:

CREATE TABLE tag_users
(
    tag_id String,
    users AggregateFunction(groupBitmap, UInt64)
)
ENGINE = MergeTree
PARTITION by tag_id
ORDER BY tag_id;

那么users 这个字段就是用bitmap来存储用户得集合。

2.4.Clickhouse使用Bitmap存储标签

2.4.1.标签表创建

一般我们设计用户画像标签,一般是有标签,标签值,以及对应的用户,不如用户A,标签为性别标签,那么标签值可能是男或者女来标注这个用户得具体信息。因为我们是使用bitmap来进行存储,故用户集群得数据结构是一个bitmap,那么整个标签表大概得数据结构如下。

CREATE TABLE user_profile_bitmap
(
ln String comment '标签名字 ',
lv String comment '标签值',
uv AggregateFunction(groupBitmap, UInt64) comment 'bitmap 存储用户'
)
ENGINE = AggregatingMergeTree()
PARTITION BY ln
ORDER BY (ln, lv)

表示标签以及标签值对应的用户集合。

2.4.2.数据写入标签表

一般我们会将用户得详细信息提前同步到clickhouse中,比如用户的登录注册,以及基本信息,费信息。活跃信息等。之后通过以下两种方式,将数据写入标签表中

1.通过 聚合函数 groupBitmapState来构造

我们可以通过groupBitmapState函数进行构造 bitmap , 通过查询业务表中,计算需要写入的标签值,通过对应的标签进行聚合查询,得到满足该条件得用户,并将用户id通过groupBitmapState函数转为bitmap进行存储

如下,查询用户省份信息写入标签表中

insert into user_profile_bitmap select '省份',province,groupBitmapState(toUInt64(user_id)) from users_table group by province;

2.通过对整形数组进行转换得到

也可以通过查询出来的用户构建数组通过bitmapBuild进行构建bitmap。

select bitmapBuild([1,2,3,4,5]) as res, toTypeName(res);

2.4.3.标签数据查询性能分析

当数据写入标签库之后,我们一般会每个标签人数,多个标签人数交际以及单个用户得标签进行统计,具体统计方式以及查询性能如下。

以下性能统计在标签库大约300多个标签,25亿用户数据进行统计。

1.查询标签预估人数

统计bitmap中的人数,需要用到2个函数

groupBitmapMergeState: 表示查询bitmap中间形态数据,因为我们存储bitmap都是用的groupBitmapState,故bitmap实际都是二进制存储,操作也需要转为中间态进行处理。

bitmapCardinality:是用来统计bitmap种得基数,即有多少个1,在这里即表示有多少个用户。

select lv, bitmapCardinality(groupBitmapMergeState(uv))as uv from user_profile_bitmap where ln='标签17' group by lv order by uv
┌─lv──────────┬──────uv─┐
│ 标签17_值_3 │ 7275347 │
│ 标签17_值_2 │ 9327228 │
│ 标签17_值_0 │ 9328032 │
│ 标签17_值_1 │ 9328159 │
└─────────────┴─────────┘
4 rows in set. Elapsed: 0.111 sec. 

数据查询只花费了0.111s,性能表现优秀。

2.查询标签有哪些用户userId

bitmapToArray:是指将bitmap转为数组,用来查询具体值。

select bitmapToArray(groupBitmapMergeState(uv))as uv from user_profile_bitmap where ln='标签10' and lv = '标签10_值_1' ;
1 rows in set. Elapsed: 10.254 sec. 

800W userID查询时间为10S,经统计主要是bitmap转数组数据量过大,网络传输所致。

3.人群圈选交集人数预估

首先我们需要先查询出来多个标签得bitmap结果,然后再将bitmap结果取交集运算。

bitmapAndCardinality: 对两个bitmap取与运算,返回满足条件得个数。

select bitmapAndCardinality(bu1,bu2) as uv from (select 1 as jid, groupBitmapMergeState(uv) as bu1 from user_profile_bitmap where ln='标签2'  ) as t1  inner join (select 1 as jid, groupBitmapMergeState(uv) as bu2 from user_profile_bitmap where ln='标签86' ) as t2 on t1.jid=t2.jid ;
┌──────uv─┐
│ 9520457 │
└─────────┘
1 rows in set. Elapsed: 0.155 sec. 

数据查询只花费了0.155s,性能表现优秀。

4.人群圈选交集用户计算

首先我们需要先查询出来多个标签得bitmap,然后将bitmap进行合并然后拆解新的bitmap转为数组。

bitmapAnd:对两个 bitmap 求交集并返回新的bitmap。

select arrayJoin( bitmapToArray((
bitmapAnd(
(select groupBitmapMergeState(uv) from user_profile_bitmap where ln='标签2' ),
(select groupBitmapMergeState(uv) from user_profile_bitmap where ln='标签86') 
)) ))as uv;

9613586 rows in set. Elapsed: 5.374 sec.

5.查询单个用户标签画像

查询单个用户所具有的标签,就要从标签库种所有列种检索bitmap种是否存在该用户,然后返回存在得标签列信息。我们用到的函数是bitmapContains。

bitmapContains:表示 bitmap 是否存在该用户,即是否存在id数字。

select ln,lv from user_profile_bitmap where bitmapContains(uv,toUInt32(9999957))=1 group by ln,lv;
162 rows in set. Elapsed: 1.204 sec. 

20亿数据350个标签中查询单个用户画像基本1s左右

2.5 .方案缺陷

因为bitmap只支持数值类型,因此如果用户id使用得是uuid或者字符串则方案不太适用,一般我们解决方法是新增一张自增id和用户id得映射表,通过标签表计算出结果再反射出用户的真实id进行使用。

3.总结

整体来看,使用clickhouse得bitmap用来存储用户标签数据,不管存储成本还是查询性能表现都比较优异。方案得缺陷也可以通过其他方式解决,具体标签扩展数据可以根据自己的业务再具体补充,本方案只提供一种最基本的设计,具体方案大家可以结合自己得业务在做细分扩展。