上篇文章 已经对 Redis 基础命令进行了一个大致的学习,接下来我们就需要解决 Issue“增加用户活跃度统计” 啦!
其实当我看到这个 Issue 的时候,我的第一反应是利用 Mysql 来实现,创建一个签到表,记录用户 ID 和 签到时间,然后统计的时候从数据库中取出来然后聚合计算,完美,哈哈。
但是当看到要求说要用 Redis 位运算的时候,我就在想,为啥呢,仔细想了一哈,发现如果用 Mysql 来实现的话虽然简单粗暴,但是也有弊端,比如我们想要做一些复杂的功能就不是太方便了,或者说不是太高性能了,比如,今天是连续签到的第几天,在一定时间内连续签到了多少天。另外一方面,如果按100万用户量级来计算,一个用户每年可以产生 365条记录,100万用户的所有签到记录那就有点恐怖了,查询计算速度也会越来越慢。
所以毅然选择 Redis,下面给大家介绍一下究竟为啥选择它。
大家知道 Redis 的字符串数据都是以二进制的形式存放的,所以说 Redis 的 Bit 操作非常适合处理这个场景,因为 Bit 的值为 0 或 1,用户是否打卡也可以用 0 或 1 来表示,我们把签到的天数对应到每个字节上,打卡了就是1,没打卡就是0,那么一个用户一年下来的记录就是 365 位的长度,100万用户一年只需要耗费大约 43 M 左右的存储空间就可以了,而且速度贼快,大伙可能会问,这个究竟是怎么计算来的,我们来看一下官方的解释:
在一台 2010MacBook Pro 上,offset 为2^32-1(分配512MB)需要~300ms,offset 为2^30-1(分配128MB)需要~80ms,offset 为2^28-1(分配32 MB)需要~30ms,offset 为2^26-1(分配8MB)需要8ms。
大概的空间占用计算公式是:( offset / 8 / 1024 / 1024 )MB
这里的 offset ,大家姑且当做用户 ID 来看,哈哈。
那么究竟如何去打卡呢,我们可以利用 setbit 命令来实现,setbit 的作用说的直白点就是:在你想要的位置操作字节值,比如说用户 3 在 3月13号 签到了,那么 setbit(20190313, 3 ,1) 就可以实现签到功能了,这里的 offset 就是3,同理,不同的用户不同的日期,改变对应的值就好了。
那么下面我们来实战一下:
1. 实例化一个 redis 连接
$redis = app('redis.connection');
2. 如何去设计 key 呢?
$dayKey = 'login:'.\now()->format('Ymd'); // 输出类似:login:20190310
// 普通写法
$dayKey = 'login:'.\date('Ymd',\time());
简单粗暴,清晰明了,哈哈。
所以我们大致的格式应该是这样子的:
3. 签到
setbit - SETBIT KEY_NAME OFFSET (Time complexity: O(1))
对 key 所储存的字符串值,设置或清除指定偏移量上的位 bit
$redis->setbit($dayKey, $this->user->id, 1);
可以看到在存储方面不仅耗费内存少,快,而且操作还方便,就这么一句话就搞定了,我当初也以为会是很复杂的操作,哈哈。并且它还有非常低的灵活高效的统计计算成本。
4. 统计一周内的签到数据
bitop - BITOP operation destkey key [key ...]
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上
AND : 对一个或多个 key 求逻辑并
OR : 对一个或多个 key 求逻辑或
XOR : 对一个或多个 key 求逻辑异或
NOT : 对给定 key 求逻辑非
$redis->bitop('AND', 'threeAnd', 'login:20190311', 'login:20190312', 'login:20190313');
echo "连续三天都签到的用户数量:" . $redis->bitCount('threeAnd');
$redis->bitop('OR', 'threeOr', 'login:20190311', 'login:20190312', 'login:20190313');
echo "三天中签到用户数量(有一天签也算签了):" . $redis->bitCount('threeOr');
$redis->bitop('AND', 'monthActivities'', $redis->keys('login:201903*'));
echo "连续一个月签到用户数量:" . $redis->bitCount('monthActivities');
echo "当前用户指定天数是否签到:" . $redis->getbit('login:20190311', $this->user->id);
.....
是不是特别方便快捷的统计查询,哈哈,
从上面的例子中大家可以看到不管在存储上面还是在统计计算上面,位运算都比 mysql 的方式好太多。
至此,一个简单的签到统计功能就已经实现了,大家可以根据自己的需求扩展,不当的地方欢迎大家指正,哈哈。
相关链接
一刻社区源码:https://github.com/overtrue/api.yike.io
reids 中 bitmap 的妙用:https://segmentfault.com/a/1190000008188655