排行榜在当今应用中扮演着至关重要的角色。无论是游戏中的玩家排名、社交平台的用户活跃度榜单,还是其他领域的各种榜单,排行榜都是用户参与性和互动性的关键。在实现排行榜功能时,选择合适的数据库和数据结构至关重要。Redis,作为一种内存数据库,以其高性能和灵活性而备受青睐。下面将探讨如何使用Redis的有序集合(Sorted Set)来实现排行榜功能。
1 Redis的有序集合
Redis是一种内存型数据库,查询效率高。Redis有一种数据结构叫有序集合(Sorted Set
),与普通集合相比,它的每个成员都关联一个分数,这个分数用于对成员进行排序。有序集合在插入和查询时都能够以 O ( l o g n ) O(log{n}) O(logn)的复杂度完成,这为排行榜的实现提供了高效的基础。有序集合不仅提供了快速的插入和查询操作,还支持范围查询,使得获取某个范围内的排名成员变得非常简单。
(关于Sorted Set如何实现高效的插入和查询,可以看我的这篇文章《Redis的跳跃表》)
2 使用有序集合实现排行榜
那么,我们要如何用Sorted Set来实现排行榜呢?
其实非常简单,首先根据需求,我们定义一个有序集合的key,例如:
- 玩家等级的排行榜,我们可以用
rank:level
来作key值。 - 每天更新的排行榜,可以在后面加个日期
rank:level:0412
- 还有些比如是每天对指定BOSS的伤害排行,可以用
rank:damage:bossID:0412
当排行数值改变的时候,我们用zadd
指令来更新数据:
redis
zadd rank:level 玩家等级 玩家ID #参数是`score`和`member`
也可以用
redis
ZINCRBY rank:level 10 lxx1 #使lxx1的积分增加10(如果lxx1在rank:level中不存在,则新增,设置积分为10)
需要查询排行榜数据的时候,我们用zrevrange
指令来获取数据(下标是从0开始):
zrevrange rank:level 0 99 WITHSCORES # 获取前100名的ID和分数
zrevrange rank:level 100 199 WITHSCORES # 获取101-200名的ID和分数
zrevrange rank:level 0 99 # 仅获取ID
这里我们使用zrevrange
,因为zset
是按从小到大排序的,zrevrange
是逆序返回zset
中的数据。
取出玩家ID之后,我们再从另外的地方(MySQL
、redis
或者内存中)获取玩家的其他数据(名字,头像等),组合出完整的榜单数据。
查看排行玩家的排行和数值:
redis
zscore rank:level lxx1 #获取玩家lxx1的分数
zrevrank rank:level lxx1 #获取玩家lxx1的排名
如果要移除某个玩家的排行,可以使用zrem
指令:
redis
zrem rank:level 玩家ID
3 实现数值相同时,按时间先后排序
游戏排行榜中,经常有这样的需求:玩家等级相同时,按照到达这个等级的时间先后顺序排序。
使用Sorted Set时,我们可以将数值乘以一个系数,然后加上时间戳来实现这个功能。
比如,玩家等级27
级,当前时间戳是1705589522
,我们可以将这两个数组组合起来,由于时间戳是越小排序越前(和等级越大排序越前相反),我们使用相减的方式:
val = 27*1e10 + 1e10 - 1705589522
最终的结果是278294410478
,其中前面的27
表示等级,后面的8294410478
是时间戳和1e10
的差值(10位数的时间戳最大可以用到2086年,有生之年够用了),等级越大,这个值越大,而等级相同时,时间戳小的值更大。
值得注意的是,Sorted Set底层是使用double
类型来存储数值,所以当排序的值过大时,加上这个时间戳可能就会不够精细。
通常来说,使用double
能表示的精确的正整数可以达到 2 53 − 1 {2}^{53}-1 253−1(9007199254740991
16位数字)(关于这个值的计算,可以看我的另一篇文章《double 类型中可精确表达的最大正整数》)
不过对于排行榜而言,如果本身的数值已经很大了,通常也不需要按照时间来排序了。比如BOSS伤害十几亿,这时候玩家连具体数值可能都看不到,通常也不会相同,用不着时间先后排序了。
4 排行榜的合并
合服的时候,使用Sorted Set
也可以很方便的合并排行榜。
Redis提供了并集(zunionstore)
操作,并集
指的是将两个或多个zset
中的元素合并为一个新的zset
。
它的语法如下:
redis
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
其中destination
表示合并到的目标key
,numkeys
表示后面有多少个key
要用来合并。例如有三个key
需要合并:rank:s1:level
、rank:s2:level
、rank:s3:level
,我们可以这样写:
ZUNIONSTORE rank:s1:level 3 rank:s1:level rank:s2:level rank:s3:level
表示将三个key
合并,然后存储到rank:s1:level
这个key
中。
不过需要注意的是,在Redis的集群模式下,这样操作有可能会报错。(具体看这里《Redis 报错:CROSSSLOT Keys in request don't hash to the same slot 的解决方案》)。