• 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。

    Redis提供了以下几个指令用于操作BitMap:

    命令 说明 可用版本 时间复杂度
    SETBIT 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。 >= 2.2.0 O(1)
    GETBIT 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。 >= 2.2.0 O(1)
    BITCOUNT 计算给定字符串中,被设置为 1 的比特位的数量。 >= 2.6.0 O(N)
    BITPOS 返回位图中第一个值为 bit 的二进制位的位置。 >= 2.8.7 O(N)
    BITOP 对一个或多个保存二进制位的字符串 key 进行位元操作。 >= 2.6.0 O(N)
    BITFIELD BITFIELD 命令可以在一次调用中同时对多个位范围进行操作。 >= 3.2.0 O(1)

    常用的几个方法:
    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。存储成本是不是很低。

    再例如:考虑到每月要重置连续签到次数,最简单的方式是按用户每月存一条签到数据。Key的格式为 u:sign:{uid}:{yyyMM},而Value则采用长度为4个字节的(32位)的BitMap(最大月份只有31天)。BitMap的每一位代表一天的签到,1表示已签,0表示未签。

    例如 u:sign:1225:202101 表示ID=1225的用户在2021年1月的签到记录

    # 用户1月6号签到
    SETBIT u:sign:1225:202101 5 1 # 偏移量是从0开始,所以要把6减1
    
    # 检查1月6号是否签到
    GETBIT u:sign:1225:202101 5 # 偏移量是从0开始,所以要把6减1
    
    # 统计1月份的签到次数
    BITCOUNT u:sign:1225:202101
    
    # 获取1月份前31天的签到数据
    BITFIELD u:sign:1225:202101 get u31 0
    
    # 获取1月份首次签到的日期
    BITPOS u:sign:1225:202101 1 # 返回的首次签到的偏移量,加上1即为当月的某一天

    上面都需要增加过期时间,redisTemplate.expireAt(key, ttl)

    使用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/)


    原文链接:https://blog.csdn.net/u011957758/article/details/74783347

    https://blog.csdn.net/Ainanaya/article/details/115324205

  • 相关阅读:
    DTS和AC3的区别
    bind出现 file does not end with newline错误
    删除桌面菜单多余项
    you have requested a nonexistent service "grid"
    php 常用函数
    Jquery
    Twig 的Filters学习
    Twig 的 tags学习(中文) 之三 完结
    PHP 正则表达式
    SQL处理字符串
  • 原文地址:https://www.cnblogs.com/duanxz/p/14700995.html
Copyright © 2020-2023  润新知