本文由云+社区发表

前言

业务已基于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) +

| | | |

+--------+ +----+---+

^

|

|

+------+-----+

| |

| 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)次数的写操作,有较好的性能和可扩展性。且通过离线+在线的双预生成机制,确保了榜单实时生效,可用性较强。

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

java千万用户实现实时排名_想知道谁是你的最佳用户?基于Redis实现排行榜周期榜与最近N期榜...相关推荐

  1. 想知道谁是你的最佳用户?基于Redis实现排行榜周期榜与最近N期榜

    本文由云+社区发表 前言 业务已基于Redis实现了一个高可用的排行榜服务,长期以来相安无事.有一天,产品说:我要一个按周排名的排行榜,以反映本周内用户的活跃情况.于是周榜(按周重置更新的榜单)诞生了 ...

  2. java查找网站在百度排名_百度网站快排系统 - 网站排名如何优化?

    原出处:超级排名系统 原文链接:http://www.chaojipaiming.com 百度网站快排系统注册地址 seo.chaojipaiming.com 网站关键词快速排名优化,7-15天关键词 ...

  3. java实现微信、手机号登陆_微信小程序获取手机号授权用户登录功能

    小程序中有很多地方都会用到注册用户信息的地方,用户需要填写手机号等,有了这个组件可以快速获取微信绑定手机号码,无须用户填写. 1.getPaDirIrkFhoneNumber这个组件通过button来 ...

  4. 怎样不停请求接口实现实时刷新_快狗打车实时数仓和基于Hologres的数据服务建设...

    前言 数据的实时化是最近几年数据行业很重要的趋势,我们在去年底也建立起新一代的实时数仓,但是在数据应用上一直没有取得很大的突破,我们希望实时数仓不仅仅是支撑大屏.核心实时报表.个别实时应用等简单的场景 ...

  5. 用户体验五要素_【产品经理】如何理解“用户体验要素”5层模型?

    -经验实验室-每日求职经验分享 如何理解"用户体验要素"的5层模型? 经验总结/理论导入 1)要素一:战略层 战略层需要定义清楚两个核心维度的内容: · 用户需求: 用户需要这个产 ...

  6. java基于Redis实现排行榜功能-附源码

    java基于Redis Zset实现排行榜功能 前言 做之前要思考的问题? Zset怎么存储需要的多个字段? 话不多说先上效果图 数据存储格式 代码 源码下载 闲暇之余,整理了一下之前利用Redis ...

  7. bs 程序用户个性化设置保存_想更改win10设置,这12种打开方法你不可不知,方便又快捷...

    "设置"应用程序是Windows 10中最有用和最重要的应用程序之一,我们设置帐户.应用程序.系统.设备.网络和Internet.个性化.时间和语言.更新和安全等等都要通过它来进行 ...

  8. java语音从哪里开始学_想学习编程,应该从哪里开始学习呢?

    经常会有人问小编:我是零基础,想学习编程,应该从哪里开始学习呢? 当听到这个问题的第一反应,就是弄清楚他们为什么想学编程.这是一个很好地起点--弄清楚从中想要得到什么.我们可以根据他们的目的来回答这个 ...

  9. android引导用户打开位置权限_想升级 App?先要个权限吧!!!

    点击上方的终端研发部,右上角选择"设为星标" 每日早8点半,技术文章准时送上 公众号后台回复"学习",获取作者独家秘制精品资料 往期文章 记五月的一个Andro ...

最新文章

  1. 28岁适合转嵌入式开发吗?
  2. java中可用于定义成员常量_13秋北航《Java语言与面向对象程序设计》在线作业三辅导 …...
  3. linux网络编程面试题,完美收官!字节4面斩下2-2Offer
  4. [转]HTTPS网络流量解密方法探索系列(一)
  5. 十大迷你iPhone天气应用
  6. 签名程序集密钥文件路径
  7. VS2012统计代码量
  8. python hbase_python 操作 hbase
  9. 【ZOJ 1964】【尺取】Bound Found【暑期 No.3】
  10. 在WPF中嵌入WebBrowser可视化页面
  11. 数字信号处理教程matlab释义与实现,数字信号处理教程 MATLAB释义与实现 陈怀琛编...
  12. 【如何 在 HTML 页面中显示数学公式】
  13. 网站在多IE版本兼容性测试工具IETester的使用方法
  14. 磁盘被写保护无法使用怎么办?
  15. Python实现的双目相机标定系统
  16. 大航海懒神辅助工具全部更新完毕,祝大家航海愉快!
  17. 文件方式实现完整的英文词频统计实例
  18. 正則表達式基本使用方法简单介绍
  19. C语言中scanf与分隔符(空格回车Tab)
  20. thunderbird收件箱只显示邮件个数,无法打开邮件

热门文章

  1. Codeforces Round #719 (A-C)
  2. c语言编程流水灯与交通灯实验,C51单片机实验报告_流水灯_交通灯_定时器_双机交互_时钟.doc...
  3. android开发设计平台,10款开发和设计应该安装的android应用
  4. python123阶乘累加_使用多线程计算阶乘累加 1!+2!+3!+...+19!+20!。其中一个线程计算阶乘,另一线程实现累加并输出结果。...
  5. svm预测结果为同一个值_SVM算法总结
  6. python数据预处理_Python数据预处理——缺失值、重复值
  7. UE4 Fix – “Lighting build failed. Swarm failed to kick off.”
  8. UE4中的字符串转换
  9. 冠榕智能灯光控制协议分析(controller-node)
  10. 【转】1.4异步编程:轻量级线程同步基元对象