大数据存储处理-bitmap的艺术

本文涉及的产品
云原生大数据计算服务 MaxCompute,5000CU*H 100GB 3个月
云原生大数据计算服务MaxCompute,500CU*H 100GB 3个月
简介: 大数据存储处理-bitmap的艺术

引言

有这样的一个面试题:

给20亿个不重复的unsigned int的整数的文件,每一行一个数字,没排过序的,然后再给一个数,如何快速判断这个数是否在那20亿个数当中?

在php中,其实是有sort 和in_array函数可以实现的,例如:

<?php
$arr = \[
  5,8,1,3,54,745,123
\];
var_dump($arr);
sort($arr);
var_dump($arr);
var\_dump(in\_array(31,$arr));
var\_dump(in\_array(54,$arr));

这样就可以直接实现排序和判断了

但是事实上真的如此吗?我们要考虑下,20亿个数有多大.

在64位环境下,php的int占用为8个字节

20亿个数字,需要占用8*20亿/1024/1024/1024约等于15G的内存

很明显,大多数人的电脑,或者服务器都没有这么大的内存,那么我们该怎么才能实现这个需求呢?

用户签到问题

或许有人会觉得这个问题不切实际,"谁特么没事闲着蛋疼算这个?"

那好,这个问题先放一边,我们换个问题

假设网站有1亿用户,现在需要增加个签到功能,用于奖励那些活跃用户

现在需要你实现以下功能:

1:记录某个用户每年的签到情况

2:统计某一个月份没有断签的用户

3:查看某个用户的连续签到情况

这个问题看起来很简单啊,新增一个记录表,每天签到则新增一条记录,用户加个索引

直接where查询用户就行了

统计没有断签也简单啊,where 月份区间 count一下就行了

至于连续签到情况,增加个字段表示连续签到就行了

这个方案确实是可以解决,那么同样的,

1亿用户,假设每天有3000万人签到,那么一个月,一年下来,数据将会是:30000000*365差不多是100亿的数据...

bitmap

那么根据上面2个需求场景,当在数据量大的情况下时,处理将会非常复杂,那该怎么做呢?那就是使用bitmap

什么是bitmap?简单来说就是位图.

二进制

首先我们从二进制讲起,众所周知,计算机最后的操作都是二进制操作,数据存储原始都是二进制存储的.

但是具体该怎么理解这个呢?

由于在一般情况,计算机识别不同的状态只能通过"断电","通电" 2种状态,

举个例子,有座岛屿,岛上的人需要别人送饭给他们吃,但是如果每天吃一种菜,会腻,然后没有电话什么的,该怎么告诉岛外的人,他们想吃什么呢?

然后,他们想到了一种方法,在岛上建了2座灯塔,如果亮一座灯塔就代表要吃猪肉,如果亮两座灯塔就代表要吃牛肉

这样,他们就可以区分2种菜了,再后来,猪肉牛肉吃腻了,想吃羊肉,鸡肉怎么办呢?

他们又想到了一种办法,由于2座灯塔是不一样的位置,所以他们约定了,

第一座灯塔亮,第二座不亮,表示猪肉, 01

第一座灯塔不亮,第二座亮,代表牛肉, 10

第一座灯塔亮,第二座亮,代表羊肉,  11

第一座灯塔不亮,第二座灯塔不亮,代表鸡肉 00

这样,他们用2座灯塔,可以表示4种不同的信息,这就是最原始的二进制了

通过二进制,我们可以用多个 简单的 "开","关"去代表非常大的信息

例如1T的硬盘,都是通过以上这种方法存储的值

二进制数字

通过上面的二进制介绍,我们了解了二进制的存储原理,那么,二进制数据是怎么表示的呢?

同样,因为只有"开","关" 两种状态,我们分别用1,0表示

那么

数字0 00

数字1 01

数字2 10

数字3 11

数字4 100

数字5 101

数字6 110

数字7 111

在64位环境上,int的表示使用了64位的开关,即每个数字(比如0,11,52),都需要占用64位8字节的存储空间,最大存储值为9223372036854775807二进制表示为64个1(1111111111111111...)

那么,我们知道了php的数字存储方式,能否想办法节省存储呢?当然是可以的

在上面我们发现,由于一个数字表示需要64个开关(64位),我们完全可以这样做:

由于数字以0开始,所以0位应该存储在第一个开关

数字1,我们在第2个开关记为1  010

数字3,我们在第3个开关记为1  100

数字63,我们在第64个开关记为1 1000000....

假设0-63只有这3个数字,那么我们存储为10000000000000000000000000000000000000000000000000000000000000110 的二进制数据

这样,我们只需要一个64位(8字节)的数字,就能表示0-63 存在的数了....

比原来的1个数字就得占用8字节,最多可节省63倍的空间!!!!

这种存储方式,就叫做bitmap

php的二进制

那上面提到的,在第n个开关记为1的操作,在php怎么实现呢?我们需要了解一下php的位运算

如下代码

<?php
/**
 * Created by PhpStorm.
 * User: Tioncico
 * Date: 2019/1/15 0015
 * Time: 15:21
 */
$a = 0B0001;
$b = 0B0010;
$c = 0B1010;
$d = 0B0011;
//与运算 &,将2边都为1的数置为1,其他为0
//例如0001&1010=0000,0001&0011=0001
echo decbin($a&$c).PHP_EOL;
echo decbin($a&$d).PHP_EOL;
//或运算 |,将2边有一个为1的数置为1,都为0的则为0
//例如0001|1010=1011
echo decbin($a|$c).PHP_EOL;
//异或运算 ^,将2边一个为0,一个为1的置为1,其他为0
//例如0001^1010=1011
echo decbin($a^$c).PHP_EOL;
// 左移运算 << ,将数字中的位往左移n位
// 例如0010<<2=1000,0010<<1=0100
echo decbin($b<<1).PHP_EOL;
// 右移运算 >> ,将数字中的位往右移n位
// 例如1010>>1=0101 1010>>2=0010
echo decbin($c>>1).PHP_EOL;
echo decbin($c>>2).PHP_EOL;

那如何存储0-63的数字呢?

<?php
/**
 * Created by PhpStorm.
 * User: Tioncico
 * Date: 2019/1/15 0015
 * Time: 15:21
 */
$a = 0B0;
//存储数字3
$num=3;
$num = 1<<($num);//将1往左移3位,1=>1000
$a=$a|$num;//1000
//存储数字5
$num=5;
$num = 1<<($num);//将1往左移5位,1=>100000
$a=$a|$num;//101000
//存储数字14
$num=14;
$num = 1<<($num);//将1往左移14位
$a=$a|$num;//100000000101000
echo decbin($a);

通过这方法,我们即可使用一个数字,记录64个数字的存在

解决题1

php代码(由于20亿数字太大,本人生成200万个数字去进行2种方法内存的比对)

<?php
/**
 * Created by PhpStorm.
 * User: Apple
 * Date: 2019/1/7 0007
 * Time: 16:59
 */
//将一个200万的数组随机打乱写入到文件
$arr = range(1,2000000);
shuffle($arr) ;
foreach ($arr as $value){
    file\_put\_contents('num.txt',$value.PHP\_EOL,FILE\_APPEND);
}

生成文件之后,单文件大小为14540kb

使用常规方法操作数组,需要占用64 M作用的内存(数组需要占用大量内存)

<?php
/**
 * Created by PhpStorm.
 * User: Apple
 * Date: 2019/1/7 0007
 * Time: 16:59
 */
ini\_set('memory\_limit', '1024M');
$arr = \[\];
$filePath = './num.txt';//文件
$fp=fopen($filePath,"r");
while(!feof($fp)){//循环读取文件数据
    $data=fgets($fp);
    $arr\[\]=(int)$data;//转换成int类型,默认char占用更多
    unset($data);
}
fclose($fp);
sort($arr);
//var_dump($arr);
echo "内存使用峰值:",(((memory\_get\_peak\_usage())/1024/1024).'MB'),PHP\_EOL;//内存使用:64.376541137695MB
echo "200w int大约占用:",2000000*8/1024/1024,'MB';

使用bitmap方式存储,只需要占用1.37MB!

<?php
/**
 * Created by PhpStorm.
 * User: Apple
 * Date: 2019/1/7 0007
 * Time: 16:59
 */
$arr = \[\];
$filePath = './num.txt';//文件
$fp=fopen($filePath,"r");
//取得文件大小
$fileSize=filesize($filePath);
while(!feof($fp)){//循环读取文件数据
    $data= fgets($fp);
    //每64个数字存储为一组
    $key = intval($data/64);
    if (isset($arr\[$key\])){
        $arr\[$key\] = $arr\[$key\]| (1<<($data%64));
    }else{
        $arr\[$key\] = 1<<($data%64);
    }
}
sort($arr);
fclose($fp);
echo "内存使用:",(((memory\_get\_usage())/1024/1024).'MB');//内存使用:1.3764343261719MB

可能有人问,那排序呢?你没有排序啊?

由代码和上面的说明可发现,在分别存储数据之后,数组已经算是排好序了

例如$arr[0],代表着0-63的数字范围

$arr[1],代表着64-127的数字范围

不需要进行再次排序了.

bitmap数据处理

我们已经获得了一个排序好的数组,

那么我们给出一个数字,如何判断该数字是否存在呢?

其实上面我们有讲到位运算

举个例子

当有个一组(1-64个数字)组成的int时,它的二进制应该为:

111111111111111111111...1111111

当这数字不存在时,二进制应该有一位为0,例如数字64-127,假设缺少65这个数字,它的二进制表达式为

111111111111111111111111111111111111111111111111111111111111101  (第二位为0)

那怎么判断呢?

与运算方法

$num = 77799999997;
var_dump(checkBitNum($arr, $num));
function checkBitNum($array, $num)
{
//判断是否有这个键名
    $key = intval($num / 64);
//    if (isset($array\[$key\])) {为了测试位运算,先把直接判断注释
    $bitToNum = 1 << ($num % 64);//算出这个数字的二进制位数
    echo "二进制位数为:" . decbin($bitToNum), PHP_EOL;
    //使用与运算
    //如果两边都为1,则置为1,其他则为0
    //$array\[$key\]有这个数字,则$num%64+1的二进制位数为1,与运算$bitToNum,则会等于$bitToNum,如果不为1,则等于0
    $tempNum = $array\[$key\] & $bitToNum;
    echo "计算结果为:" . decbin($tempNum), PHP_EOL;
    if ($tempNum != $bitToNum) {
        return false;
    }
    return true;
//    } else {
//        return false;
//    }
}

另一种方法,或运算方法:

function checkBitNum($array, $num)
{
//判断是否有这个键名
    $key = intval($num / 64);
//    if (isset($array\[$key\])) {为了测试位运算,先把直接判断注释
    $bitToNum = 1 << ($num % 64);//算出这个数字的二进制位数
    echo "二进制位数为:" . decbin($bitToNum), PHP_EOL;
    //使用或运算
    //如果两边有一个为1,则置为1,否则为0
    //$array\[$key\]有这个数字,则$num%64+1的二进制位数为1,或运算$bitToNum($bitToNum该位为1,其他为0),结果将会等于$array\[$key\]
    //如果$array\[$key\]没有这个数字,则或运算之后,$array\[$key\]该位会置为1,结果将不会等于$array\[$key\]
    $tempNum = $array\[$key\] | $bitToNum;
    echo "arr\[key\]为:" . decbin($array\[$key\]), PHP_EOL;
    echo "计算结果为:" . decbin($tempNum), PHP_EOL;
    if ($tempNum != $array\[$key\]) {
        return false;
    }
    return true;
//    } else {
//        return false;
//    }
}

这样,我们就已经使用bitmap,存储空间小,速度快,解决了大量简单数据下的排序,判断

解决用户签到问题

在前面,我们已经通过了bitmap,解决了大数据存储排序问题,那么问题二的签到问题怎么办呢?

很简单,我们继续使用bitmap即可,

通过bitmap的特性,我们可以如下实现:

<?php
/**
 * Created by PhpStorm.
 * User: Apple
 * Date: 2019/1/7 0007
 * Time: 16:59
 */
$data = \[\];
$data = saveSign($data, '01-01');
$data = saveSign($data, '01-02');
$data = saveSign($data, '01-03');
$data = saveSign($data, '01-05');
$data = saveSign($data, '01-30');
$data = saveSign($data, '01-29');
$data = saveSign($data, '01-31');
foreach ($data as$key=> $value){
    echo "{$key}月份签到情况:".decbin($value);//输出11100000000000000000000000101110
    //从右往左看,每一位代表一天,1代表已签到
}
function saveSign($data, $date)
{
    //以月份为最小单位存储
    $dateArr = explode('-',$date);
    $key = $dateArr\[0\];
    if (isset($data\[$key\])){
        $data\[$key\] = $data\[$key\]| (1<<((int)$dateArr\[1\]%64));
    }else{
        $data\[$key\] = 1<<((int)$dateArr\[1\]%64);
    }
    return $data;
}

计算方法和上面一样,只需要通过与或预算就能清楚签到情况

对比如下:

数据库直接存储:1亿用户,假设每天有3000万人签到,那么一个月,一年下来,数据将会是:30000000*365差不多是100亿的数据...

数据库存储bitmap:1亿用户,假设每天有3000万人签到,如果按月份储存bitmap,一年下来,数据将会是:30000000*12只有3亿

bitmap缺点

前面说这么多,那bitmap有缺点吗?当然有

1:bitmap不能存储多状态情况,bitmap只有0和1 两个状态,无法做多状态的存储

2:bitmap不能存储重复数据,bitmap是通过不同的位数,代表不同的数据和不同的状态,不能通过bitmap存储重复的数据

3:bitmap不能做非运算,什么叫非运算呢?和缺点1类似,由于bitmap只有0和1两种状态,当你要查询0状态时,由于int 的位数问题,你可能会查出不在限定位数的0,例如上面的签到数据,当你要取出所有未签到天数时,可能会取出当月32-63号的数据,当然你可自己判断

4:bitmap受int位数限制,在32位机器上,int只有32位4个字节,所以你一个int数据只能最多存储32条数据

其他

关于第二个签到问题补充

在很多情况下,我们可能需要的是记录某一天有多少个会员的签到情况,而不是某个会员的签到情况,那这个能用bitmap吗?

当然可以,我们需要采用会员id横向存储,例如:

<?php
/**
 * Created by PhpStorm.
 * User: Apple
 * Date: 2019/1/7 0007
 * Time: 16:59
 */
$data = \[\];
$data = saveSign($data, 1);
$data = saveSign($data, 2);
$data = saveSign($data, 63);
$data = saveSign($data, 4);
$data = saveSign($data, 117);
$data = saveSign($data, 98);
$data = saveSign($data, 654);
$data = saveSign($data, 235);
foreach ($data as$key=> $value){
    echo "今天第{$key}批会员签到情况:".decbin($value).PHP_EOL;//输出11100000000000000000000000101110
    //从右往左看,每一位代表一天,1代表已签到
}
function saveSign($data, $userId)
{
    $key = intval($userId / 64);
    if (isset($data\[$key\])) {
        $data\[$key\] = $data\[$key\] | (1 << ($userId % 64));
    } else {
        $data\[$key\] = 1 << ($userId % 64);
    }
    return $data;
}

我们以天为单位,把签到的会员id存入数据中,数据库只需要查询某一天的bitmap数据,就能得到某一天的所有会员签到情况

相关实践学习
基于MaxCompute的热门话题分析
本实验围绕社交用户发布的文章做了详尽的分析,通过分析能得到用户群体年龄分布,性别分布,地理位置分布,以及热门话题的热度。
SaaS 模式云数据仓库必修课
本课程由阿里云开发者社区和阿里云大数据团队共同出品,是SaaS模式云原生数据仓库领导者MaxCompute核心课程。本课程由阿里云资深产品和技术专家们从概念到方法,从场景到实践,体系化的将阿里巴巴飞天大数据平台10多年的经过验证的方法与实践深入浅出的讲给开发者们。帮助大数据开发者快速了解并掌握SaaS模式的云原生的数据仓库,助力开发者学习了解先进的技术栈,并能在实际业务中敏捷的进行大数据分析,赋能企业业务。 通过本课程可以了解SaaS模式云原生数据仓库领导者MaxCompute核心功能及典型适用场景,可应用MaxCompute实现数仓搭建,快速进行大数据分析。适合大数据工程师、大数据分析师 大量数据需要处理、存储和管理,需要搭建数据仓库?学它! 没有足够人员和经验来运维大数据平台,不想自建IDC买机器,需要免运维的大数据平台?会SQL就等于会大数据?学它! 想知道大数据用得对不对,想用更少的钱得到持续演进的数仓能力?获得极致弹性的计算资源和更好的性能,以及持续保护数据安全的生产环境?学它! 想要获得灵活的分析能力,快速洞察数据规律特征?想要兼得数据湖的灵活性与数据仓库的成长性?学它! 出品人:阿里云大数据产品及研发团队专家 产品 MaxCompute 官网 https://www.aliyun.com/product/odps&nbsp;
目录
相关文章
|
3月前
|
存储 算法 数据挖掘
【2023年中国高校大数据挑战赛 】赛题 B DNA 存储中的序列聚类与比对 Python实现
本文介绍了2023年中国高校大数据挑战赛赛题B的Python实现方法,该赛题涉及DNA存储技术中的序列聚类与比对问题,包括错误率分析、序列聚类、拷贝数分布图的绘制以及比对模型的开发。
82 1
【2023年中国高校大数据挑战赛 】赛题 B DNA 存储中的序列聚类与比对 Python实现
|
1月前
|
存储 消息中间件 大数据
大数据-69 Kafka 高级特性 物理存储 实机查看分析 日志存储一篇详解
大数据-69 Kafka 高级特性 物理存储 实机查看分析 日志存储一篇详解
36 4
|
1月前
|
消息中间件 存储 缓存
大数据-71 Kafka 高级特性 物理存储 磁盘存储特性 如零拷贝、页缓存、mmp、sendfile
大数据-71 Kafka 高级特性 物理存储 磁盘存储特性 如零拷贝、页缓存、mmp、sendfile
53 3
|
1月前
|
存储 消息中间件 大数据
大数据-70 Kafka 高级特性 物理存储 日志存储 日志清理: 日志删除与日志压缩
大数据-70 Kafka 高级特性 物理存储 日志存储 日志清理: 日志删除与日志压缩
41 1
|
1月前
|
存储 消息中间件 大数据
大数据-68 Kafka 高级特性 物理存储 日志存储概述
大数据-68 Kafka 高级特性 物理存储 日志存储概述
26 1
|
1月前
|
消息中间件 分布式计算 NoSQL
大数据-41 Redis 类型集合(2) bitmap位操作 geohash空间计算 stream持久化消息队列 Z阶曲线 Base32编码
大数据-41 Redis 类型集合(2) bitmap位操作 geohash空间计算 stream持久化消息队列 Z阶曲线 Base32编码
27 2
|
1月前
|
存储 算法 NoSQL
大数据-138 - ClickHouse 集群 表引擎详解3 - MergeTree 存储结构 数据标记 分区 索引 标记 压缩协同
大数据-138 - ClickHouse 集群 表引擎详解3 - MergeTree 存储结构 数据标记 分区 索引 标记 压缩协同
34 0
|
1月前
|
存储 消息中间件 分布式计算
大数据-137 - ClickHouse 集群 表引擎详解2 - MergeTree 存储结构 一级索引 跳数索引
大数据-137 - ClickHouse 集群 表引擎详解2 - MergeTree 存储结构 一级索引 跳数索引
35 0
|
1月前
|
存储 SQL 分布式计算
大数据-127 - Flink State 04篇 状态原理和原理剖析:状态存储 Part2
大数据-127 - Flink State 04篇 状态原理和原理剖析:状态存储 Part2
20 0
|
1月前
|
存储 消息中间件 大数据
大数据-126 - Flink State 03篇 状态原理和原理剖析:状态存储 Part1
大数据-126 - Flink State 03篇 状态原理和原理剖析:状态存储 Part1
59 0
下一篇
无影云桌面