Bitmap(即Bitset)
Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),bitmap就是通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。
Redis从2.2.0版本开始新增了setbit
,getbit
,bitcount
等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit
等命令只不过是在set
上的扩展。在bitmap上可执行AND,OR,XOR以及其它位操作。
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的个数
参数:[key, start ,offset] (是以byte为单位不是bit)
返回值:LONG
bitOp
说明:对不同的二进制存储数据进行位运算(AND、OR、NOT、XOR)
参数:operation destkey key [key …]
返回值:LONG
优势
1.基于最小的单位bit进行存储,所以非常省空间。
2.设置时候时间复杂度O(1)、读取时候时间复杂度O(n),操作是非常快的。
3.二进制数据的存储,进行相关计算的时候非常快。
4.方便扩容
限制
redis中bit映射被限制在512MB之内,所以最大是2^32位。建议每个key的位数都控制下,因为读取时候时间复杂度O(n),越大的串读的时间花销越多。
在一台2010MacBook Pro上,offset为2^32-1(分配512MB)需要~300ms,offset为2^30-1(分配128MB)需要~80ms,offset为2^28-1(分配32MB)需要~30ms,offset为2^26-1(分配8MB)需要8ms。<来自官方文档>
大概的空间占用计算公式是:($offset/8/1024/1024)MB
一个简单的例子:日活跃用户
为了统计今日登录的用户数,我们建立了一个bitmap,每一位标识一个用户ID。当某个用户访问我们的网页或执行了某个操作,就在bitmap中把标识此用户的位置为1。在Redis中获取此bitmap的key值是通过用户执行操作的类型和时间戳获得的。
1011 1101 | 0010 0101 |
这个简单的例子中,每次用户登录时会执行一次redis.setbit(daily_active_users, user_id, 1)。将bitmap中对应位置的位置为1,时间复杂度是O(1)。统计bitmap结果显示有今天有9个用户登录。Bitmap的key是daily_active_users,它的值是1011110100100101。
我以前一直以为是从后往前数15-8 | 7-0,跟字节一样,后来研究发现是 实际上顺序从前往后排 0 -7| 8 - 15
127.0.0.1:6379> set daily_active_users "\x00\x00"
OK
127.0.0.1:6379> get daily_active_users
"\x00\x00"
127.0.0.1:6379> setbit daily_active_users 0 1
(integer) 0
127.0.0.1:6379> get daily_active_users
"\x80\x00"
127.0.0.1:6379> setbit daily_active_users 14 1
(integer) 0
127.0.0.1:6379> get daily_active_users
"\x80\x02"
用户签到
很多网站都提供了签到功能(这里不考虑数据落地事宜),并且需要展示最近一个月的签到情况,如果使用bitmap我们怎么做?一言不合亮代码!
connect('127.0.0.1');
//用户uid
$uid = 1;
//记录有uid的key
$cacheKey = sprintf("sign_%d", $uid);
//开始有签到功能的日期
$startDate = '2017-01-01';
//今天的日期
$todayDate = '2017-01-21';
//计算offset
$startTime = strtotime($startDate);
$todayTime = strtotime($todayDate);
$offset = floor(($todayTime - $startTime) / 86400);
echo "今天是第{$offset}天" . PHP_EOL;
//签到
//一年一个用户会占用多少空间呢?大约365/8=45.625个字节,好小,有木有被惊呆?
$redis->setBit($cacheKey, $offset, 1);
//查询签到情况
$bitStatus = $redis->getBit($cacheKey, $offset);
echo 1 == $bitStatus ? '今天已经签到啦' : '还没有签到呢';
echo PHP_EOL;
//计算总签到次数
echo $redis->bitCount($cacheKey) . PHP_EOL;
/**
* 计算某段时间内的签到次数
* 很不幸啊,bitCount虽然提供了start和end参数,但是这个说的是字符串的位置,而不是对应"位"的位置
* 幸运的是我们可以通过get命令将value取出来,自己解析。并且这个value不会太大,上面计算过一年一个用户只需要45个字节
* 给我们的网站定一个小目标,运行30年,那么一共需要1.31KB(就问你屌不屌?)
*/
//这是个错误的计算方式
echo $redis->bitCount($cacheKey, 0, 20) . PHP_EOL;
活跃用户统计
$key1 = 'Userlogin2017-08-01';
$key2 = 'Userlogin2017-08-02';
$key3 = 'Userlogin2017-08-03';
##分别记录下 8月1号 和 8月2号 的活跃用户
$redis->setBit($key1, $uid, 1);
$redis->setBit($key2, $uid, 1);
##进行bitmap 计算统计1号2号都活跃的用户
$redis->bitOp('AND','8182',$key1,$key2);
$both_active = $redis->bitCount('8182');
##进行bitmap 计算统计1号 或 2号 或 3号 活跃的用户
$redis->bitOp('OR','818283',$key1,$key2,$key3);
$other_active = $redis->bitCount('818283');
假设当前站点有5000W用户,那么一天的数据大约为50000000/8/1024/1024=6MB
如果活跃用户在百万级别,使用Redis BitMap很划算。
如果活跃用户很少,而用户id都是10位以上的int。那就很浪费内存了,还不如使用set集合,然后求交集就可以了。
参考:
https://segmentfault.com/a/1190000008188655
https://blog.csdn.net/u011957758/article/details/74783347
https://blog.csdn.net/qq_28018283/article/details/76572342