本文原文写于语雀, 导出后排版稍有变化,这里贴出原文地址把。
点击传送原文
介绍
所谓滚动排行榜,即不是固定的某一周,某一个月这种起始点在存储和查询时在某一个时间段是固定的。
比如近7天榜,近30天榜,随着每一天的变化这个榜单的取值范围是一直在变化的。
那么这个滚动排行榜与一般排行榜在实现上最大的区别在哪呢?
以我们最常用的redis实现方案来说,如果是固定周榜, 那么我们在一周内的任一时间点,在产生排行榜数据的时候, 只需要往这个固定的key里存入数据即可。在取值时,也直接一个命令zrevrange即可搞定。因为在这一周中的任一一天,都属于同一周。
但是如果是滚动榜呢,比如近7天榜,那么查询的数据永远都是今天加上前6天的数据要做一次聚合才能算出来排行榜,还用之前一个固定的key是没有办法解决这个问题的,这也是难点,写的时候怎么实现,要把数据如何存储,取的时候又如何让数据快速可用?
方案
1. 同步写n天滚动榜单
这种实现方式简单点说,和固定榜单实现方式是一样的。以redis实现周榜方式为例,比如业务排行榜的key我们取值为gift_ranking:${week},类型我们选择zset。用户每得到一个礼物就递增对应价值,最终排行榜统计中奖用户价值。
${week},按习惯,反正只要能够根据当前时间无论这一周的哪一天都算出来是这个值就行,比如我们就用每周的周一日期作为这个动态key的组成部分。
用户id为1001的得到价值为8888的礼物时,以及获取排行榜时,命令如下。比如
# 增加排行榜用户数据
ZINCRBY gift_ranking:20211108 礼物价值 用户id
# 获取排行榜前10名
zrevrange gift_ranking:20211108 0 9 withscores
如果做7天滚动榜如何做呢?同步写n天key要做的就是如下修改
- w e e k 要改为 {week}要改为 week要改为{day}, 每天一个key, 当天的要往当天key里写数据
- 除了写当天的,还要写(ZINCRBY)往后6天的,每次写操作都是这个逻辑。
- 取数据时,直接取当天即可
优点:
- 实现简单,取数据也简单,直接取即可用
缺点
- 每一次写需要写多个key, 这里就不把部分写失败当做缺点了,因为不严谨的话其它方案也会存在这个问题。如果是7天榜单,不想浪费过多精力,可以使用这种方案。但是天数多了,肯定是不合适的。
2. 写当日榜单 + 当日滚动榜单 + 定时
这种方案每次写,写两个榜单,一个是当日榜单,一个是当日滚动榜单。
对应的key分别为gift_ranking:
d
a
y
,
g
i
f
t
r
a
n
k
i
n
g
:
s
c
r
o
l
l
:
{day}, gift_ranking:scroll:
day,giftranking:scroll:{day}。以今日2021年11月08为例,写操作为
# 增加当日排行榜用户数据
ZINCRBY gift_ranking:20211108 礼物价值 用户id
# 增加当日滚动排行榜用户数据
ZINCRBY gift_ranking:scroll:20211108 礼物价值 用户id
取数据时,要取滚动榜单的key
zrevrange gift_ranking:scroll:20211108 0 9 withscores
定时的作用是在每天凌晨时,将前6天的数据聚合起来到当天的key中, 作为初始化数据,这样再有新数据写的时候是在6天数据的基础上做递增,因此能够保证每天的滚动榜的数据是全部近7天的数据的。可以使用zset的 ZUNIONSTORE命令来合并到新key中。
但是需要注意的一个点是,这里ZUNIONSTORE将前6天的数据合并到新的zset中要用一个临时zset,不能直接用当天的滚动榜key,因为该命令是覆盖,如果在定时执行的时候当天已经产生数据了,就会丢失数据。然后遍历这个临时zset,调用ZINCRBY命令将数据递增到定时当日滚动榜中。
优点:
- 实现起来简单, key也只会固定写两个,取值时取当天滚动榜即可使用。
缺点:
- 依赖定时倒不是问题,问题时在定时未将数据初始化完成时,排行榜的数据是有缺失的。即无法做到数据的平滑过渡,这个是最致命的问题。如果定时时间较长,则榜单将在这段时间内都存在问题。
3. 写当日榜单+当日滚动榜+次日滚动榜+定时
看起来步骤很多,实际上就是去解决2定时数据不能平滑过渡的问题
写的时候,写三个操作,无论是7天榜,还是30天,还是更多的,都是固定写这三个。
依然以今日2021年11月8号来举例,写数据时,执行以下操作
# 增加当日排行榜用户数据
ZINCRBY gift_ranking:20211108 礼物价值 用户id
# 增加当日滚动排行榜用户数据
ZINCRBY gift_ranking:scroll:20211108 礼物价值 用户id
# 增加次日滚动排行榜用户数据
ZINCRBY gift_ranking:scroll:20211109 礼物价值 用户id
增加次日的目的是保证,今日的数据提前加到次日上,因为今日的数据是个变量,再今日未结束时,谁也无法知道会有多少数据,所以这一块就保持同步操作,才能保证9号时间一到,9号的数据能平滑过渡直接就包含了8号这个变量中的数据。
定时的逻辑也不再是为了补齐定时当天滚动榜缺少的数据,因为当天的事情当天做,这样依然解决不了数据平滑过渡的问题。
定时是为了用历史数据来刷第二天的数据, 即如果是8号的定时,其实是为了初始化9号的数据,那么怎么个初始化法呢?
按照上述方案,当天的数据是同步写的,所以不需要定时关心,当日滚动榜的前一天的数据是前一天同步写的,也不需要关心(即上面那个写的时候双写逻辑)。那么次日滚动榜还缺少的其实最开始的5天的数据,而最开始的5天的数据,在第6天的时候就成为了历史数据不会变化。
即8号执行定时任务刷9号的数据,定时不需要处理8号当天的数据(8号有数据时会同步写到9号),也不需要处理9号(9号是明天,还没有数据)。然后就还差7、6、5、4、3这5天的日榜数据。而今天是8号,这几天的数据已经板上钉钉了,所以可以直接计算出来,然后递增到9号的滚动榜的key上。
注意这里操作的key,是滚动榜key, gift_ranking📜20211109, 因为9号目前还没有日榜数据。
这个时候只要把钱5天的数据做一次聚合,然后遍历,递增到gift_ranking📜20211109 key上即可
伪代码
ZUNIONSTORE gift_ranking:scroll:20211109 5 gift_ranking:scroll:20211107 gift_ranking:scroll:20211106 gift_ranking:scroll:20211105 gift_ranking:scroll:20211104 gift_ranking:scroll:20211103
不过这种方法有个缺陷, 写到新的key上是用覆盖的方式,这样由于前面我们实现方式是当天加数据的同时也会递增到第二天的滚动榜上,那么这里一旦定时执行这个脚本, 就有可能丢失在定时执行期间的这段数据。
所以我们只能采用笨的方法, 一种是比较容易想到,但实现起来有点繁琐恶心的方式。
即将7、6、5、4、3这5天的日榜数据全部取出来,然后遍历聚合,由此得到一个5天的结果集。然后再遍历这个结果集,使用ZINCRBY将每个用户的数据都递增到9号的滚动榜上去。这样保证了原子性, 9号对应用户的数据也不会覆盖。而且还有一个好处,这样定时也不必一定要卡在凌晨,可以放到业务的低峰期时间就行。
还有一种方案,实现起来比较简单,但理解有点难度。
经过前面的说法,可以知道,定时只需要关心7、6、5、4、3这5天的数据,那么这5天的数据除了从这5天的日榜上得到,其实还可以从另一个地方得到。因为我们有同步写和定时补第二天的滚动榜数据,这样就能保证第二天的数据在凌晨时是直接可用的,而我们拿当天排行榜数据时,也是直接操作当天的滚动榜key,而不是日榜key,所以当天的滚动榜是已经包含了近7天的数据的。
现在定时是在8号执行,去补9号滚动榜前5天的数据,而今天是8号,8号的滚动榜数据只要有业务触发,还是会一直变化的。但是7号的滚动榜数据已经成为了历史,不会再变化。而且7号的滚动榜数据本身就已经聚合了7、6、5、4、3这5天的数据,只不过7号的滚动榜数据也是针对7号的近7天数据,所以这里面还多了7号近7天榜的最开始的两天的数据。
即7号的滚动榜数据如果给9号用,最开始的1号和2号两天对于9号的近7天榜来说是过期的,但是7号滚动榜的后5天数据却是9号需要的。那么定时就可以直接在8号的时候取7号的滚动榜数据,然后再取1号和2号的日榜数据。
先聚合1号和2号有日榜数据的用户两天的总和, 然后再遍历7号滚动榜的同时,去判断当天正在遍历的用户在1号和2号是否有日榜数据,如果有就减去,那么这个值就是这个用户在9号滚动榜前5天的数据之和了。这个时候再调用ZINCRBY将用户和计算出来的值递增到9号滚动榜这个key上。即
# 增加次日滚动排行榜用户数据
ZINCRBY gift_ranking:scroll:20211109 上述算出来的前5天的用户的数据和 用户id
这个操作,又保证了如果9号滚动榜数据已经有数据的话,会是递增的方式。
再来串联一下9号滚动榜的数据是如何可以直接使用的。
经过上述逻辑,8号的定时已经将9号前5天的数据累加上去了,还剩最后两天的数据,即8号和9号的数据没有包含。
今天就是8号,今天有业务数据触发写操作时,本身就会写当日日榜,当日滚动榜和次日滚动榜数据,所以8号的数据,会实时累加到9号的滚动榜上。而9号的数据呢,今天是8号, 所以不可能有9号的日榜数据,当时间跨度到9号那一刻时,取排行榜直接取9号的滚动榜数据,由前面可知,这里已经包含了前面6天的数据,只有9号当天的数据还没包含。当然这里不是一定没有包含,因为时间到9号的时候如果有业务触发,本身就会写当日滚动榜,所以9号的实时数据也已经累加到当日滚动榜中了,这就保证了数据永远都是实时,且完全没有延迟。

163

被折叠的 条评论
为什么被折叠?



