PHP使用Redis的BitMap特性实现签到功能
一、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天签到数据:28Byte
- 1000W用户1天签到数据:28Byte * 1000w = 268M
- 1000W用户1年签到数据:268M * 365 = 95.6G
- 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占用空间
- 31位需要3Byte + key(1Byte)
- 一个用户一个月签到数据:4Byte
- 1000W用户1个月签到数据: 40M
- 1000W用户1年签到数据:40M * 12 = 480M
- 1000W用户5年签到数据:480M * 5 = 2G
三、命令行实现BitMap签到
签到
- 用户id为123的用户2021年3月1号签到
$ setBit sign:123:202103 0 1
- 用户id为123的用户2021年3月2号签到
$ setBit sign:123:202103 1 1
获取签到
-
查看用户id为123的用户2021年3月1号签到
$ getBit sign:123:202103 0
-
查看用户id为123的用户2021年3月2号签到
$ getBit sign:123:202103 1
-
查看用户id为123的用户2021年3月
首次签到
$ bitPos sign:123:202103 1
-
查看用户id为123的用户2021年3月
首次漏签
$ bitPos sign:123:202103 0
-
查看用户指定范围内的签到 返回的是十进制
bitfield sign:123:202103 get u3 6
统计签到
- 获取用户当月签到次数
$ bitCount sign:123:202103
四、PHP实现BitMap签到
签到
-
用户id为123的用户2021年3月8号签到
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $key = 'sign:123:202103'; $day = 8; $redis->setBit($key, $day, 1); ?>
-
用户id为123的用户2021年3月10号签到
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $key = 'sign:123:202103'; $day = 10; $redis->setBit($key, $day, 1); ?>
查看签到
-
查看用户id为123的用户2021年3月10号签到
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $key = 'sign:123:202103'; $day = 8; $sign = $redis->getBit($key, $day); var_dump($sign); ?>
-
查看用户id为123的用户2021年3月份的签到情况(
bitmap
实际就是string
类型,使用Get
)<?php $redis = new Redis(); $redis->connect('127.0.0.1', 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); } ?>
-
查看用户id为123的用户2021年3月
首次签到
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $key = 'sign:123:202103'; $day = $redis->bitPos($key,1); var_dump($day); ?>
- 查看用户id为123的用户2021年3月
首次漏签
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $key = 'sign:123:202103'; $day = $redis->bitPos($key,0); var_dump($day); ?>
- 查看用户id为123的用户2021年3月
统计签到
-
查看用户id为123的用户2021年3月份签到次数
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $key = 'sign:123:202103'; $count = $redis->bitcount($key,0,-1); var_dump($count); ?>
-
查看用户id为123的用户2021年3月8号连续签到次数
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 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使用。