如何用Redis实现排行榜及相同积分按时间排序功能

需求:对组队活动中各个队伍的贡献值进行排行。

不考虑积分相同

Redis的Sorted Set是String类型的有序集合。集合成员是少数的,这就意味着集合中不能出现重复的数据。

每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是少数的,但分数(score)却可以重复。

下面先不考虑积分相同的情况,实现排行榜

// 准备数据,其中value为每个队伍的ID,score为队伍的贡献值> zadd z1 5 a 6 b 1 c 2 d 10 e(integer) 5// 分页查询排行榜所有的队伍和贡献值,要使用zrevrange,而不是zrange,贡献值越大越排在前面> zrevrange z1 0 2 withscores1) "e"2) "10"3) "b"4) "6"5) "a"6) "5"// 增加某个队伍的贡献值> zincrby z1 3 d"5"> zincrby z1 4 c"5"// 查询排行榜所有的队伍> zrevrange z1 0 -1 withscores 1) "e" 2) "10" 3) "b" 4) "6" 5) "d" 6) "5" 7) "c" 8) "5" 9) "a"10) "5"// 查询某个队伍的排名> zrevrank z1 d(integer) 2

Redis默认实现是相同分数的成员按字典顺序排序(09,AZ,a~z),上面使用的是zrevrange,所以是倒序,所以相同分数排序就不能根据时间优先来排序。

积分相同按时间排序,排名少数

在上面的实现中,如果两个队伍的贡献值相同,也就是积分值相同,无法根据时间的先后进行排行。

所以需要设计一个分数 = 贡献值 + 时间戳 ,谁分数大谁排前面,最后还要能根据分数能解析出来贡献值。

设计1

使用整型存储分数值,redis中score本身是一个double类型,能精确存储的最大整型数字为2^53=9007199254740992(16位)。而精确到毫秒的时间戳需要13位,此时留给存储贡献值只有3位数了,当前如果时间只要精确到秒,只需要10位,这样留给贡献值就有6位。

整体设计:高3位表示贡献值,低13位表示时间戳。

如果我们简单地把score结构由:贡献值 * 10^13 + 时间戳 拼凑,因为分数越大越靠前,而时间戳越小则越靠前,这样两部分的判断规则是相反的,无法简单把两者合成一起成为score。

但是我们可以逆向思维,可以用同一个足够大的数Integer.MAX减去时间戳,时间戳越小,则得到的差值越大,这样我们就可以把score的结构改为:贡献值 * 10^13 + (Integer.MAX-时间戳),这样就能满足我们的需求了。

设计2

由于redis的score值是double类型,可以使用整数部分存储贡献值,小数部分存储时间戳,同样时间戳的部分使用一个最大值减去它。

这样,整体设计变为:分数=贡献值 + (Integer.MAX-时间戳) * 10^-13

弊端:由于分数值是由两个变量来计算得出,所以在给队伍增加贡献值时,无法简单的使用之前的zincrby来改变score的值了,这样在并发情况下为队伍增加贡献值就会导致score值不准确。

错误情况模拟:

假设现在队伍A的贡献值为10队伍A中的队员X为队伍增加贡献值1,在程序中算出score为11.xxx队伍A中的队员Y为队伍增加贡献值1,在程序中算出score为11.yyy队伍A中的队员X调用redis的zadd命令设置队伍的贡献值为11.xxx队伍A中的队员Y调用redis的zadd命令设置队伍的贡献值为11.yyy最后算出队伍A的贡献值为11,无法保证增加贡献值这一个操作的原子性。

此时需要借助lua脚本来保证计算和设置贡献值这两个操作的原子性:

// 其中KEYS[1]为排行榜key,KEYS[2]为队伍ID// 其中ARGV[1]为增加的贡献值,ARGV[2]为Integer.MAX-时间戳local score = redis.call('zscore', KEYS[1], KEYS[2]) if not(score) then	score=0 end score=math.floor(score) + tonumber(ARGV[1]) + tonumber(ARGV[2]) redis.call('zadd', KEYS[1], score, KEYS[2]) return 1

由于redis中无法使用时间函数,所以(Integer.MAX-时间戳) * 10^-13部分由脚本外程序计算好传入。

分页查询排行榜,查询队伍的排名等功能都可以继续使用上面的命令。

积分相同按时间排序,并列排名

所谓并列排行榜,就是存在相同排名情况的排行榜。

我们期望的结果如下表:

队伍ID 贡献值 排名
a 100 1
b 99 2
c 99 2
d 88 4
e 87 5

当然现实中也有排名不跳过的情况,我这里考虑的是排名跳过的情况。

redis中score的设计还是采用上面的分数=贡献值 + (Integer.MAX-时间戳) * 10^-13,只是在查询排名时需要进行计算。

比如要查上表中队伍b的排名,思路如下:

  • 首先查到队伍b的score

  • 再查到跟队伍b的score的整数部分相同(也就是贡献值一样),排在名列前茅个的队伍的value(队伍ID)

  • 根据上一步得到的队伍ID查询此队伍的排名就是队伍b的排名

使用命令实现上面的步骤如下:

> zscore 排行榜key teamId> zrevrangebyscore(排行榜key, 上一步得到的score+1, 上一步得到的score, limit, 0 , 1)> zrevrank(排行榜key, 上一步得到的teamId)

为了性能考虑,可以使用下面的脚本一次查出来:

// KEYS[1]表示排行榜key// KEYS[2]表示要查询的队伍的IDlocal rank = 0 local score = redis.call('zscore', KEYS[1], KEYS[2]) if not(score) then    score=0 else     score=math.floor(score)     local firstScore = redis.call('zrevrangebyscore', KEYS[1], score+1, score, 'limit', 0, 1)     rank=redis.call('zrevrank', KEYS[1], firstScore[1]) end return {score,rank}

下面附上分页查询排行榜的脚本,假如一页10条,不用下面的脚本需要查询10次上面的脚本,如果连上面的脚本都没有使用的话就要查询30次redis。

// 排行榜key// ARGV[1]分页起始偏移// ARGV[2]分页结束偏移local list = redis.call('zrevrange', KEYS[1], ARGV[1], ARGV[2], 'withscores') local result={} local i = 1 for k,v in pairs(list) do     if k%2 == 0 then         local teamId = list[k-1]         local score = math.floor(v)         local firstScore = redis.call('zrevrangebyscore', KEYS[1], score+1, score, 'limit', 0, 1)         local rank=redis.call('zrevrank', KEYS[1], firstScore[1])         local l = {teamId=teamId, contributionValue=score, teamRank=rank+1}         result[i] = l i = i + 1     end end return cjson.encode(result)

此脚本使用了cjson库,返回的是一个json。

到此,相信大家对“如何用Redis实现排行榜及相同积分按时间排序功能”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

文章标题:如何用Redis实现排行榜及相同积分按时间排序功能,发布者:亿速云,转载请注明出处:https://worktile.com/kb/p/22032

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
亿速云的头像亿速云认证作者
上一篇 2022年8月31日 下午11:35
下一篇 2022年9月1日 下午7:36

相关推荐

  • windows驱动精灵ahci驱动怎么安装

    驱动精灵ahci驱动: 答:驱动精灵是无法安装ahci驱动的。 在硬盘IDE模式下是无法发现achi驱动的,所以无法安装。 而改成ata模式也会造成蓝屏。 ahci驱动安装方法: 1、按下“win+r”打开运行,输入 regedit。 2、依次打开 HKEY_LOCAL_MAHCINESystemC…

    2022年9月10日
    85500
  • 如何分析APP测试及流程

    目前工作中,测试App会涉及到一下几个方面:客户端、小程序、h6页面等,看似不同却又大相径庭。 点击添加图片描述(非常多60个字) 1、功能模块测试:功能模块测试其实最重要的是考察测试者的逻辑思维能力和对需求的理解能力以及一些页面交互性,输入输出的考虑等,所以以上三者是共通的,没有太大的区别。 2、…

    2022年9月2日
    48600
  • powerpoint的主要功能有哪些

    powerpoint的主要功能是创建演示文稿;用户还可以在投影仪或者计算机上进行演示,也可以将演示文稿打印出来,PPT适用于工作汇报、企业宣传、产品推介、婚礼庆典、项目竞标、管理咨询、教育培训等领域,并具有相册制作、文稿合并、运用母板、图片运动、动画控制等功能。 本教程操作环境:windows10系…

    2022年9月15日
    2.4K00
  • Burpsuit结合SQLMapAPI产生的批量注入插件是怎样的

    1.1变动: 增加过滤设置 优化显示结果 增加运行提示信息 增加域名正则匹配 整个插件分为三个面板:任务面板、sqlmapapi参数配置面板、过滤条件面板。 任务面板 Server : SQLmapapi服务的IP和端口 THREAD:同时检测的任务数量 Domain:需要检测的域名,支持正则匹配 …

    2022年9月20日
    55400
  • Python如何多线程并发下载图片

    最终效果 这就是我们最终要构建的效果。 安装依赖项 让我们安装每个人最喜欢的 requests 库。 pip install requests 现在,我们将看到一些用于下载单个 URL 并尝试自动查找图像名称以及如何使用重试的基本代码。 import requestsres = requests.g…

    2022年9月22日
    69400
  • Docker资源限制Cgroup怎么使用

    1.Cgroup简介 _cgroups,是一个非常强大的linux内核工具,他不仅可以限制被namespace隔离起来的资源, 还可以为资源设置权重、计算使用量、操控进程启停等等。所以cgroups (Control groups) 实现了对资源的配额和度量。 cgroups有四大功能: 资源限制:…

    2022年9月15日
    68700
  • windows KB4531955安装失败如何解决

    KB4531955安装失败解决方法 方法一: 更新升级最新的.net framework(目前是4.8版本) 方法二: 1.点击开始按钮>输入cmd>右键点击搜索结果中的“命令提示符”>以管理员身份运行 2.在打开的命令提示符窗口中运行: net s较好 w…

    2022年9月1日
    44000
  • SpringBoot登录验证token拦截器如何实现

    用户访问接口验证,如果用户没有登录,则不让他访问除登录外的任何接口。 实现思路: 1.前端登录,后端创建token(通过JWT这个依赖),返给前端 2.前端访问其他接口,传递token,后端判断token存在以或失效 3.失效或不存在,则返回失效提示,前端根据接口返回的失效提示,让其跳转到登录界面 …

    2022年9月20日
    77100
  • 电脑0x000007b蓝屏如何修复

    启动无限蓝屏0x000007b修复方法: 方法一: 1、先强制重启电脑,等待开机画面出现后按下快捷键“F12”进入bios系统中。 2、进入bios启动项设置界面,用方向键将光标移动到“advanced”。 3、然后再次选择“SATA Configuration”并按下回车键进行确认。 4、之后会出…

    2022年9月16日
    77800
  • MySQL优化器hash join怎么使用

    前言 数据库的优化器相当于人类的大脑,大部分时候都能做出正确的决策,制定正确的执行计划,走出一条高效的路,但是它毕竟是基于某些固定的规则、算法来做的判断,有时候并没有我们人脑思维灵活,当我们确定优化器选择执行计划错误时该怎么办呢,语句上加hint,提示它选择哪条路是一种常见的优化方法。 我们知道Or…

    2022年9月15日
    63400
站长微信
站长微信
电话联系

400-800-1024

工作日9:30-21:00在线

分享本页
返回顶部