如何利用 Redis 快速实现签到统计功能

@这是小豪的第十一篇文章

上篇文章 已经对 Redis 基础命令进行了一个大致的学习,接下来我们就需要解决 Issue “增加用户活跃度统计” 啦!

其实当我看到这个 Issue 的时候,我的第一反应是利用 MySql 来实现,创建一个签到表,记录用户 ID 和 签到时间,然后统计的时候从数据库中取出来然后聚合计算,完美,哈哈。

但是当看到要求说要用 Redis 位运算的时候,我就在想,为啥呢,仔细想了一哈,发现如果用 MySql 来实现的话虽然简单粗暴,但是也有弊端,比如我们想要做一些复杂的功能就不是太方便了,或者说不是太高性能了,比如,今天是连续签到的第几天,在一定时间内连续签到了多少天。另外一方面,如果按100万用户量级来计算,一个用户每年可以产生 365条记录,100万用户的所有签到记录那就有点恐怖了,查询计算速度也会越来越慢。

所以毅然选择 Redis ,下面给大家介绍一下究竟为啥选择它。

准备

大家知道 Redis 的字符串数据都是以二进制的形式存放的,所以说 RedisBit 操作非常适合处理这个场景,因为 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());

简单粗暴,清晰明了,哈哈。

所以我们大致的格式应该是这样子的:

file

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 的方式好太多。

至此,一个简单的签到统计功能就已经实现了,大家可以根据自己的需求扩展,不当的地方欢迎大家指正,哈哈。

finecho # Lhao

附言 1  ·  2个月前

留言中好多同学都会对使用 $redis->keys ('login:201905*') 存有疑义,所以就对这个指令做了更进一步的了解,具体请戳:《Redis 中 Keys 与 Scan 的使用》

本帖由系统于 4个月前 自动加精
finecho
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 31

学习了, bitop 这个命令没用到过,学习了. setbit 其实还可以做 实时日活等统计,

4个月前 评论
finecho

@jichun 嗯嗯,是啊,还有很多应用场景,这里只是列出了一个场景,哈哈

4个月前 评论

不错,简单实用

4个月前 评论

非常不错的分享

主要还是平时对位运算符的运用与理解(位运算逻辑是通用型跟正则类似)

NOTICE:NOT:对一个或多个 key 求逻辑异或 -> BITOP NOT destkey srckey 对给定 key 求逻辑非(非多个key)
NOTICE:$redis->keys('login:201903') -> $redis->keys('login:201903*')

4个月前 评论

@Lhao $redis->bitop('AND', 'monthActivities'', $redis->keys('login:201903')); 这里的 $redis->keys('login:201903') 应该是 $redis->keys('login:201903') 少写了个 '' 星号吧

4个月前 评论
finecho

@jichun 呀,对 哈哈

4个月前 评论
finecho

@Fishers :heart_eyes: :stuck_out_tongue_closed_eyes:

4个月前 评论
wanghan

XOR和NOT有使用场景吗~

4个月前 评论
wanghan

如果key的数量是50亿以上,id是5万个,还能用这种方法吗?

4个月前 评论

突然某一天领导要求加一个统计用户签到高峰期时段的功能就凉凉了 :joy:

4个月前 评论

我的做法是两张表,一张会员签到信息表,表里记录比如连续签到天数,最近一次签到时间之类的信息,另一张表记录用户的签到日志,在大多数情况下不会用到签到日志表,但是应对某些临时需求时还是有作用的

4个月前 评论
Ali

@生活无限好 所以日志表就是为了查询某个时间段签到的人数统计吗?
所以某个时间段访问的高峰期,和签到的高峰期,恰好不一致吗?
我感觉如果是需要统计高峰期的话, 他是不是可以 通过重新设计他的 key 来实现,加上准确的十分秒.

4个月前 评论

@Ali 如果过了几天又要求按地区统计签到用户呢,然后再重新设计他的 key 吗,这些不是最开始就已经确定的需求,而是临时需求 :grin:

4个月前 评论
Ali

@生活无限好 那只有干死产品了 :joy: :joy:

4个月前 评论
finecho

@Ali @生活无限好 哈哈对,干死产品

4个月前 评论
GalaxyNo_1

@Lhao 如何该用户一个月的签到情况

4个月前 评论
finecho

@GalaxyNo_1 我觉得吧如果是单个用户可以遍历使用 getbit,如果统计多个用户的情况的话,可以利用 mget 批量获取到 value ,然后转换为二进制进行处理。哈哈,突然感觉这样处理有点难受,或许也可以给每个用户定义一个 key 记录每月的登录状态,offset 为 1- 30 :see_no_evil:

4个月前 评论
GalaxyNo_1

@Lhao :speak_no_evil:

4个月前 评论
xiaopi

如果用户id是uuid那就不行了 :joy:

4个月前 评论

keys*,这种命令是不被允许的。要统计一个用户最新的连续签到区间咋整?或者历史最长连续签到区间。

4个月前 评论

@生活无限好 @Lhao 我感觉你俩的方法结合起来是最完美的,mysql只留一个签到记录表就行了,剩下的需要聚合的处理在redis里拿数据 :joy:

4个月前 评论

统计连续签到天数怎么实现呢?

3个月前 评论
aen233

哇 厉害厉害 学习了

3个月前 评论
yuanshang

666,boolean 类型的数据,感觉都可以用 bit 存储。学到了

2个月前 评论

$redis->keys('login:201905*')是不被允许的

2个月前 评论
finecho

@唐启胤 已经附言了噢 :grin:

2个月前 评论

位图做签到的话,key最好不用用日期。不然每一天都是一个key。这里使用用户id为offset,其实不太对。实际中不可能每个用户每天都签到,这所导致的就是offset不连续。导致中间很多位都是0,反而得不偿失。最好是以用户id为key,签到天数为offset。这样就算某个用户一年不签到,中间也才360多个空位。

2周前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!