php redis 二进制,Redis中bitmap的使用及在PHP中的应用

0x00 Bitmap是什么?

Bitmaps are not an actual data type, but a set of bit-oriented operations defined on the String type. Since strings are binary safe blobs and their maximum length is 512 MB, they are suitable to set up to 2^32 different bits.

位图并不是一个数据类型,而是在字符串类型之上定义的一组面向位的操作。由于字符串是二进制安全的,其最大长度为512 MB,因此它们可以设置多达2^32个不同的值(位)。

Bit operations are divided into two groups: constant-time single bit operations, like setting a bit to 1 or 0, or getting its value, and operations on groups of bits, for example counting the number of set bits in a given range of bits (e.g., population counting).

位操作应用场景主要分为两种:一种是简单的设置(1或0)和获取值操作,另一种是对位组的操作,例如在给定的位范围内计算设定位的数量(例如,人数统计)。

One of the biggest advantages of bitmaps is that they often provide extreme space savings when storing information. For example in a system where different users are represented by incremental user IDs, it is possible to remember a single bit information (for example, knowing whether a user wants to receive a newsletter) of 4 billion of users using just 512 MB of memory.

位图的一个最大的优点是: 在存储信息时可以极大地节省空间。例如,在一个用递增值作为用户id的系统中,仅使用512MB内存就可以记住40亿用户的一个比特信息(例如,知道用户是否希望接收新闻稿)

0x01 操作指令

SETBIT 设置指定key的偏移量的位值

SETBIT key offset value

GETBIT 获取指定key的偏移量的位值

GETBIT key offset

BITCOUNT

BITCOUNT key [start end] 统计设置为1的位的个数, 可以加区间(注意这里的区间是字节byte, 等于8个bit),

BITOP 位操作(bit operation缩写)

BITOP operation destkey key [key ...]

对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种。

BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑与,并将结果保存到 destkey 。

BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。

BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。

BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。

BITPOS 返回第一个位值符合条件的offset. (bit position缩写)

BITPOS key bit [start] [end] 返回位图中第一个值为bit的二进制位的位置。 在默认情况下将检测整个位图,但用户也可以通过可选的start参数和end参数指定要检测的范围。

0x02 数据的存储

redis以类似数组索引作为offset去存入0/1的值.

比如, 我把key=bit1的0,2,5,9,12,16,21位置存入1:

setbit bit1 0 1

setbit bit1 2 1

setbit bit1 5 1

setbit bit1 9 1

setbit bit1 12 1

setbit bit1 16 1

setbit bit1 21 1

redis存入的数据应该是:

byte0: 10100100 //0-7位

byte1: 01001000 //8-15位

byte2: 10000100 //16-23位

注意: 这里不要把二进制转换成十进制去算.

那我们现在用php取出数据看一下:

$redis = new Redis();

$redis->connect('127.0.0.1', 6379);

//$redis->auth('YourPassword');

$value = $redis->get('bit1');//字符串的bit

$bitmap = unpack('C*', $value); //解包: redis返回来的是二进制字符串,我们需要把它解成对应的无符号字符, 这里是数字

$count = 0; //这里统计个数, 如果只需要取一段数据时可以用

foreach($bitmap as $key => $number) {

echo $key.':'.$number.' => '.decbin($number).PHP_EOL;

while($number) {

$number &= ($number - 1);

$count++;

}

}

打印的结果是:

1:164 => 10100100

2:72 => 1001000

3:132 => 10000100

注意: decbin函数把十进制转换为二进制, 返回的结果位数可能小于8, 比如第二个byte的数据就是7位, 前面需要补0(可以使用str_pad函数), 然后就与我们之前手动计算的一样了.

现在再说一下bitcount的坑. 这个方法提供了start和end两个参数, 但是注意是byte, 不是bit的.

比如: bitcount bit1 0 0, 返回的结果是3, 而不是1. 因为redis查询的是第一个byte的数据, 而不是第一个bit的数据!

c18431dfbe340a200c6f03fb0dad4ba9.png

0x03 使用场景

1. 用户签到

我们把每个用户每年的签到作为一个key保存下来: key = attedance_%yyyy_UserId

同时offset使用the day of the year: offset = floor((current_timestamp - timestamp(yyyy-01-01)) / 86400)

setbit attendance_2020_10081 1 1

setbit attendance_2020_10081 2 1

setbit attendance_2020_10081 5 1

现在就可以直接查询某用户某一年的签到次数了。

那如果要显示某一个月每天的签到情况呢? 每天查询一次肯定会增加redis负载, 那就一次性的取出来, 然后按前面的方法做数据拆解.

如果数据量大(最大索引值很大)的话, unpack和循环的时间会很久, 不适合了.

判断某一天是否签到还是比较容易的:

$user_id = 10081;//用户id

$time = time();

$year = date('Y', $time);

$key = 'attendance_'.$year.'_'.$user_id;//每个用户按年度计算。 当然也可以把每个用户的所有签到数据放一个key

$off_set = floor(($time - strtotime($year.'-1-1'))/86400);

$status = $redis->getBit($key, $off_set);

if($status == 1){

echo '今天已签到';

}else{

echo '今天还未签到';

}

2. 统计活跃用户

以yyyymmdd为key,用户id为offset,如果当日活跃过就设置为1.

//日期对应的活跃用户数据模拟

$data = [

'20200801' => [1,2,3,4,5,6,7,8,9,10],

'20200802' => [1,2,3,4,5,6,7,8],

'20200803' => [1,2,3,4,5,6],

'20200804' => [1,2,3,4],

'20200805' => [1,2]

];

//批量设置活跃状态

foreach($data AS $date=>$arr_uid){

$cacheKey = sprintf("stat_%s", $date);

foreach($arr_uid AS $uid){

$redis->setBit($cacheKey, $uid, 1);

}

}

那么该如何计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只要有一天在线就称为活跃),那么需要使用BITOP.

$redis->bitOp('AND', 'stat', 'stat_20200801', 'stat_20200802', 'stat_20200803', 'stat_20200804', 'stat_20200805');

echo "总活跃用户:" . $redis->bitCount('stat') . PHP_EOL; //2个

$redis->bitOp('AND', 'stat1', 'stat_20200801', 'stat_20200802', 'stat_20200803');

echo "总活跃用户:" . $redis->bitCount('stat1') . PHP_EOL; //6个

那如果数据量超过100w以上呢? 我们来模拟个100-200w的数据:

$time = microtime(true);

//批量设置活跃状态

foreach($arr_date AS $key=>$month){

$cacheKey = sprintf("stat_%s", $month);

for($i = 1; $i <= 3000000; $i++){

$rand = rand(0,10);

if($rand > 4) { //差不多5成的概率

$redis->setBit($cacheKey, $i, 1);

}

}

}

$time2 = microtime(true);

echo $time2 - $time;

执行之后, 3个key下的数据都在160w左右.

9a90d70feab586adf45e88c8f8252220.png

那下面来测试一下合并操作

//先清空stat和stat1这两个key,然后再执行下面的操作

$time = microtime(true);

$redis->bitOp('AND', 'stat', 'stat_20200801', 'stat_20200802');

echo "总活跃用户:" . $redis->bitCount('stat') . PHP_EOL;

$time2 = microtime(true);

echo $time2 - $time;

基于移动版 Intel I7-8650的centos 7.6虚机,1核2G的配置, 执行完成如下:

总活跃用户:891678

0.036981105804443

3个key全部与操作, 执行时间在0.05s, 都是可以接受的.

so, 百万左右数据的简单的2个string的合并还是比较快速的. 千万以上可能需要分片处理.

那么如何查询一个月内每天都在签到人数呢? 或者签到人数最多的一天如何查询呢??? TO DO

3. 用户在线状态

只需要一个key,以用户ID为offset,如果在线就设置为1,不在线就设置为0. 基本业务的实现很简单.

===========================

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值