百度&必应权4, 日IP8000. 查看详情
自助收录

如何设计排行榜功能?

面试指南2年前 (2022)更新 江南白衣
309 0 0

如何设计一个排行榜?
排行榜到处可见,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜等等。今天让我们从程序设计的角度,来看看如何设计一个排行榜!我们先从最基础的实现方式来说起。

如何设计排行榜功能?

MySQL 的 ORDER BY 关键字

第一种要介绍的实现方式就是直接使用 MySQL 的 ORDER BY 关键字。 ORDER BY 关键字可以对查询出来的数据按照指定的字段进行排序。

我相信但凡是学过 MySQL 的人,一定都用过 ORDER BY 关键字!没用过的,先不要看下面的文章了,麻烦默默反思 3 分钟。

我之前在一个用户数据量不大(6w 用户左右)并且排序需求并不复杂的项目中使用的就是这种方法。

这种方式的优缺点也比较明显。好处是比较简单,不需要引入额外的组件,成本比较低。坏处就是每次生成排行榜都比较耗时,对数据库的性能消耗非常之大,数据量一大,业务场景稍微复杂一点就顶不住了。

我们这里创建一个名为 cus_order 的表,来实际测试一下这种排序方式。为了测试方便, cus_order 这张表只有 id、score、name这 3 个字段。

CREATE TABLE `cus_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`score` int(11) NOT NULL,
`name` varchar(11) NOT NULL DEFAULT ”,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8mb4;

我们定义一个简单的存储过程(PROCEDURE)来插入 100w 测试数据。

DELIMITER ;;
CREATE DEFINER=`root`@`%` PROCEDURE `BatchinsertDataToCusOder`(IN start_num INT,IN max_num INT)
BEGIN
DECLARE i INT default start_num;
WHILE i < max_num DO
insert into `cus_order`(`id`, `score`, `name`)
values (i,RAND() * 1000000,CONCAT(‘user’, i));
SET i = i + 1;
END WHILE;
END;;
DELIMITER ;

存储过程定义完成之后,我们执行存储过程即可!

CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据

等待一会,100w 的测试数据就插入完成了!

为了能够对这 100w 数据按照 score 进行排序,我们需要执行下面的 SQL 语句。

SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;#降序排序
为了能够查看这套 SQL 语句的执行时间,我们需要通过show profiles命令。

不过,请确保你的 profiling 是开启(on)的状态(可以通过 show variables 命令查看)。

默认情况下, profiling 是关闭(off)的状态,你直接通过set @@profiling=1命令即可开启。

然后,我们就查询到了具体的执行速度。

{
“Query_ID”: 6,
“Duration”: 3.63526325,
“Query”: “SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC”
}

可以看到,一共耗时了接近 4 s。

如何优化呢? 加索引并且限制排序数据量 是一种比较常见的优化方式。

我们对 score 字段加索引,并限制只排序 score 排名前 500 的数据。

这个时候,我们再执行下面的 SQL 语句,速度就快了很多,只需要 0.01 秒就排序了前 500 名的数据。

{
“Query_ID”: 38,
“Duration”: 0.0102915,
“Query”: “SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC LIMIT 500”
}

当然了,这只是一个最简单的场景,实际项目中的复杂度要比我这里列举的例子复杂很多,执行速度也会慢很多。

不过,能不用 MySQL 的 ORDER BY 关键字还是要看具体的业务场景。如果说你的项目需要排序数据量比较小并且业务场景不复杂的话(比如你对你博客的所有文章按照阅读量来排序),我觉得直接使用 MySQL 的 ORDER BY 关键字就可以了。

Redis 的 sorted set

了解过 Redis 常见数据结构的小伙伴,都知道 Redis 中有一个叫做 sorted set 的数据结构经常被用在各种排行榜的场景下。

通过 sorted set ,我们能够轻松应对百万级别的用户数据排序。这简直就是专门为排行榜设计的数据结构啊!

Redis 中 sorted set 有点类似于 Java 中的 TreeSet 和 HashMap 的结合体,sorted set 中的数据会按照权重参数 score 的值进行排序。

如何设计排行榜功能?

我们这里简单来演示一下。我们把上表中的数据添加到sorted set中。

# 通过 zadd 命令添加了 6 个元素到 cus_order_set 中
127.0.0.1:6379> ZADD cus_order_set 112.0 user1 100.0 user2 123.0 user3 100.0 user4 33.0 user5 993.0 user6
(integer) 6

sorted set 基本可以满足大部分排行榜的场景。

如果我们要查看包含所有用户的排行榜怎么办? 通过 ZRANGE (从小到大排序) / ZREVRANGE (从大到小排序)

如何设计排行榜功能?
如果我们要查看只包含前 3 名的排行榜怎么办? 限定范围区间即可。

如何设计排行榜功能?
如果我们需要查询某个用户的分数怎么办呢? 通过 ZSCORE 命令即可。

127.0.0.1:6379> ZSCORE cus_order_set “user1”
“112”

如果我们需要查询某个用户的排名怎么办呢? 通过 ZREVRANK 命令即可。

127.0.0.1:6379> ZREVRANK cus_order_set “user3”
(integer) 1 # user3 排名第2

如何对用户的排名数据进行更新呢? 通过 ZINCRBY命令即可。

# 对 user1 的分数加2
127.0.0.1:6379> ZINCRBY cus_order_set +2 “user1”
“114”
# 对 user1 的分数减1
127.0.0.1:6379> ZINCRBY cus_order_set -1 “user1”
“113”
# 查看 user1 的分数
127.0.0.1:6379> ZSCORE cus_order_set “user1”
“113”

除了我上面提到的之外,还有一些其他的命令来帮助你解决更多排行榜场景的需求,想要深入研究的小伙伴可以仔细学习哦!

不过,需要注意的一点是:Redis 中只保存了排行榜展示所需的数据,需要用户的具体信息数据的话,还是需要去对应的数据库(比如 MySQL)中查。

你以为这样就完事了? 不存在的!还有一些无法仅仅通过 Redis 提供的命令解决的场景。

比如,如何实现多条件排序? 其实,答案也比较简单,对于大部分场景,我们直接对 score 值做文章即可。

更具体点的话就是,我们根据特定的条件来拼接 score 值即可。比如我们还要加上时间先后条件的话,直接在score 值添加上时间戳即可。

再比如,如何实现指定日期(比如最近 7 天)的用户数据排序?

我说一种比较简单的方法:我们把每一天的数据都按照日期为名字,比如 20350305 就代表 2035 年 3 月 5 号。

如果我们需要查询最近 n 天的排行榜数据的话,直接 ZUNIONSTORE来求 n 个 sorted set 的并集即可。

ZUNIONSTORE last_n_days n 20350305 20350306….
我不知道大家看懂了没有,我这里还是简单地造一些数据模拟一下吧!

# 分别添加了 3 天的数据
127.0.0.1:6379> ZADD 20350305 112.0 user1 100.0 user2 123.0 user3
(integer) 3
127.0.0.1:6379> ZADD 20350306 100.0 user4
(integer) 1
127.0.0.1:6379> ZADD 20350307 33.0 user5 993.0 user6
(integer) 2

通过 ZUNIONSTORE 命令来查看最近 3 天的排行榜情况:

127.0.0.1:6379> ZUNIONSTORE last_n_days 3 20350305 20350306 20350307
(integer) 6

现在,这 3 天的数据都集中在了 last_n_days 中。

127.0.0.1:6379> ZREVRANGE last_n_days 0 -1
1) “user6”
2) “user3”
3) “user1”
4) “user4”
5) “user2”
6) “user5”

如果一个用户同时在多个 sorted set 中的话,它最终的 score 值就等于这些 sorted set 中该用户的 score 值之和。

既然可以求并集,那必然也可以求交集。你可以通过 ZINTERSTORE 命令来求多个 n 个 sorted set 的交集。

有哪些场景可以用到多个sorted set 的交集呢? 比如每日打卡的场景,你对某一段时间每天打卡的人进行排序。

这个命令还有一个常用的权重参数 weights (默认为 1)。在进行并集/交集的过程中,每个集合中的元素会将自己的 score *weights 。

我下面演示一下这个参数的作用。

# staff_set 存放员工的排名信息
127.0.0.1:6379> ZADD staff_set 3.0 staff1 4.0 staff2
(integer) 2
# staff_set 存放管理者的排名信息
127.0.0.1:6379> ZADD manager_set 1.0 manager1 2.0 manager2
(integer) 2

如果,我们需要将员工和管理者放在一起比较,不过,两者权重分别为 1 和 3。

# staff_set 的权重为1 manager_set的权重为3
127.0.0.1:6379> ZUNIONSTORE all_user_set 2 staff_set manager_set WEIGHTS 1 3
(integer) 4

最终排序的结果如下:

127.0.0.1:6379> ZREVRANGE all_user_set 0 -1
1) “manager2”
2) “staff2”
3) “staff1”
4) “manager1”

总结

上面我一共提到了两种设计排行榜的方法:

1、MySQL 的 ORDER BY 关键字
2、Redis 的 sorted set

其实,这两种没有孰好孰坏,还是要看具体的业务场景。如果说你的项目需要排序数据量比较小并且业务场景不复杂的话(比如你对你博客的所有文章按照阅读量来排序),我觉得直接使用 MySQL 的 ORDER BY 关键字就可以了,没必要为了排行榜引入一个 Redis。

另外,在没有分页并且数据量不大的情况下,直接在前端拿到所有需要用到的数据之后再进行排序也是可以的。

© 版权声明

相关文章

暂无评论

暂无评论...