Redis详解(八)bitmap

1. 什么是 bitmap

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

  • 位(bit):是计算机 内部数据储存的最小单位,11001100是一个八位二进制数。
  • 字节(byte):是计算机中数据处理的基本单位,习惯上用大写 B 来表示,1B(byte,字节)= 8bit

2. 重要 API

命令含义
getbit key offset用于获取Redis中指定key对应的值,中对应offset的bit
setbit key key offset value用于修改指定key对应的值,中对应offset的bit
bitcount key [start end]获取位图指定范围中位值为1的个数,如果不指定start与end,则取所有
bitop op destKey key1 [key2...]做多个BitMap的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destKey中
bitpos key tartgetBit [start end]计算位图指定范围第一个偏移量对应的的值等于targetBit的位置
1. 找不到返回-1
2. start与end没有设置,则取全部
3. targetBit只能取0或者1

3. bitmap的优点和限制

3.1 优点

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

3.2 限制

bitmap的value实际是一个string结构。Redis中string value限制在512MB之内,所以最大是2^32位。建议每个key的位数都控制下,因为读取时候时间复杂度O(n),越大的串读的时间花销越多。

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

在一台2010 MacBook 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。

4. 使用场景

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

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

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

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

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

4.1 用户在线状态

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

当数据量大时key需要进行分片处理,大概10w用户分为一个key,减少位移耗费时间。

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

4.2 统计活跃用户

使用时间作为缓存的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'); 

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

4.3 用户签到

使用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。存储成本是不是很低。

4.4 布隆过滤器

布隆过滤器主要是用于应对缓存穿透问题。

比如爬虫服务器在爬取电商网站的商品信息时,首先经过缓存,如果缓存查不到,再去数据库获取信息,因为爬虫的效率很高,且sku很有可能是不存在或者已下架的,就会造成缓存穿透,大量请求被发送到数据库,导致服务器受到影响。

此时,可以在缓存层之前,添加一个布隆过滤器,布隆 过滤器看作是一个bitmap,sku作为offset值,如果商品真实存在,bit值设为1。首先将商品数据初始化,当有请求时,通过getbit判断sku是否有效。如果布隆过滤器认为商品不存在,就拒绝访问,这样就可以保护存储层。

4.5 视频属性的无限延伸

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

我们可以使用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. 分片粒度可参考某一单位时间的增长值来判断,这样也有利于预算占了多少空间,虽然空间不会占很多。

5. 可能会遇到的坑

5.1 bitcout的陷阱

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

这里写图片描述

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

6. bitmap进阶用法

6.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小很多。

6.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/)

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值