需求场景:
- 排行榜,优先按照积分排序,相同分数按照达成时间排序
- 用户量极大,需要准确计算当前用户的排名情况及其分数
- 对同一个用户的积分,不会有并发修改的场景
- 实时更新排行榜
先了解一下 Redis
的集合 sets
和有续集合 sorted sets
:
Sets
: 不重复且无序的字符串元素的集合。Sorted sets
,类似Sets
,但是每个字符串元素都关联到一个叫score
浮动数值(floating number value
)。里面的元素总是通过score进行着排序,所以不同的是,它是可以检索的一系列元素。(例如你可能会问:给我前面10个或者后面10个元素)
有序集合的成员是唯一的,但分数(score
)却可以重复。
比如以 ranking
为 key,记录一批玩家的游戏分数,如下:
1 | 127.0.0.1:6379> zadd ranking 10 amy |
按照分数从高到低,查看其排名如下:
1 | 127.0.0.1:6379> zrevrange ranking 0 -1 withscores |
如果再添加一个同分数的玩家,然后查看排行榜:
1 | 127.0.0.1:6379> zadd ranking 9 anna |
重复几次,同为9分的几个人名次不会改变,如果使用 zrange
来查看,这三个人的名次会也会反过来,可以确定,当分数相同时,排序方式为按照member
的字典返序排列。所以回到题目,直接使用游戏分数作为存储的 score
值,不能满足需求中 相同分数按照达成时间排序 的需求
再加上实时排行榜等需求,无法拆分存储,只能在一个 zset
里完成所有功能,那么解决方案就是讲 score
做一个组合:游戏分数+时间。但是如果直接做拼接:
1 | $score = sprintf("%d%d", $baseScore, time()); |
相同分数下,越早达成,排名越靠后。所以需要对 time()
时间戳做一下转换:
1 | $score = sprintf("%d%d", $baseScore, 2000000000 - time()); |
如此,
$baseScore
越大,排名越靠前$baseScore
相同,越早完成 时间戳越小,2000000000 - time()
差值越大,排名越靠前
用户实时排名、真实分数信息皆可直接从该数据中获得。
唯一的问题在于,用户量过大时,排行榜整体数据可能偏大,key
过期可能引发 redis
雪崩,所以 ranking
这个 key
只能设置为无过期时间,在业务场景失效后,代码循环逐个删除 zset
中的数据。