专栏首页廖可知的专栏基于Redis实现排行榜周期榜与最近N期榜
原创

基于Redis实现排行榜周期榜与最近N期榜

前言

业务已基于Redis实现了一个高可用的排行榜服务,长期以来相安无事。有一天,产品说:我要一个按周排名的排行榜,以反映本周内用户的活跃情况。于是周榜(按周重置更新的榜单)诞生了。为了满足产品多变的需求,我们一并实现了小时榜、日榜、周榜、月榜几种周期榜。本以为可长治久安了,又有一天,产品体验业务后说:我想要一个最近7天榜,反映最近一段时间的用户活跃情况,不想让历史的高分用户长期占据榜首,可否?于是,滚动榜(最近N期榜)的需求诞生了。

周期榜

周期榜实现还是很容易的,给每个周期算出一个序号,作为榜单名后缀,进入新的周期自然切换读写新榜单,平滑过度。以日榜为例,根据时间戳ts计算每日序号s=ts/86400,以日序号s作为后缀即可实现零点后自动读写新日榜。小时榜与此雷同,不再赘述。

对于周榜,可以选定某一个周一(或周日,看需求)的时间戳为基准,计算基准到当前经过的周数为周序号,以此作为榜单后缀。

对于月榜,稍有不同,因为月份天数不固定,所以不能按照上述方法计算。但我们可以根据时间戳取得年、月信息,以年月做标志(如201810)后缀,即可实现月榜。

滚动榜

方案探讨

滚动榜需要考虑多个周期榜数据的聚合与自动迭代更新,实现起来就没那么容易了。下面分析几个方案。

方案1:每日一个滚动榜,当日离线补齐数据

还以日榜为例,最近N天榜就是把前N-1天到当天的每一个日榜榜单累加即可,比如最近7天榜,就是前6天到当天的每一个日榜中相同元素数据累加。因此,最直观的一个方案是:首先记录每天的排行榜R,那么第i天的最近N天榜S_i=\sum_{n=0}^{N-1}R_{i-n},其中,R_{i-x}表示第i天的前x天的日榜。实现上,可以每日生成一个滚动榜S和当天日榜R,加分时同时写入S和R,每日零点后跑工具将前N-1天数据累加写入当日滚动榜S。

这个方案的优点是直观,实现简单。但缺点也很明显,一是每日一个滚动榜,消耗内存较多;二是数据更新不实时,需要等待离线作业完成累加后S中的数据才完全正确;三是时间复杂度高,7天榜还好,只需要读过去6天数据,如果是100天榜,该方案需要读过去99天榜,显然不可接受。

方案2:全局一个滚动榜,当日离线补齐数据

基于方案1,如果业务无需查询历史的S,可以只使用全局一个S,无需每日创建一个S_i。加分操作还是同时加当日的R_i和全局唯一的S,但每日零点的离线作业改为从S中减去R_{i-(N-1)}的数据(即将最早一天的数据淘汰,从而实现S的计数滚动)。

此方案减少了内存使用,同时离线任务每次只需读取一个日榜做减法,时间复杂度为O(1);但仍需要离线作业完成才能保证数据正确性,还是无法做到平滑过渡。

方案3:每日一个滚动榜,实时更新

要做到每日零点后榜单实时生效,而不需要等待离线作业的完成,一种方案是预写未来的榜单。不难得出,当日分数会计入往后N-1天的滚动榜中。因此,可以写当天的滚动榜S_i的同时,写往后N-1天的榜单S_{i+1}S_{i+N-1}

该方案不仅能脱离离线作业做到实时更新,且可以省略每天的日榜。但缺点也不难看出,对于7天滚动榜,每次写操作需要更新7个榜单,写入量小时还勉强能接受,如果写操作量大或者需要的是30天、60天滚动榜,此方案可行性几乎为零。

方案4:实时更新,常数次写操作

有不有办法做到既能实时更新,写榜数量也不随N的增加而增加呢?不难看出,第i天滚动榜S_i=\sum_{n=0}^{N-1}R_{i-n},而第i+1天的滚动榜S_{i+1}=\sum_{n=0}^{N-1}R_{(i+1)-n}=\sum_{n=0}^{N-2}R_{i-n}+R_{i+1}。显然,S_{i+1}=S_i-R_{i-(N-1)}+R_{i+1}。由于R_{i+1}在刚达到零点时必然为空且可以在次日实时加到S_{i+1}上,因此如果我们能提前准备好S_i-R_{i-(N-1)}这部分数据,那么在零点进入i+1天后,R_{i+1}自然就是可用状态了。

以3天滚动榜为例,次日滚动榜初始态为当日滚动榜减去n-2天的日榜数据。
     +-------------------------------------------+
     |                                           |
+----+---+   +--------+   +--------+             |
|        |   |        |   |        |             |
| R(i-2) |   | R(i-1) |   |  R(i)  |             |
|        |   |        |   |        |             |
+----+---+   +----+---+   +---+----+             |
     |            |           |                  |
     |            |           |                  |
     |            |           |                  |
     |            |           v+                 v-
     |            |
     |            |    +  +--------+        +--------+
     |            +-----> |        |     +  |        |
     |                 +  |  S(i)  | +---+> | S(i+1) |
     +-----------------+> |        |        |        |
                          +--------+        +--------+

那么,如何提前准备好S_i-R_{i-(N-1)}这部分数据呢?可以如下处理:

  • 对一个元素加分时,加当日周期榜R_i、滚动榜S_i;还需根据其在今日滚动榜中的分数s、及n-1天日榜中的分数r,计算出其在明日滚动榜中的初始分数s-r写入明日滚动榜中;即3个写操作;
  • 如果一个元素在当日没有任何加分操作,那么不会触发写入初始分数操作,所以还需要一个离线工具补齐。与方案1、2不同的是,该离线工具可提前一天运行,即当日运行离线工具补齐次日的滚动榜数据即可。

简而言之:第一步是运行离线工具生成次日的滚动榜;第二步是在写操作时同时更新次日的滚动榜。

该方案也是每日一个滚动榜。相对方案3而言,是空间换时间。如果空间不足且无保留历史的需求,可在离线工具中清理历史数据。

                                +--------------+
                                |              |
                                |   AddScore   |
                                |              |
                                +-+----+-----+-+
                                  |    |     |
                                  v    |     |
+--------+   +--------+   +-------++   |     |
|        |   |        |   |        |   |     |
| R(i-2) |   | R(i-1) |   |  R(i)  |   |     |
|        |   |        |   |        |   |     |
+--------+   +--------+   +--------+   |     |
                                       |     v
                          +--------+   |    ++-------+
                          |        |   |    |        |
                          |  S(i)  +<--+    | S(i+1) |
                          |        |        |        |
                          +--------+        +----+---+
                                                 ^
                                                 |
                                                 |
                                          +------+-----+
                                          |            |
                                          |    Tool    |
                                          |            |
                                          +------------+

方案4的实现

以下是实现参考。此处仅列出核心的lua脚本。Redis命令调用脚本的参数定义为:

eval script 4 当日日榜key 当日滚动榜key 即将淘汰的日榜key 明日滚动榜key 榜单元素名 加分数

lua脚本script如下:

--加今日日榜分数
redis.call('ZINCRBY', KEYS[1], ARGV[2], ARGV[1])

--加今日滚动榜分数
local rs = redis.call('ZINCRBY', KEYS[2], ARGV[2], ARGV[1])
local curRoundScore = 0
if (rs) then
    curRoundScore = tonumber(rs)
end

--取即将淘汰的日榜分数
rs = redis.call('ZSCORE', KEYS[3], ARGV[1])
local oldCycleScore = 0
if (rs) then
    oldCycleScore = tonumber(rs)
end

--计算次日滚动榜初始分数
local nextRoundScore = curRoundScore - oldCycleScore
if nextRoundScore < 0 then
    nextRoundScore = 0
end

--设置次日滚动榜分数
redis.call('ZADD', KEYS[4], nextRoundScore, ARGV[1])

--返回今日分数
rs = redis.call('ZREVRANK', KEYS[2], ARGV[1])
return {curRoundScore, rs}

关于榜单key计算准确度的探讨 我们的业务是在排行榜接入层逻辑中计算榜单后缀的,这种方案对逻辑层多台机器的时间一致性要求较高,如果逻辑层服务器时钟不一致,可能在时间切换点上出现不同机器读写不同榜单的问题。如果业务对时间精确度要求严格,可以考虑通过lua脚步在redis端计算后缀。

.

关于内存容量限制的探讨 基于ZSet实现的排行榜,每个元素约需要100字节内存。如果榜单长度为1000万,则每个榜单约需要1G内存。滚动榜的计算需要每日保留一个日榜,如果滚动周期较长,则可能单机内存容量不足以容纳所有需要的榜单。 考虑到历史日榜数据是不会变更的,因此不在lua脚本中读取历史日榜数据也无一致性问题。故可以将榜单打散到多个Redis实例,在接入层做逻辑读取历史日榜的分数,再以参数形式传入给lua脚本处理。

总结

在榜单长度不大且并发量不高的场景下,使用关系数据库+Cache的方案实现排行榜有更高的灵活性。而在海量数据与高并发的场景下,Redis是一个更好的选择。本文基于Redis实现的滚动榜,不论滚动周期多长,都只需要常数(3)次数的写操作,有较好的性能和可扩展性。且通过离线+在线的双预生成机制,确保了榜单实时生效,可用性较强。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Redis 源码 bug 深入定位过程分享

    Redis 3.2.8及以前的版本中 AOF 重写过程存在fd泄漏的风险。本文描述了一次与此相关的 fd泄漏问题的定位过程,并结合源码分析了问题产生的根源。

    廖可知
  • 多姿势扩展 Redis 命令

    空间宠物业务需要实现一个定时消息触发组件,如在特定时刻给用户推送收集糖果通知、biubiu球功能定时回收用户丢弃的球等。可见,消息只有在特定时间到达才能被处理。...

    廖可知
  • Redis 4.0 PSYNC2中second_replid_offset探究

    Redis 4.0起引入了PSYNC2同步方式,分析源码时我们注意到,server数据中增加了replid2、second_replid_offset两个成员。...

    廖可知
  • git 入门教程之删除文件 原

    回忆一下文件的常见操作,新增文件,修改文件,删除文件等,新增和修改文件都单独讨论过,现在我们来研究一下如何删除文件.

    雪之梦技术驿站
  • 1.2.3 、Google Analytics参数配置与调优

    GA基础跟踪代码部署完之后并不是万事大吉的,还需要对其做一些配置和调优,参数配置与调优主要是在GTM上和GA上做一些配置,确保和提高数据的准确度,下面先介绍在G...

    GA小站
  • GAE、SAE、BAE 对比分析

    目前,云服务很多,例如GAE、BAE、SAE、TAE、CAE、ACE、EC2、AZURE各种云。本文主要从以下几个方面对GAE、SAE和BAE的优劣进行分析。

    阳光岛主
  • python文件常用操作

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haluoluo211/article/d...

    bear_fish
  • 公有云与私有云优劣对比分析

    T客汇官网:tikehui 撰文 |Felix 选择公有云或私有云并不是一个二选一的问题。行业分析师指出大部分的公司使用了多云战略,也就是说明他们至少使用了两种...

    人称T客
  • 【IoT迷你赛】致谢腾讯云

    我现在在新能源领域做项目经理,对于编程是一个门外汉,但我希望进入物联网圈子,未来主要关注的应用:

    用户5915855
  • 百知教育 - 面向对象的基础

    课程目录( L. Y7 g3 T& p& h) o0 I2 K* i) R 13_this.wmv. V$ c- \0 V" }) c6 j ...

    会呼吸的Coder

扫码关注云+社区

领取腾讯云代金券