• 技术文章 >数据库 >Redis

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

    WBOYWBOY2022-08-22 19:54:57转载656

    php入门到就业线上直播课:进入学习

    推荐学习: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 withscores
    1) "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贡献值排名
    a1001
    b992
    c992
    d884
    e875

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

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

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

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

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

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

    // KEYS[1]表示排行榜key
    // KEYS[2]表示要查询的队伍的ID
    local 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实现排行榜及相同积分按时间排序功能的实现的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:脚本之家,如有侵犯,请联系admin@php.cn删除

    千万级数据并发解决方案(理论+实战):点击学习

    Mysql单表千万级数据量的查询优化与性能分析

    Mysql主从原理及其在高并发系统中的应用

    专题推荐:redis
    上一篇:简单聊聊Redis处理接口幂等性的两种方案 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• Redis从环境搭建到熟练使用(总结分享)• 图文详解Redis集群与扩展• redis分片集群搭建与使用详解• Redis步骤解析之sentinel哨兵集群• Redis的共享session应用实现短信登录
    1/1

    PHP中文网