排行榜到处可见,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜等等。
今天让我们从程序设计的角度,来看看如何设计一个排行榜!
我们先从最基础的实现方式来说起。
MySQL 的 ORDER BY 关键字
第一种要介绍的实现方式就是直接使用 MySQL 的 ORDER BY
关键字。 ORDER BY
关键字可以对查询出来的数据按照指定的字段进行排序。
我相信但凡是学过 MySQL 的人,一定都用过 ORDER BY
关键字!没用过的,先不要看下面的文章了,麻烦默默反思 3 分钟。
我之前在一个用户数据量不大(6w 用户左右)并且排序需求并不复杂的项目中使用的就是这种方法。
这种方式的优缺点也比较明显。好处是比较简单,不需要引入额外的组件,成本比较低。坏处就是每次生成排行榜都比较耗时,对数据库的性能消耗非常之大,数据量一大,业务场景稍微复杂一点就顶不住了。
我们这里创建一个名为 cus_order
的表,来实际测试一下这种排序方式。为了测试方便, cus_order
这张表只有 id
、score
、name
这 3 个字段。
我们定义一个简单的存储过程(PROCEDURE)来插入 100w 测试数据。
存储过程定义完成之后,我们执行存储过程即可!
等待一会,100w 的测试数据就插入完成了!
为了能够对这 100w 数据按照 score
进行排序,我们需要执行下面的 SQL 语句。
为了能够查看这套 SQL 语句的执行时间,我们需要通过show profiles
命令。
不过,请确保你的 profiling
是开启(on)的状态(可以通过 show variables
命令查看)。
默认情况下, profiling
是关闭(off)的状态,你直接通过set @@profiling=1
命令即可开启。
然后,我们就查询到了具体的执行速度。
可以看到,一共耗时了接近 4 s。
如何优化呢? 加索引并且限制排序数据量 是一种比较常见的优化方式。
我们对 score
字段加索引,并限制只排序 score
排名前 500 的数据。
这个时候,我们再执行下面的 SQL 语句,速度就快了很多,只需要 0.01 秒就排序了前 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
的值进行排序。
User | Score |
user1 | 112.0 |
user2 | 100.0 |
user3 | 123.0 |
user4 | 100.0 |
user5 | 33.0 |
user6 | 993.0 |
我们这里简单来演示一下。我们把上表中的数据添加到sorted set
中。
sorted set
基本可以满足大部分排行榜的场景。
如果我们要查看包含所有用户的排行榜怎么办? 通过 ZRANGE
(从小到大排序) / ZREVRANGE
(从大到小排序)
如果我们要查看只包含前 3 名的排行榜怎么办? 限定范围区间即可。
如果我们需要查询某个用户的分数怎么办呢? 通过 ZSCORE
命令即可。
如果我们需要查询某个用户的排名怎么办呢? 通过 ZREVRANK
命令即可。
如何对用户的排名数据进行更新呢? 通过 ZINCRBY
命令即可。
除了我上面提到的之外,还有一些其他的命令来帮助你解决更多排行榜场景的需求,想要深入研究的小伙伴可以仔细学习哦!
不过,需要注意的一点是:Redis 中只保存了排行榜展示所需的数据,需要用户的具体信息数据的话,还是需要去对应的数据库(比如 MySQL)中查。
你以为这样就完事了? 不存在的!还有一些无法仅仅通过 Redis 提供的命令解决的场景。
比如,如何实现多条件排序? 其实,答案也比较简单,对于大部分场景,我们直接对 score
值做文章即可。
更具体点的话就是,我们根据特定的条件来拼接 score
值即可。比如我们还要加上时间先后条件的话,直接在score
值添加上时间戳即可。
再比如,如何实现指定日期(比如最近 7 天)的用户数据排序?
我说一种比较简单的方法:我们把每一天的数据都按照日期为名字,比如 20350305 就代表 2035 年 3 月 5 号。
如果我们需要查询最近 n 天的排行榜数据的话,直接 ZUNIONSTORE
来求 n 个 sorted set
的并集即可。
我不知道大家看懂了没有,我这里还是简单地造一些数据模拟一下吧!
通过 ZUNIONSTORE
命令来查看最近 3 天的排行榜情况:
现在,这 3 天的数据都集中在了 last_n_days
中。
如果一个用户同时在多个 sorted set
中的话,它最终的 score
值就等于这些 sorted set
中该用户的 score
值之和。
既然可以求并集,那必然也可以求交集。你可以通过 ZINTERSTORE
命令来求多个 n 个 sorted set
的交集。
有哪些场景可以用到多个**sorted set**
的交集呢? 比如每日打卡的场景,你对某一段时间每天打卡的人进行排序。
这个命令还有一个常用的权重参数 weights
(默认为 1)。在进行并集/交集的过程中,每个集合中的元素会将自己的 score
*weights
。
我下面演示一下这个参数的作用。
如果,我们需要将员工和管理者放在一起比较,不过,两者权重分别为 1 和 3。
最终排序的结果如下:
总结
上面我一共提到了两种设计排行榜的方法:
- MySQL 的 ORDER BY 关键字
- Redis 的 sorted set
其实,这两种没有孰好孰坏,还是要看具体的业务场景。如果说你的项目需要排序数据量比较小并且业务场景不复杂的话(比如你对你博客的所有文章按照阅读量来排序),我觉得直接使用 MySQL 的 ORDER BY
关键字就可以了,没必要为了排行榜引入一个 Redis。
另外,在没有分页并且数据量不大的情况下,直接在前端拿到所有需要用到的数据之后再进行排序也是可以的。