一、BitMap

BitMap原理

bitMap 位图并不是一种数据结构,其实也就是 byte 数组,用二进制表示,只有 0 和 1 两个数字。基于string数据类型的按位操作符,高阶数据类型的一种。

bitMap支持的最大位数是2^32位,使用512M内存就可以存储多达42.9亿的字节信息。

BitMap操作命令

  • gitbit
    • 用法:getbit key offset
    • 含义:对key所存储的字符串值,获取指定偏移量上的位(bit)
  • setbit
    • 用法:setbit key offset value
    • 含义:对key所存储的字符串值,设置或清除指定偏移量上的位(bit)
      1. 返回值为该位在setbit之前的值
      2. value只能取0或1
      3. offset从0开始,即使原位图只能10位,offset可以取1000
  • bitcount
    • 用法:bitcount key [start end]
    • 含义:获取位图指定范围中位值为1的个数。如果不指定start与end,则取所有
  • bitop
    • 用法:bitop op destKey key1 [key2…]
    • 含义:做多个BitMap的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destKey中
  • bitpos
    • 用法:bitpos key tartgetBit [start end]
    • 含义:计算位图指定范围第一个偏移量对应的的值等于targetBit的位置
      1. 找不到返回-1
      2. start与end没有设置,则取全部
      3. targetBit只能取0或者1
  • BITFIELD
    • 用法:BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
    • 含义:自3.2.0起可用。该命令将 Redis 字符串视为一个位数组,并且能够处理具有不同位宽和任意非(必要)对齐偏移量的特定整数字段

二、BitMap对比MySql

mysql存储

  • mysql表结构

    字段 描述 类型 存储需求
    id 数据表主键 bigInt 8byte
    user_id 用户id bigInt 8byte
    sign_date 签到日期 DATETIME 8btye
    amount 连续签到 int 4byte
  • mysql占用存储空间

    1. 1个用户1天签到数据:28Byte
    2. 1000W用户1天签到数据:28Byte * 1000w = 268M
    3. 1000W用户1年签到数据:268M * 365 = 95.6G
    4. 1000W用户5年签到数据:95.6G * 5 = 478G

redis BitMap存储

  • redis BitMap存储

    key value
    user_id_2021_04 0 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0…(签到最多31天)
  • redis BitMap占用空间

    1. 31位需要3Byte + key(1Byte)
    2. 一个用户一个月签到数据:4Byte
    3. 1000W用户1个月签到数据: 40M
    4. 1000W用户1年签到数据:40M * 12 = 480M
    5. 1000W用户5年签到数据:480M * 5 = 2G

三、命令行实现BitMap签到

签到

  1. 用户id为123的用户2021年3月1号签到

    $ setBit sign:123:202103 0 1
  2. 用户id为123的用户2021年3月2号签到

    $ setBit sign:123:202103 1 1

获取签到

  1. 查看用户id为123的用户2021年3月1号签到

    $ getBit sign:123:202103 0
  2. 查看用户id为123的用户2021年3月2号签到

    $ getBit sign:123:202103 1
  3. 查看用户id为123的用户2021年3月 首次签到

    $ bitPos sign:123:202103 1
  4. 查看用户id为123的用户2021年3月 首次漏签

    $ bitPos sign:123:202103 0
  5. 查看用户指定范围内的签到 返回的是十进制

    bitfield sign:123:202103 get u3 6

统计签到

  • 获取用户当月签到次数

    $ bitCount sign:123:202103

四、PHP实现BitMap签到

签到

  1. 用户id为123的用户2021年3月8号签到

    <?php
    
        $redis = new Redis();
        $redis->connect('11', 6379);
        $key = 'sign:123:202103';
        $day = 8;
        $redis->setBit($key, $day, 1);
    ?>
  2. 用户id为123的用户2021年3月10号签到

    <?php
    
        $redis = new Redis();
        $redis->connect('11', 6379);
        $key = 'sign:123:202103';
        $day = 10;
        $redis->setBit($key, $day, 1);
    ?>

查看签到

  1. 查看用户id为123的用户2021年3月10号签到

    <?php
    
        $redis = new Redis();
        $redis->connect('11', 6379);
        $key = 'sign:123:202103';
        $day = 8;
        $sign = $redis->getBit($key, $day);
        var_dump($sign);
    ?>
  2. 查看用户id为123的用户2021年3月份的签到情况( bitmap 实际就是 string 类型,使用 Get)

    <?php
    
        $redis = new Redis();
        $redis->connect('11', 6379);
        $key = 'sign:123:202103';
        $sign = $redis->get($key); //这里查出来的是二进制字符串
        $bitmap_bin_str = StrToBin($sign);
        for($i = 1; $i <= 31; $i++) {
            echo $i.'号签到'.$bitmap_bin_str{$i-1}.PHP_EOL;
        }
        # 二进制字符串转字符
        function StrToBin($str){
            $arr = preg_split('/(?<!^)(?!$)/u', $str);
            foreach($arr as &$v){
                $temp = unpack('H*', $v); $v = base_convert($temp[1], 16, 2);
                unset($temp);
            }
            return join(' ',$arr);
        }
    ?>
  3. 查看用户id为123的用户2021年3月 首次签到

    <?php
    
    $redis = new Redis();
    $redis->connect('11', 6379);
    $key = 'sign:123:202103';
    $day = $redis->bitPos($key,1);
    var_dump($day);
    ?>
    1. 查看用户id为123的用户2021年3月 首次漏签
    <?php
    
    $redis = new Redis();
    $redis->connect('11', 6379);
    $key = 'sign:123:202103';
    $day = $redis->bitPos($key,0);
    var_dump($day);
    ?>

统计签到

  1. 查看用户id为123的用户2021年3月份签到次数

    <?php
    
        $redis = new Redis();
        $redis->connect('11', 6379);
        $key = 'sign:123:202103';
        $count = $redis->bitcount($key,0,-1);
        var_dump($count);
    ?>
  2. 查看用户id为123的用户2021年3月8号连续签到次数

    <?php
    
        $redis = new Redis();
        $redis->connect('11', 6379);
        $key = 'sign:123:202103';
        $str = '1111001110000000000000000000000'; //用户当月签到情况
        $day = 8;
        if ($str{$day-1} == '0') {
            var_dump(0);
            exit;
        }
        $count = 1;
        for ($i=$day; $i>=0; $i--) {
            // var_dump($i-1);
            $sign = ($str{$i-1}) & ($str{$i-2});
            if ($sign == 1) {
                $count += 1;
            } else {
                break;
            }
        }
        var_dump($count);
    ?>

五、使用经验和资料参考

使用经验

  • BitMap 是 sting 类型,最大 512 MB
  • 注意 setbit 时的偏移量,可能有较大耗时
  • 位图不是绝对好,获取不到签到的准确时间,只能判断指定日期有没有签到。
  • redis配合mysql使用,当月数据存redis,上月数据同步到mysql(一个月数据的签到存一条mysql数据)。
  • 签到日志可以配合nongoDB使用。

资料参考