Redis 是一个内存型数据库,其最大的作用就是当做缓存使用。提供了几种数据类型,方便开发过程中应对不同的需求场景。

需求是这样的,有一些需要排序的数据,按照不止一个维度进行排序,对应到数据库里,就是按照两个字段进行综合排序,那在 Redis 中应该使用哪种数据结构呢?

按单一字段排序

首先我们先把问题简化一下,按照一个字段进行排序,看看应该怎么实现。

排行榜场景很常见,比如历史数据排行榜、热度排行榜(微博热搜、知乎热榜等、B站热门等等),根据播放量进行排序,那针对这样的场景,使用 Redis 怎么解决呢?

我们首先忽略掉实时更新的问题,假设是昨日榜单,数据已经确定了的,不会改变。

解决方案

列表

有人立马想到用列表方式解决,因为列表本身就是有序的,所以把榜单以 Lists 的形式放到 Redis 中,查询的时候直接取 列表就好了,什么都不用管了。

这种方案就是把 Redis 纯粹当做缓存使用,存什么取什么,其他额外的动作一概没有。

但是这种方案的缺陷也在于此,存进来的数据就已经是排序好的数据,所以,这个排序逻辑要在外部程序中提前写好,然后将排序好的数据存进去。

那如果外部程序不对榜单进行排序,或者说条件不允许的情况怎么办呢?那列表就没办法了。

有序集合 sorted set

sorted set 是在 Sets 的基础上增加了分数设置作为排序依据,所以除了具备 Sets 的特性外,还可以进行排序。

它提供了一个 score 属性,正好可以用来做排序依据,对榜单类的需求完美匹配。

# 按分数从小到大排序
ZRANGE key start stop [WITHSCORES]

# 按分数从大到小排序
ZREVRANGE key start stop [WITHSCORES]  

使用 sorted set 就可以降低外部程序的复杂度了,程序中不用对榜单进行排行了,只需要将符合入榜条件的记录拿出来,存到 sorted set 中就可以了。

在 sorted set 结构中,就用访问量当做分数,查询的时候执行 ZREVRANGE 命令就可以了。

按两个字段排序

现在需求变了一下,排序的维度有所变化,加了一个维度。还比如说热度排行榜吧,之前按访问量排序,现在增加了按照访问量和评论量总和排序,反应到数据库上是这样的:

select * from post order by read_count,comment_count desc

这种情况下怎么做呢?

解决方案

当然还可以用列表

仍然还可以用列表实现,逻辑都不变,只要改变外部程序的查询逻辑就可以了,之前按一个字段排序,现在按两个来。

有序集合 sorted set

我们知道 sorted set 中只有一个 score 属性,那怎么用这一个属性实现两个维度的排序呢?

这就需要在 score 属性上做文章了,最简单的实现,可以将访问量+评论数的和作为 score。

但是这样精度上太细了,如果访问量足够大,比如上百万的访问量,那几千个评论数也就几乎可以忽略不计了,最后实际的排序还是按照访问量来的。但是在小访问量上其实就可以用这种方式。

比如,访问量有 9000、8500 的两个文章,它俩的评论量分别为 100 和 700,那最后加起来的结果就是 9100 和 9200,所以,最后的排序结果实际上是阅读量 8500 的排在前面。

如果访问量过大的话,可以提高评论的权重,比如一个评论量等于 100或者1000 个阅读量。

还可以降低阅读量的精度,比如以万、十万为单位,如果以十万为精度,那 111万和 119万就都按 110万来算。

实时榜单

除了一些历史数据榜单外, 还有很多榜单都是实时的,这种情况下就要保证 redis 中的数据是实时更新的。

那如果是实时更新的情况,用列表存储就不合适了,读写 redis 是为了追求高性能,如果还用列表存储就意味着外部程序要做实时的排序、更新 redis 操作,可想而知,那样外部程序根本无法做到实时更新,或者代价极大。

所以,就要靠 sorted set 来解决了,可以使用 ZINCRBY命令快速更新 sorted 中的元素的分数,比如视频的播放量更新的时候通过更新 redis 中的值,因为更新 redis 的速度是非常快的,所以可以实现播放量的实时更新。

ZINCRBY key increment member

因为依靠的是 sorted set 中的分数来排序,所以,当成员的分数更新了之后,查询出来的排行信息就是实时的榜单了。