本文由云+社区发表

前言

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

周期榜

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

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

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

滚动榜

方案探讨

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

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

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

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

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

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

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

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

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

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

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

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

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

那么,如何提前准备好Si−Ri−(N−1)这部分数据呢?可以如下处理:

  • 对一个元素加分时,加当日周期榜Ri、滚动榜Si;还需根据其在今日滚动榜中的分数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)次数的写操作,有较好的性能和可扩展性。且通过离线+在线的双预生成机制,确保了榜单实时生效,可用性较强。

此文已由作者授权腾讯云+社区发布


想知道谁是你的最佳用户?基于Redis实现排行榜周期榜与最近N期榜的更多相关文章

  1. 基于redis的排行榜设计和实现

    前言: 最近想实现一个网页闯关游戏的排行榜设计, 相对而言需求比较简单. 秉承前厂长的训导: “做一件事之前, 先看看别人是怎么做的”. 于是乎网上搜索并参考了不少排行榜的实现机制, 很多人都推荐了r ...

  2. 基于redis排行榜的实战总结

    前言: 之前写过排行榜的设计和实现, 不同需求其背后的架构和设计模型也不一样. 平台差异, 有的立足于游戏平台, 为多个应用提供服务, 有的仅限于单个游戏.排名范围差异, 有的面向全局排名, 有的只做 ...

  3. 20款最佳用户体验的Sublime Text 2/3主题下载及安装方法

    20款最佳用户体验的Sublime Text 2/3主题下载及安装方法

  4. 基于Redis的在线用户列表解决方案

    前言: 由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能: 在单机环境下,在线列表的实现方案可以采用SessionListener来完成 ...

  5. [项目回顾]基于Redis的在线用户列表解决方案

    迁移:基于Redis的在线用户列表解决方案 前言: 由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能: 在单机环境下,在线列表的实现方案 ...

  6. 基于Redis位图实现用户签到功能

    场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 当月签到满 ...

  7. 如果您想确保Windows 10在新用户登录时不安装内置应用程序,则必须删除所有配置的应用程序。

    原文 如果您想确保Windows 10在新用户登录时不安装内置应用程序,则必须删除所有配置的应用程序. 本文的内容 已安装与配置的应用程序 删除配置的应用程序 安装与配置的应用程序^ 在介绍如何删除所 ...

  8. 基于Redis缓存的Session共享(附源码)

    基于Redis缓存的Session共享(附源码) 在上一篇文章中我们研究了Redis的安装及一些基本的缓存操作,今天我们就利用Redis缓存实现一个Session共享,基于.NET平台的Seesion ...

  9. [转载] 基于Redis实现分布式消息队列

    转载自http://www.linuxidc.com/Linux/2015-05/117661.htm 1.为什么需要消息队列?当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消 ...

随机推荐

  1. SAP MM 标准采购组织的分配对于寄售采购订单收货的影响

    SAP MM 标准采购组织的分配对于寄售采购订单收货的影响 PO 4100004022 是一个寄售的采购订单, 采购组织是CSAS, 工厂代码SZSP.采购信息记录也是有的, MIGO试图对该采购订单 ...

  2. GDAL读取的坐标起点在像素左上角还是像素中心?

    目录 1. 问题 2. 结论 3. 例外 1. 问题 笔者在处理地理栅格数据的时候,总是会发生偏差半个像素的问题. 比如说通过ArcMap打开一张.tif,查看其地理信息:同时用记事本打开.tfw,比 ...

  3. VirtualAPK的简单使用

    VirtualApk引入步骤: 一.宿主应用引入VirtualApk 1.在项目的build.gradle文件中加入依赖: dependencies { classpath 'com.didi.vir ...

  4. 事务及其特性ACID

    一.事务的定义 事务是一组单元化的操作,这组操作可以保证要么全部成功,要么全部失败(只要有一个失败的操作,就会把其他已经成功的操作回滚). 一般所说的数据库事务,它是访问并可能更新数据库中各种数据项的 ...

  5. Windows 10-限制Windows更新上传带宽

    Windows Update Delivery Optimization可帮助您更快,更可靠地获取Windows更新和Microsoft Store应用程序. Windows Update Deliv ...

  6. kafka 幂等生产者及事务(kafka0.11之后版本新特性)

    1. 幂等性设计1.1 引入目的生产者重复生产消息.生产者进行retry会产生重试时,会重复产生消息.有了幂等性之后,在进行retry重试时,只会生成一个消息. 1.2 幂等性实现1.2.1 PID ...

  7. Storm入门(十三)Storm Trident 教程

    转自:http://blog.csdn.net/derekjiang/article/details/9126185 英文原址:https://github.com/nathanmarz/storm/ ...

  8. SCSS & SASS Color 颜色函数用法

    最近做一个没有设计师参与的项目,发现 scss 内置的颜色函数还挺好用.记录分享下 rgba() 能省掉手工转换 hex 到 rgb 格式的工作,如以下 SCSS 代码 $linkColor: #20 ...

  9. iOS开发之Masonry框架源码解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  10. redis 初识

    架构 sharding redis 集群是主从式架构,数据分片是根据hash slot(哈希槽来分布) 总共有16384个哈希槽,所以理论上来说,集群的最大节点(master) 数量是16384个.一 ...