想知道谁是你的最佳用户?基于Redis实现排行榜周期榜与最近N期榜
本文由云+社区发表
前言
业务已基于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期榜的更多相关文章
- 基于redis的排行榜设计和实现
前言: 最近想实现一个网页闯关游戏的排行榜设计, 相对而言需求比较简单. 秉承前厂长的训导: “做一件事之前, 先看看别人是怎么做的”. 于是乎网上搜索并参考了不少排行榜的实现机制, 很多人都推荐了r ...
- 基于redis排行榜的实战总结
前言: 之前写过排行榜的设计和实现, 不同需求其背后的架构和设计模型也不一样. 平台差异, 有的立足于游戏平台, 为多个应用提供服务, 有的仅限于单个游戏.排名范围差异, 有的面向全局排名, 有的只做 ...
- 20款最佳用户体验的Sublime Text 2/3主题下载及安装方法
20款最佳用户体验的Sublime Text 2/3主题下载及安装方法
- 基于Redis的在线用户列表解决方案
前言: 由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能: 在单机环境下,在线列表的实现方案可以采用SessionListener来完成 ...
- [项目回顾]基于Redis的在线用户列表解决方案
迁移:基于Redis的在线用户列表解决方案 前言: 由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能: 在单机环境下,在线列表的实现方案 ...
- 基于Redis位图实现用户签到功能
场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 当月签到满 ...
- 如果您想确保Windows 10在新用户登录时不安装内置应用程序,则必须删除所有配置的应用程序。
原文 如果您想确保Windows 10在新用户登录时不安装内置应用程序,则必须删除所有配置的应用程序. 本文的内容 已安装与配置的应用程序 删除配置的应用程序 安装与配置的应用程序^ 在介绍如何删除所 ...
- 基于Redis缓存的Session共享(附源码)
基于Redis缓存的Session共享(附源码) 在上一篇文章中我们研究了Redis的安装及一些基本的缓存操作,今天我们就利用Redis缓存实现一个Session共享,基于.NET平台的Seesion ...
- [转载] 基于Redis实现分布式消息队列
转载自http://www.linuxidc.com/Linux/2015-05/117661.htm 1.为什么需要消息队列?当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消 ...
随机推荐
- SQL两列数据,行转列
SQL中只有两列数据(字段1,字段2),将其相同字段1的行转列 转换前: 转换后: --测试数据 if not object_id(N'Tempdb..#T') is null drop table ...
- Windows Server 2016-PS筛选导出用户邮箱属性包含某字段列表
生产环境中我们往往会遇到以多个邮箱别名结尾的情况,如何快速导出当前域用户邮箱以某字段或后缀结尾的用户列表信息变得尤为重要,本例简单汇总下如何通过Powershell快速筛选出当前邮箱信息包含azure ...
- wampserver一系列问题总结
总结下之前wampserver集成包安装的问题. windows sever 2008 R2 64,wampserver3.1.4_x64. 在安装的过程,有选择默认浏览器和编辑器,这些安装步骤简单, ...
- @Autowired注解警告Field injection is not recommended
在使用spring框架中的依赖注入注解@Autowired时,idea报了一个警告 大部分被警告的代码都是不严谨的地方,所以我深入了解了一下. 被警告的代码如下: @Autowired UserDao ...
- Could not load file or assembly……
今天在运行一个ASP.NET Core项目的时候发现这样的错误: 我一开始觉得这是个很简单的问题,很明显,出错的原因是项目中某些地方还保留了对Njt.MvcAuthLib这个库的引用,而现在我不需要了 ...
- web server性能优化浅谈
作者:ZhiYan,Jack47 转载请保留作者和原文出处 Update: 2018.8.8 在无锁小节增加了一些内容 性能优化,优化的东西一定得在主路径上,结合测量的结果去优化.不然即使性能再好,逻 ...
- JDK中的Atomic包中的类及使用
引言 Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作.原子变量的底层使用了处理器提供的原子指令,但是不同的CPU ...
- 线性回归预测PM2.5----台大李宏毅机器学习作业1(HW1)
一.作业说明 给定训练集train.csv,要求根据前9个小时的空气监测情况预测第10个小时的PM2.5含量. 训练集介绍: (1)CSV文件,包含台湾丰原地区240天的气象观测资料(取每个月前20天 ...
- 怎么隐藏 iOS Safari 打开网页时的地址栏和工具栏探索
先来看一张截图 红色框处就是用手机浏览器打开页面时,自动显示出来的头部地址栏和底部工具栏 如果现在有一个需求,用手机浏览器打开页面时,把地址栏和工具栏隐藏,该怎么办呢? 起初我在度娘找到了好几篇博客都 ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...