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

在日常的开发中,经常会碰到需要对用户的分值等进行排序,比如在游戏里面需要对战斗力进行排行,在组队活动中需要对各个队伍的贡献值进行排行,在微信中需要对各个好友的步数进行排行,此时一般会选择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/21562

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
亿速云的头像亿速云认证作者
上一篇 2022年8月30日 下午11:21
下一篇 2022年8月30日 下午11:22

相关推荐

  • Redis RESP协议如何实现

    回顾RESP协议 RESP是基于TCP来实现的Redis通信协议,该协议是以/r/n(行)进行分割的,协议支持5种类型,具体信息如下: 类型 前缀 备注 简单字符串 + 简单字符串以+开头 错误数据 – 错误数据以-开头 整数 : 整数以:开头 复杂字符串 $ 复杂字符串以$开头 数组 …

    2022年9月8日
    60400
  • redis分片集群如何搭建与使用

    前言 redis可以说在实际项目开发中使用的非常频繁,在redis常用集群中,我们聊到了redis常用的几种集群方案,不同的集群对应着不同的场景,并且详细说明了各种集群的优劣,本篇将以redis 分片集群为切入点,从redis 分片集群的搭建开始,详细说说redis 分片集群相关的技术点; 单点故障…

    2022年9月13日
    1.3K00
  • windows浩辰cad看图王怎么画图

    浩辰cad看图王能画图吗: 答:能。 能够完成最基本的图形绘制的功能。 浩辰cad看图王画图步骤: 1、打开浩辰CAD看图王。 2、找到需要编辑的文件。 3、在如图所示的位置中找到绘图。 4、通过上面的功能即可画图。 “windows浩辰cad看图王怎么画图”的内容就介绍到这里了,感谢大家的阅读。如…

    2022年9月21日
    69600
  • 远程访问服务器有什么作用

    远程访问服务器有什么作用? 远程访问服务器可以带来多种益处,主要有1、提升工作效率、2、增强数据安全性、3、便于系统管理、4、节约成本。其中,提升工作效率是最直接的优势之一。通过远程访问,用户无论身处何地,只要有网络连接,都可以轻松访问服务器上的应用程序、文件和资源。这种灵活性极大地提高了工作的便利…

    2024年4月25日
    500
  • 什么是单独的服务器

    摘要 独立服务器,通常被称为单独的服务器或物理服务器,是指完全为一个用户或组织独占使用的服务器。它与共享服务器环境或虚拟服务器不同,提供了更高的性能、安全性和控制权。主要特点包括:1、完全独占;2、高度定制性;3、更高的安全性以及4、性能优先。高度定制性意味着用户可以根据具体的应用需求和性能要求,定…

    2024年4月23日
    2800
  • html5里required的概念是什么

    在html5中,required是“必须”的意思,是指定标签中的一个属性;该属性是一个布尔属性,规定必须在提交之前填写输入的字段,若使用该属性,则字段是必填的,并且该属性是html5中新增的属性,语法为“<input required>”。 本教程操作环境:windows10系统、HTM…

    2022年9月2日
    84000
  • 电脑键盘不能打字变成快捷键了怎么解决

    电脑键盘不能打字变成快捷键解决方法 名列前茅种方法 最简单的就是重启一下电脑,方便快捷的解决问题。 第二种方法 1,一般情况下是电脑键盘上的“windows“键出问题了,应该是压下后未弹起,可以先检查一下。 “windows“键就是ctrl和alt键之间的 2,WIN键(也就是那个开机四个方块图样的…

    2022年9月6日
    6.7K00
  • SQL语句知识点有哪些

    一、SQL简介 SQL (Structured Query Language:结构化查询语言) 是用于管理关系数据库管理系统(RDBMS)。 SQL 的范围包括数据插入、查询、更新和删除,数据库模式创建和修改,以及数据访问控制。 1.什么是数据库 数据库 (DB database) 概念:数据仓库 …

    2022年9月22日
    85800
  • cad字体如何添加进去

    cad字体添加进去的方法 1、首先右键CAD的图标,找到属性点击。 2、然后切换到快捷方式栏目。 3、然后打开文件所在的位置。 4、找到Fonts文件夹。 5、Fonts文件夹就是CAD的字体库了; 如果需要添加字体,只需要把下载下来的字体文件复制进这个文件夹就可以了。 到此,相信大家对“cad字体…

    2022年9月15日
    1.4K00
  • word页面变成左右两页怎么调回来

    调回来的方法: 1、首先打开word,然后点击顶部“视图”。 2、此时你可以看到单页选项,然后点击菜单中的“单页”。 3、此时就可以看到页面已经恢复正常了。 4、你也可以在变左右两页时,把右下角的显示比例调成100%来恢复正常。 以上就是“word页面变成左右两页怎么调回来”这篇文章的所有内容,感谢…

    2022年9月19日
    3.9K00
注册PingCode 在线客服
站长微信
站长微信
电话联系

400-800-1024

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

分享本页
返回顶部