前言

这是一篇拖了很久的总结,项目中引入了redis的bitmap的用法,感觉挺高大上的,刨根问底,故留下总结一篇当作纪念。
说清楚几个问题:
1.bitmap的原理、用法。
2.bitmap的优势、限制。
3.bitmap空间、时间粗略计算方式。
4.bitmap的使用场景。
5.使用bitmap过程中可能会遇到的坑。
6.bitmap进阶用法(思考)。

bitmap的原理、用法

原理

8bit = 1b = 0.001kb

bitmap就是通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。
一个bit的值,或者是0,或者是1;也就是说一个bit能存储的最多信息是2。

用法

setBit

说明:给一个指定key的值得第offset位 赋值为value。

参数:key offset value: bool or int (1 or 0)

返回值:LONG: 0 or 1

getBit

说明:返回一个指定key的二进制信息

参数:key offset

返回值:LONG

bitCount

说明:返回一个指定key中位的值为1的个数(是以byte为单位不是bit)

参数:key start offset

返回值:LONG

bitOp

说明:对不同的二进制存储数据进行位运算(AND、OR、NOT、XOR)

参数:operation destkey key [key …]

返回值:LONG

bitmap的优势、限制

优势

1.基于最小的单位bit进行存储,所以非常省空间。
2.设置时候时间复杂度O(1)、读取时候时间复杂度O(n),操作是非常快的。
3.二进制数据的存储,进行相关计算的时候非常快。
4.方便扩容

限制

redis中bit映射被限制在512MB之内,所以最大是2^32位。建议每个key的位数都控制下,因为读取时候时间复杂度O(n),越大的串读的时间花销越多。

bitmap空间、时间粗略计算方式

在一台2010MacBook Pro上,offset为232-1(分配512MB)需要~300ms,offset为230-1(分配128MB)需要~80ms,offset为228-1(分配32MB)需要~30ms,offset为226-1(分配8MB)需要8ms。<来自官方文档>

大概的空间占用计算公式是:($offset/8/1024/1024)MB

bitmap的使用场景

使用方式很多,根据不同的业务需求来,但是总的来说就两种,以用户为例子:

1.一种是某一用户的横向扩展,即此个key值中记录这当前用户的各种状态值,允许无限扩展(2^32内)

点评:这种用法基本上是很少用的,因为每个key携带uid信息,如果存储的key的空间大于value,从空间角度看有一定的优化空间,如果是记录长尾的则可以考虑。

2.一种是某一用户的纵向扩展,即每个key只记录当前业务属性的状态,每个uid当作bit位来记录信息(用户超过2^32内需要分片存储)

点评:基本上项目使用的场景都是基于这种方式的,按业务区分方便回收资源,key值就一个,将uid的存储转为了位的存储,十分巧妙的通过uid即可找到相应的值,主要存储量在value上,符合预期。

1.视频属性的无限延伸

需求分析:

一个拥有亿级数据量的短视频app,视频存在各种属性(是否加锁、是否特效等等),需要做各种标记。

可能想到的解决方案:

1.存储在mysql中,肯定不行,一个是随着业务增长属性一直增加,并且存在有时间限制的属性,直接对数据库进行加减字段是非常不合理的做法。即使是存在一个字段中用json等压缩技术存储也存在读效率的问题,并且对于大几亿的数据来说,废弃的字段回收起来非常麻烦。

2.直接记录在redis中,根据业务属性+uid为key来存储。读写效率角度没毛病,但是存储的角度来说key的数据量都大于value了,太耗费空间了。即使是用json等压缩技术来存储。也存在问题,解压需要时间,并且大几亿的数据回收也是难题。

设计方案:

使用redis的bitmap进行存储。
key由属性id+视频分片id组成。value按照视频id对分片范围取模来决定偏移量offset。10亿视频一个属性约120m还是挺划算的。

伪代码:

function set($business_id , $media_id , $switch_status=1){$switch_status = $switch_status ? 1 : 0;$key = $this->_getKey($business_id, $media_id);$offset = $this->_getOffset($media_id);return $this->redis->setBit($key, $offse, $switch_status);
}function get($business_id , $media_id){$key = $this->_getKey($business_id,$media_id);$offset = $this->_getOffset($media_id);return $this->redis->getBit($key , $offset);
}function _getKey($business_id, $media_id){return 'm:'.$business_id.':'.intval($media_id/10000);
}function _getOffset($media_id){return $media_id % 10000;
}

这样基本实现了属性的存储,后续增加新属性也只是business_id再增加一个值。

至于为什么分片呢?分片的粒度怎么衡量?

分片有两个原因:1.读取的时候时间复杂度是O(n)存储越长读取时间越多 2.bitmap有长度限制2^32。

分片粒度怎么衡量:1.如果主键id存在的断层那么请尽可能选择的粒度可以避开此段id范围,防止空间浪费,因为来一个00000…9999个0…01,那么因为存一个属性而存了全部的,就浪费了。2.分片粒度可参考某一单位时间的增长值来判断,这样也有利于预算占了多少空间,虽然空间不会占很多。

2.用户在线状态

需求分析:

需要对子项目提供一个接口,来提供某用户是否在线?

设计方案:

使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户id为偏移量offset,如果在线就设置为1,不在线就设置为0,3亿用户只需要36MB的空间。

伪代码:

$status = 1;
$redis->setBit('online', $uid, $status);
$redis->getBit('online', $uid);

需要加上如例子1一样分片的方式。10亿真的太多了。10w分一片。

3.统计活跃用户

需求分析:

需要计算活跃用户的数据情况。

设计方案:

使用时间作为缓存的key,然后用户id为offset,如果当日活跃过就设置为1。之后通过bitOp进行二进制计算算出在某段时间内用户的活跃情况。

伪代码:

$status = 1;
$redis->setBit('active_20170708', $uid, $status);
$redis->setBit('active_20170709', $uid, $status);$redis->bitOp('AND', 'active', 'active_20170708', 'active_20170709');

上亿用户需要加上如例子1一样分片的方式。几十万或者以下,可无需分片省的业务变复杂。

4.用户签到

需求分析:

用户需要进行签到,对于签到的数据需要进行分析与相应的运运营策略。

设计方案:

使用redis的bitmap,由于是长尾的记录,所以key主要由uid组成,设定一个初始时间,往后没加一天即对应value中的offset的位置。

伪代码:

$start_date = '20170708';
$end_date = '20170709';
$offset = floor((strtotime($start_date) - strtotime($end_date)) / 86400);
$redis->setBit('sign_123456', $offset, 1);//算活跃天数
$redis->bitCount('sign_123456', 0, -1)

无需分片,一年365天,3亿用户约占300000000*365/8/1000/1000/1000=13.68g。存储成本是不是很低。

使用bitmap过程中可能会遇到的坑

1.bitcout的陷阱

如果你有仔细看前文的用法,会发现有这么一个备注“返回一个指定key中位的值为1的个数(是以byte为单位不是bit)”,这就是坑的所在。

有图有真相:

所以bitcount 0 0 那么就应该是第一个字节中1的数量的,注意是字节,第一个字节也就是1,2,3,4,5,6,7,8这八个位置上。

bitmap进阶用法(思考)

以下内容来自此文的笔记:http://www.infoq.com/cn/articles/the-secret-of-bitmap/

1.空间
redis的bitmap已经是最小单位的存储了,有没有办法对二进制存储的信息再进行压缩呢?进一步省空间?

答案是有的。
可以对记录的二进制数据进行压缩。常见的二进制压缩技术都是基于RLE(Run Length Encoding,详见http://en.wikipedia.org/wiki/Run-length_encoding)。

RLE编码很简单,比较适合有很多连续字符的数据,比如以下边的Bitmap为例:

可以编码为0,8,2,11,1,2,3,11

其意思是:第一位为0,连续有8个,接下来是2个1,11个0,1个1,2个0,3个1,最后是11个0(当然此处只是对RLE的基本原理解释,实际应用中的编码并不完全是这样的)。

可以预见,对于一个很大的Bitmap,如果里边的数据分布很稀疏(说明有很多大片连续的0),采用RLE编码后,占用的空间会比原始的Bitmap小很多。

2.时间

redis虽然是在内存操作,但是超过redis指定存储在内存的阀值之后,会被搞到磁盘中。要是进行大范围的计算还需要从磁盘中取出到内存在计算比较耗时,效率也不高,有没有办法尽可能内存中多放一些数据,缩短时间?

答案是有的。

基于第一点同时引入一些对齐的技术,可以让采用RLE编码的Bitmap不需要进行解压缩,就可以直接进行AND/OR/XOR等各类计算;因此采用这类压缩技术的Bitmap,加载到内存后还是以压缩的方式存在,从而可以保证计算时候的低内存消耗;而采用word(计算机的字长,64位系统就是64bit)对齐等技术又保证了对CPU资源的高效利用。因此采用这类压缩技术的Bitmap,保持了Bitmap数据结构最重要的一个特性,就是高效的针对每个bit的逻辑运算。

常见的压缩技术包括BBC(有专利保护,WAH(http://code.google.com/p/compressedbitset/)和EWAH(http://code.google.com/p/javaewah/)

扩展阅读

Bitmap的秘密
Redis内存压缩实战
Redis中bitmap的妙用
Redis中BitMap是如何储存的,以及PHP如何处理
使用redis的setbit和bitcount来进行区间统计的坑

如果你觉得有收获~可以关注我的公众号【咖啡色的羊驼】~第一时间收到我的分享和知识梳理~

一看就懂系列之 详解redis的bitmap在亿级项目中的应用相关推荐

  1. 一看就懂的CE-NET详解

    一看就懂的CE-NET详解 针对问题 u-net及其变体存在限制,连续的pooling和交错的卷积运算会导致一些空间信息的丢失. 文章贡献 提出了一个DAC模块和一个RMP模块来捕获更多的高级特征并保 ...

  2. 一看就懂 详解redis的bitmap(面试加分项)

    在上文<面试杀手锏:Redis源码之SDS>中我们深入分析了 SDS 的实现,本次介绍的位图(BitMap)就是借助 SDS 实现的. 本文在最后讲解了BitMap对腾讯面试题的解决方案, ...

  3. 肝进ICU,万字真言点化八大排序——我奶奶都看得懂的算法详解

    目录 传统艺能

  4. 大型企业网络配置系列课程详解(五) --Frame-Relay配置与相关概念的理解

    大型企业网络配置系列课程详解(五)             --Frame-Relay配置与相关概念的理解   实验原理: Frame-Relay(帧中继)简称FR,是国际电信联盟通信标准化组(ITU ...

  5. 大型企业网络配置系列课程详解(三)--OSPF高级配置与相关概念的理解

    大型企业网络配置系列课程详解(三)<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office&qu ...

  6. 旋转排序数组系列题详解

    旋转排序数组系列题详解 文章目录 旋转排序数组系列题详解 一.问题描述:旋转数组的最小数字 二.分析:二分查找 三.代码 四.问题描述:寻找旋转排序数组中的最小值 五.分析:二分搜索 六.代码 七.问 ...

  7. 大型企业网络配置系列课程详解(四) --HSRP和VRRP配置与相关概念的理解(一)...

    大型企业网络配置系列课程详解(四)<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office&qu ...

  8. Windows 网络服务架构系列课程详解(一) ----DHCP服务器的搭建与配置

    Windows 网络服务架构系列课程详解(一) ---------DHCP服务器的搭建与配置   实验背景: 企业网络环境中在没有配置DHCP服务器时,经常会遇到这样的情况,用户不懂怎么去配置IP地址 ...

  9. 大型企业网络配置系列课程详解(八) --VoIP的配置与相关概念的理解

    大型企业网络配置系列课程详解(八) --VoIP的配置与相关概念的理解   试验背景: 伴随着电路交换网络的发展,PSTN(公共交换电话网络)的普及已经给人类带来了前所未有的方便,在现实生活中只要有P ...

最新文章

  1. samli文件_5.3 smali文件格式
  2. Hibernate各种主键生成策略与配置详解 - 真的很详细啊!!
  3. 水鱼 学习回顾 <1>
  4. 程序时序图 Sequence Diagram(序列图) UML图(数据流程图)的绘制
  5. [CF475E]Strongly Connected City 2
  6. Free tour II SPOJ - FTOUR2 点分治 + 树状数组
  7. python具体应用过程_python公开课|Python for循环的具体应用就是python流程控制的核心,想学会就来看看...
  8. 关于ubuntu 是否需要使用std::到问题。
  9. C语言访问存储器的方法
  10. 深度学习(7) - 长短时记忆网络(LSTM)
  11. 真实版“删库跑路”?程序员蓄意破坏线上生产环境!
  12. 2008-2011大股东增持专题
  13. 基于marlin固件的SCARA机器人
  14. 小程序开发入门常见小问题-(1)
  15. 心脏出血漏洞修复记录
  16. 熊孩子乱敲键盘攻破Linux,“熊孩子”乱敲键盘就攻破了Linux桌面,大神:17年前我就警告过你们...
  17. 基于SpringBoot旅游信息管理系统网站
  18. 三国志战略版:Daniel_袁术分析
  19. Memoires 5.0.0 日记本工具
  20. C/C++之cstring头文件

热门文章

  1. 300元蓝牙无线耳机性价比之王,盘点三百元内最好的蓝牙耳机
  2. pytorch中的sum
  3. python sum函数导入list_python sum函数怎么用
  4. C++之面向对象(下)
  5. html更改链接颜色,HTML超链接的颜色怎么改
  6. 【忏悔的博客】2020普及组三校联考(西附)
  7. https://developer.apple.com 苹果开发者
  8. 震坤行面试题java_10个经典的Java面试题
  9. 面经:本5 服务端 6 轮面试阿里
  10. 如何发现新的潜力项目?工具推荐