CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

京东云开发者
• 阅读 156

一、背景介绍

CDP系统中目前存在大量由用户ID集合组成的标签和群体,截止当前已有几千+标签,群体2W+。

大量的标签都是亿级别数据量以上,例如性别、职业、学历等均,甚至有群体中的ID数量达到了数十亿+。

并且随着用户ID池的不断增加,标签和群体本身包含的ID数量也随之增加,如何存储如此多的数据,标签与群体之间的组合计算,是我们面临的挑战。

二、问题描述

如此大量的用户ID集合,虽然标签和群体的ID集合本质类似,但是都需要存储亿级别的ID数据,这就对存储结构提出较高的要求。

这里拿群体举例,如果某群体包含1000W个用户ID,通过文本文件存储,大概需要150M,40亿的群体就达到了惊人的150*40*10=60000M,大约60G,而我们的群体数量已经达到了几W+,再加上标签数据,所需要的存储空间将不可接受。

并且,数据的存储只是其中一个方面,后续针对标签和群体的组合计算,创建出更细粒度的ID包也是一个挑战。

三、解决方案

面对以上问题,CDP采用了Bitmap的思路来解决,不但解决了存储空间问题,而且Bitmap本身的交并差运算,能够很好的支持用户对不同标签和群体的组合计算,详细方案如下。

1)Bitmap简介

为了便于理解,首先介绍一下什么是bitmap。

它的基本思想是用bit位来唯一标记某个数值,这样可以用它来记录一个数值没有重复的数据元组。并且每一条数据只使用一个bit来标识,能够大大的节省存储空间。

比如,我想存储一个数值数组[2,4,6,8]。

Java中如果用byte类型来存储,不考虑其他开销,需要4个字节的空间,一个字节8位,也就是4*8=32bit。

倘若使用更大的数据类型,存储空间也会相应增大,如使用Integer(4字节),则需要4*4*8=128bit。

而如果采用bitmap的思想,只需要构建一个8bit空间,也就是一个字节的空间来存储,如下图。

CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

2)用户ID池编码

通过上文的例子,可以看到,使用Bitmap思想来存储,实际上每一个数据是一个bit,而且不能重复,这一点用户ID是符合的,没有重复的用户ID。

由于bitmap里只能存0或者1来标识当前位是否有值,而用户ID确是一个字符串,这就需要将数十亿的用户ID进行唯一性编码,这个编码也就是我们常说的offset偏移量。

每一个用户ID对应一个唯一的offset,目前已到数十亿,也就是说当前最大的偏移量是数十亿+,这部分由数据同学帮我们加工一张ID池表,其中包含了ID和offset的对应关系。这样,新注册的id,只要顺序增加offset值即可。

下边是一个简单示意图,假设我有8个id,id1id8,对应的offset编号为18。

我要建一个只包含双数id的标签或群体,则我只需要将offset为2,4,6,8的位设为1即可。

CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

3)遇到问题

有了存储的数据结构,还有id池,接下来就是具体实现了。

提到Bitmap,首先想到的是Java中的一种实现方案BitSet,不过它存在两个问题。

一是我们的id池已经到达几十亿+,已经超出了BitSet所能处理的范围,当前超出了2^32=4294967296

CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

另一个问题是,倘若我建一个包含两个id的群体,第一个offset是1,第二个offset是10000000,这种情况还是要创建一个1000wbit的空间来存储,并且只有两个bit位是1,其他的全为0,这显然造成了很大的空间浪费。

也就是说,数据越稀疏,空间浪费越严重

CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

下方位BitSet扩容时的代码,由代码中也可以看到,默认扩容2倍,当需要的大小超过2倍时,则按照需要扩容。

    public void set(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);

        words[wordIndex] |= (1L << bitIndex); // Restores invariants

        checkInvariants();
    }

    private void expandTo(int wordIndex) {
        int wordsRequired = wordIndex+1;
        if (wordsInUse < wordsRequired) {
            ensureCapacity(wordsRequired);
            wordsInUse = wordsRequired;
        }
    }

    private void ensureCapacity(int wordsRequired) {
        if (words.length < wordsRequired) {
            // Allocate larger of doubled size or required size
            int request = Math.max(2 * words.length, wordsRequired);
            words = Arrays.copyOf(words, request);
            sizeIsSticky = false;
        }
    }


当用户圈的群体特别稀疏时,有可能会造成很大的空间浪费,所以,我们需要使用一种能够压缩的高效的位图实现。

4)RoaringBitmap压缩

我们最终使用的是RoaringBitmap,一种高效的压缩位图实现,简称RBM。于2016年由S. Chambi、D. Lemire、O. Kaser等人在论文《Better bitmap performance with Roaring bitmaps》 《Consistently faster and smaller compressed bitmaps with Roaring》中提出。

基本实现思路如下:

以整型int(32位)为例,将数据分成高16位和低16位两部分,低16位不变,作为数据位Container,高16位作为桶的编号Container,可以理解为高位的Container中,存放了很多个低位Container。

高低位计算示例:

protected static char highbits(int x) {
  return (char) (x >>> 16);
}

protected static char lowbits(int x) {
  return (char) x;
}

比如,我要存放65538这个值,则高位为65538>>>16=1,低位为65538-65536*1=2,即存储在1号桶的2号位置,存储位置如下图:

CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

我们当前使用的RoaringBitmap版本为0.8.13,Container包含了三种实现:ArrayContainer(数组容器),BitmapContainer(位图容器),RunContainer(行程步长容器)

CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

不过,上文中提到当前id池已经超过了整型所能标识的最大范围(2^32=4294967296),所以需要一个能够处理64位的实现,我们使用了RoaringBitmap包中支持64位的Roaring64NavigableMap。

它的实现思路和32位的基本一致,分成了高32位和低32位两部分

jar包引入方式:

<dependency>
     <groupId>org.roaringbitmap</groupId>
     <artifactId>RoaringBitmap</artifactId>
     <version>0.8.13</version>
</dependency>

public void add(long... dat) {
    for (long oneLong : dat) {
      addLong(oneLong);
    }
 }

public void addLong(long x) {
  int high = high(x);
  int low = low(x);
  …………
}

public static int high(long id) {
  return (int) (id >> 32);
}

public static int low(long id) {
  return (int) id;
}

bitmap位图操作方法:

CDP技术系列(一):使用bitmap存储数十亿用户ID的标签或群体

四、现状及展望

目前,CDP画像的标签和群体均采用了RoaringBitmap的存储方式。人群和标签的交并差计算,生成更加精细化的人群就可以通过bitmap的操作来实现。

有了良好的存储方式,下一步就是如何将存储在数据仓库的明细数据,加工成原始的标签或者群体,具体实现方案会在下一篇分享。

作者:京东科技 黎宇飞

来源:京东云开发者社区 转载请注明来源

点赞
收藏
评论区
推荐文章
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
janusgraph
精确查询语句含义测试语句执行时间查询顶点标签为FALV的顶点数量g.V().hasLabel('FALV').count()2400s查询顶点属性中id为19012201clockWithResult(1){g.V().has('id','19012201')}0.18540099999999998s查询顶点属性中
Wesley13 Wesley13
2年前
JQ选择器
如果你想寻找id以“sub\_”开头的元素,你可以使用:$("id^'sub_'")如果你想寻找id以“trim”结尾的元素,你可以使用:$("id$'trim'")要获得id包含“AAA”的元素,需要使用(这比遍历要快)$("id'trim'")可以指定元素类型input或者其他标签
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
京东云开发者 京东云开发者
3个月前
CDP技术系列(二):ClickHouse+Bitmap实现海量数据标签及群体组合计算
一、背景介绍上一篇文章介绍了CDP中,面对单个标签或群体数十亿的数据如何存储我们都知道数据仓库的概念,它的里边存储了我们所有的数据,其中就包含了标签或群体所依赖的数据,但是这些数据并不能直接拿来使用,想要变成业务需要的标签或群体数据,还需要进行加工。数据工
京东云开发者 京东云开发者
3个月前
CDP 技术系列(三):百万级 QPS 的人群命中服务接口性能优化指南
一、背景介绍CDP系统提供了强大的标签和群体的构建能力,面对海量数据的标签和群体,我们采用了BitmapClickHouse的存储与计算方案。详细内容可以参考之前文章。有了群体之后,它们被广泛的应用到支付,消金,财富,营销等各种核心业务的用户拉新,交易转