基于exZset轻松实现多维排行榜

exZset是阿里云自研的数据结构,可实现256维度的double类型的分值排序。

背景信息

原生Zset痛点:原生RedisSorted Set(也称Zset)排序结构只支持1double类型的分值排序,这在实现多维度排序时非常困难。例如通过IEEE 754结合拼接的方式实现多维度排序,此类方式存在实现复杂、精度下降、ZINCRBY命令无法使用等局限性。

exZset特点

借助阿里云自研的exZset数据结构,可帮助您轻松实现多维度排序能力,相较于传统方案具有如下优势:

  • 最大支持256维的double类型的分值排序(排序优先级为从左往右)。

    对于多维score而言,左边的score优先级大于右边的score,以一个三维score为例:score1#score2#score3,exZset在比较时,会先比较score1,只有score1相等时才会比较score2,否则就以score1的比较结果作为整个score的比较结果。同样,只有当score2相等时才会比较score3。若所有维度分数都相同,则会按照元素顺序(ascii顺序)进行排序。

    为了方便理解,可以把#想象成小数点(.),例如0#99、99#9099#99大小关系可以理解为0.99 < 99.90 < 99.99,即0#99 < 99#90 < 99#99。

  • 支持EXZINCRBY命令,不再需要取回当前数据,在本地增加值后再拼接写回Tair

  • 支持和原生Zset相似的API。

  • 提供 普通排行榜分布式架构排行榜 的能力。

  • 提供开源TairJedis客户端,无需任何编解码封装,您也可以参考开源自行实现封装其他语言版本。

说明

关于本文中使用的exZset相关命令,详细解释,请参见exZset

应用场景

排序需求常见于各类游戏、应用、奖牌等排行榜中,通常业务对排序的需求如下:

  • 支持增删改查和反向排序,可根据分数范围获取相应用户。

  • 快速返回排序请求的结果。

  • 具备扩展能力(即 分布式架构排行榜 ),在数据分片容量或计算能力不足时,可以将其扩展到其他数据分片。

实现奖牌榜

在奖牌榜中,从金、银、铜牌的维度对参与方进行排名,先按照金牌数量排序;如果金牌数量一致,再以银牌数量排序;如果银牌数量也一致,再按照铜牌数量排序。在本示例中,参与方EF的金牌数相等,但是银牌数参与方E大于F,因此参与方E排名靠前,通过exZset的多维排序能力,您只需要使用简单的API即可完成该需求。

排名

参与方

金牌金牌

银牌银牌

铜牌铜牌

1

A

32

21

16

2

B

25

29

21

3

C

20

7

12

4

D

14

4

16

5

E

13

21

18

6

F

13

17

14

代码示例

本方案需使用TairJedis(Tair自研)客户端。

  1. 添加pom.xml配置。

            <dependency>
                <groupId>com.aliyun.tair</groupId>
                <artifactId>alibabacloud-tairjedis-sdk</artifactId>
                <version>5.3.1</version>
            </dependency>
  2. 示例代码。

    import io.valkey.JedisPool;
    import io.valkey.JedisPoolConfig;
    import com.aliyun.tair.tairzset.LeaderBoard;
    
    public class LeaderBoardExample {
        // 配置实例连接地址、端口号、账号密码等信息。
        private static final int DEFAULT_CONNECTION_TIMEOUT = 5000;
        private static final int DEFAULT_SO_TIMEOUT = 2000;
        private static final String HOST = "<r-bp1mx0ydsivrbp****.redis.rds.aliyuncs.com>";
        private static final int PORT = 6379;
        private static final String PASSWORD = "<Pass****word>";
        private static final JedisPoolConfig config = new JedisPoolConfig();
    
        public static void main(String[] args) {
            JedisPool jedisPool = new JedisPool(config, HOST, PORT, DEFAULT_CONNECTION_TIMEOUT,
                    DEFAULT_SO_TIMEOUT, PASSWORD, 0, null);
    
            // 创建排行榜。
            LeaderBoard lb = new LeaderBoard("leaderboard", jedisPool, 10, true, false);
    
            // 如果金牌数相同,按照银牌数排序,否则继续按照铜牌。
            //                    金牌 银牌 铜牌
            lb.addMember("A",     32,  21, 16);
            lb.addMember("D",     14,  4,  16);
            lb.addMember("C",     20,  7,  12);
            lb.addMember("B",     25,  29, 21);
            lb.addMember("E",     13,  21, 18);
            lb.addMember("F",     13,  17,  14);
    
            // 获取A的排名。
            lb.rankFor("A"); // 1
            System.out.println(lb.rankFor("A"));
    
            // 获取Top3。
            lb.top(3);
            System.out.println(lb.top(3));
            // [{"member":"A","score":"32#21#16","rank":1}, 
            // {"member":"B","score":"25#29#21","rank":2}, 
            // {"member":"C","score":"20#7#12","rank":3}]
    
            // 获取整个排行榜。
            lb.allLeaders();
            System.out.println(lb.allLeaders());
            // [{"member":"A","score":"32#21#16","rank":1}, 
            // {"member":"B","score":"25#29#21","rank":2}, 
            // {"member":"C","score":"20#7#12","rank":3}, 
            // {"member":"D","score":"14#4#16","rank":4}, 
            // {"member":"E","score":"13#21#18","rank":5}, 
            // {"member":"F","score":"13#17#14","rank":6}]
        }
    } 

    更多操作请参见alibabacloud-tairjedis-sdk中的com.aliyun.tair.tairzset.LeaderBoard介绍。

实现实时、小时、日、周和月维度的排行榜

该场景下的需求是实现月榜,那么这个Key就从月的维度进行索引。

利用TairZset的多级索引能力可以轻松实现不同时间范围的排行榜。本案例中,月度的所有数据存储在1Key中(名称为julyZset),写入演示数据如下:

EXZINCRBY julyZset 7#2#6#16#22#100 7#2#6#16#22_user1
EXZINCRBY julyZset 7#2#6#16#22#50 7#2#6#16#22_user2
EXZINCRBY julyZset 7#2#6#16#23#70 7#2#6#16#23_user1
EXZINCRBY julyZset 7#2#6#16#23#80 7#2#6#16#23_user1
说明
  • 7#2#6#16#22#100表示7月第261622分,更新其分数为100。

  • 7#2#6#16#22_user1表示此时间点更新的用户,用户名加入了具体时间前缀。

排行榜类型

具体实现的命令和返回结果

小时级别实时排行榜,即从当前时间往前推算一个小时(例如16:23~15:23)。

说明

如果访问非常频繁,可以将结果进行缓存。

查询命令:

EXZREVRANGEBYSCORE julyZset 7#2#6#16#23#0 7#2#6#15#23#0

返回结果:

1) "7#2#6#16#22_user1"
2) "7#2#6#16#22_user2"

固定1小时排行榜,例如查询16:00~17:00时间段的排行榜。

查询命令:

EXZREVRANGEBYSCORE julyZset 7#2#6#17#0#0 7#2#6#16#0#0

返回结果:

1) "7#2#6#16#22_user1"
2) "7#2#6#16#22_user2"

日排行榜,例如查询75号的日排行榜。

在查询前,插入一条75号的数据:

EXZINCRBY julyZset 7#2#5#10#23#70 7#2#5#10#23_user1

返回结果:

"7#2#5#10#23#70"

查询命令:

EXZREVRANGEBYSCORE julyZset 7#2#6#0#0#0 7#2#5#0#0#0

返回结果:

1) "7#2#5#10#23_user1"

周排行榜,例如查询7月第2周的排行榜。

查询命令:

EXZREVRANGEBYSCORE julyZset 7#3#0#0#0#0 7#2#0#0#0#0

返回结果:

1) "7#2#6#16#22_user1"
2) "7#2#6#16#22_user2"
3) "7#2#5#10#23_user1"

月排行榜,例如查询7月的月排行榜。

在查询前,插入一条720号的数据:

EXZINCRBY julyZset 7#4#20#12#20#50 7#4#20#12#20_user1

返回结果:

"7#4#20#12#20#50"

查询命令:

EXZREVRANGEBYSCORE julyZset 7#6#0#0#0#0 7#0#0#0#0#0

返回结果

1) "7#4#20#12#20_user1"
2) "7#2#6#16#22_user1"
3) "7#2#6#16#22_user2"
4) "7#2#5#10#23_user1"