排行榜数据存储 - redis 有续集合的一种使用场景

需求场景:

  • 排行榜,优先按照积分排序,相同分数按照达成时间排序
  • 用户量极大,需要准确计算当前用户的排名情况及其分数
  • 对同一个用户的积分,不会有并发修改的场景
  • 实时更新排行榜

先了解一下 Redis 的集合 sets 和有续集合 sorted sets

  • Sets: 不重复且无序的字符串元素的集合。
  • Sorted sets ,类似 Sets ,但是每个字符串元素都关联到一个叫 score 浮动数值(floating number value)。里面的元素总是通过score进行着排序,所以不同的是,它是可以检索的一系列元素。(例如你可能会问:给我前面10个或者后面10个元素)

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

比如以 ranking 为 key,记录一批玩家的游戏分数,如下:

1
2
3
4
5
6
127.0.0.1:6379> zadd ranking 10 amy
(integer) 1
127.0.0.1:6379> zadd ranking 9 bob
(integer) 1
127.0.0.1:6379> zadd ranking 7 cathy
(integer) 1

按照分数从高到低,查看其排名如下:

1
2
3
4
5
6
7
127.0.0.1:6379> zrevrange ranking 0 -1 withscores
1) "amy"
2) "10"
3) "bob"
4) "9"
5) "cathy"
6) "7"

如果再添加一个同分数的玩家,然后查看排行榜:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> zadd ranking 9 anna
(integer) 1
127.0.0.1:6379> zadd ranking 9 jack
(integer) 1
127.0.0.1:6379> zrevrange ranking 0 -1 withscores
1) "amy"
2) "10"
3) "jack"
4) "9"
5) "bob"
6) "9"
7) "anna"
8) "9"
9) "cathy"
10) "7"

重复几次,同为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 中的数据。