已经知道可以使用集合类型来存储文章标签数据,但是如果想要标签排序呢?

一、有序集合的集合操作

集合类型提供了强大的集合操作命令,但是如果需要排序就要用到有序集合类型。Redis 的作者在设计Redis的命令时考虑到了不同数据类型的使用场景,对于不常用到的或者在不损失过多性能的前提下可以使用现有命令来实现的功能,Redis就不会单独提供命令来实现。这 一原则使得Redis在拥有强大功能的同时保持着相对精简的命令。

有序集合常见的使用场景是大数据排序,如游戏的玩家排行榜,所以很少会需要获得键中的全部数据。同样 Redis 认为开发者在做完交集、并集运算后不需要直接获得全部结果, 而是会希望将结果存入新的键中以便后续处理。这解释了为什么有序集合只有 ZINTERSTORE和ZUNIONSTORE命令而没有ZINTER和ZUNION命令。

当然实际使用中确实会遇到需要直接获得集合运算结果的情况,除了等待 Redis 加入相关命令,我们还可以使用 MULTI, ZINTERSTORE, ZRANGE, DEL和 EXEC这5 个命令自己实现ZINTER:

MULTI 
ZINTERSTORE tempKey ... 
ZRANGE tempKey ... 
DEL tempKey 
EXEC

二、sort命令

除了使用有序集合外,我们还可以借助 Redis 提供的 SORT命令来解决前言问题。

SORT命令可以对列表类型、集合类型和有序集合类型键进行排序,并且可以完成与关系数据 库中的连接查询相类似的任务。

假如博客中标有“ruby ”标签的文章的ID分别是“2”、“6”、“12”和“26”。由于在集合类 型中所有元素是无序的,所以使用SMEMBERS命令并不能获得有序的结果[3] 。为了能够让博客的标签页面下的文章也能按照发布的时间顺序排列(如果不考虑发布后再修改文章发布时间,就是按照文章ID的顺序排列),可以借助SORT命令实现,方法如下所示:

redis> SORT tag:ruby :posts
1) "2" 
2) "6" 
3) "12" 
4) "26"

除了集合类型,SORT 命令还可以对列表类型和有序集合类型进行排序:

redis> LPUSH mylist 4 2 6 1 3 7 
(integer) 6 
redis> SORT mylist 
1) "1" 
2) "2" 
3) "3" 
4) "4" 
5) "6" 
6) "7"

除了可以排列数字外,SORT命令还可以通过ALPHA参数实现按照字典顺序排列非数字 元素,就像这样:

redis> LPUSH mylistalpha a c e d B C A
(integer) 7
redis> SORT mylistalpha 
(error) ERR One or more scores can't be converted into double
redis> SORT mylistalpha ALPHA 
1) "A" 
2) "B" 
3) "C" 
4) "a" 
5) "c" 
6) "d" 
7) "e"

从这段示例中可以看到如果没有加ALPHA参数的话,SORT命令会尝试将所有元素转换 成双精度浮点数来比较,如果无法转换则会提示错误。
回到前言问题,SORT 命令默认是按照从小到大的顺序排列,而一般博客中显示文章 的顺序都是按照时间倒序的,即最新的文章显示在最前面。SORT命令的DESC参数可以实现
将元素按照从大到小的顺序排列:

redis> SORT tag:ruby :posts DESC 
1) "26" 
2) "12" 
3) "6" 
4) "2"

那么如果文章数量过多需要分页显示呢?SORT命令还支持LIMIT参数来返回指定范围的 结果。用法和 SQL 语句一样,LIMIT offset count,表示跳过前 offset 个元素并获取之后的 count个元素。 SORT命令的参数可以组合使用,像这样:

redis> SORT tag:ruby :posts DESC LIMIT 1 2 
1) "12" 
2) "6"

三、BY参数

很多情况下列表(或集合、有序集合)中存储的元素值代表的是对象的ID(如标签集合 中存储的是文章对象的ID),单纯对这些ID自身排序有时意义并不大。更多的时候我们希望 根据ID对应的对象的某个属性进行排序。回想前面内容,我们通过使用有序集合键来存储文章 ID列表,使得博客能够支持修改文章时间,所以文章ID的顺序和文章的发布时间的顺 序并不完全一致;

博客是使用散列类型键存储文章对象的,其中time字段存储的就是文章的发布时 间。现在我们知道ID为“2”、“6”、“12”和“26”的四篇文章的time字段的值分别 为“1352619200”、“1352619600”、“1352620100”和“1352620000”(Unix时间)。如果要按照 文章的发布时间递减排列结果应为“12”、“26”、“6”和“2”。为了获得这样的结果,需要使用 SORT命令的另一个强大的参数:BY

BY参数的语法为BY参考键。其中参考键可以是字符串类型键或者是散列类型键的某个 字段(表示为键名->字段名)。如果提供了 BY 参数,SORT 命令将不再依据元素自身的值 进行排序,而是对每个元素使用元素的值替换参考键中的第一个*并获取其值,然后依据 该值对元素排序。就像这样:

redis> SORT tag:ruby :posts BY post:*->time DESC 
1) "12" 
2) "26" 
3) "6" 
4) "2"

在上例中SORT命令会读取post:2、post:6、post:12、post:26几个散列键中的time字段的值 并以此决定tag:ruby:posts键中各个文章ID的顺序。 除了散列类型之外,参考键还可以是字符串类型,比如:

redis> LPUSH sortby list 2 1 3 
(integer) 3 
redis> SET itemscore:1 50 
OK
redis> SET itemscore:2 100
OK
redis> SET itemscore:3 -10 
OK
redis> SORT sortbylist BY itemscore:* DESC 
1) "2" 
2) "1" 
3) "3"

当参考键名不包含*时(即常量键名,与元素值无关),SORT 命令将不会执行排序操 作,因为Redis认为这种情况是没有意义的(因为所有要比较的值都一样)。例如:

redis> SORT sortbylist BY anytext 
1) "3" 
2) "1" 
3) "2"

例子中 anytext 是常量键名(甚至 anytext 键可以不存在),此时 SORT 的结果与 LRANGE的结果相同,没有执行排序操作。在不需要排序但需要借助SORT命令获得与元素 相关联的数据时,常量键名是很有用的。
如果几个元素的参考键值相同,则 SORT 命令会再比较元素本身的值来决定元素的顺 序。像这样:

redis> LPUSH sortby list 4 
(integer) 4 
redis> SET itemscore:4 50 
OK
redis> SORT sortbylist BY itemscore:* DESC 
1) "2" 
2) "4" 
3) "1" 
4) "3"

示例中元素"4"的参考键itemscore:4的值和元素"1"的参考键itemscore:1的值都是50,所以 SORT命令会再比较"4"和"1"元素本身的大小来决定二者的顺序。 当某个元素的参考键不存在时,会默认参考键的值为0:

redis> LPUSH sortby list 5 
(integer) 5 
redis> SORT sortbylist BY itemscore:* DESC 
1) "2" 
2) "4" 
3) "1" 
4) "5" 
5) "3"

上例中"5"排在了"3"的前面,是因为"5"的参考键不存在,所以默认为 0,而"3"的参考键 值为−10。
补充知识 参考键虽然支持散列类型,但是*只能在“->”符号前面(即键名部分)才有 用,在“->”后(即字段名部分)会被当成字段名本身而不会作为占位符被元素的值替换,即 常量键名。但是实际运行时会发现一个有趣的结果:

redis> SORT sortby list BY somekey->somefield:* 
1) "1" 
2) "2" 
3) "3" 
4) "4" 
5) "5"

上面提到了当参考键名是常量键名时 SORT 命令将不会执行排序操作,然而上例中确进 行了排序,而且只是对元素本身进行排序。这是因为 Redis 判断参考键名是不是常量键名的 方式是判断参考键名中是否包含“*”,而 somekey->somefield:中包含“”所以不是常量键 名。所以在排序的时候Redis对每个元素都会读取键somekey中的 somefield:字段(“”不会 被替换),无论能否获得其值,每个元素的参考键值是相同的,所以Redis会按照元素本身的 大小排列。


四、get参数

现在博客已经可以按照文章的发布顺序获得一个标签下的文章 ID 列表了,接下 来要做的事就是对每个ID都使用HGET命令获取文章的标题以显示在博客列表页中。有没有 觉得很麻烦?不论你的答案如何,都有一种更简单的方式来完成这个操作,那就是借助SORT 命令的GET参数。

GET参数不影响排序,它的作用是使 SORT命令的返回结果不再是元素自身的值,而是 GET参数中指定的键值。GET参数的规则和BY参数一样,GET参数也支持字符串类型和散列 类型的键,并使用“*”作为占位符。要实现在排序后直接返回ID对应的文章标题,可以这样 写:

redis> SORT tag:ruby :posts BY post:*->time DESC GET post:*->title 
1) "Windows 8 app designs" 
2) "RethinkDB - An open-source distributed database built with love" 
3) "Uses for cURL" 
4) "The Nature of Ruby "

在一个SORT命令中可以有多个GET参数(而BY参数只能有一个),所以还可以这样 用:

redis> SORT tag:ruby :posts BY post:*->time DESC GET post:*->title GET post:*->time 
1) "Windows 8 app designs" 
2) "1352620100" 
3) "RethinkDB - An open-source distributed database built with love" 
4) "1352620000" 
5) "Uses for cURL" 
6) "1352619600" 
7) "The Nature of Ruby " 
8) "1352619200"

可见有N个GET参数,每个元素返回的结果就有N行。这时有个问题:如果还需要返回 文章ID 该怎么办?答案是使用 GET #。就像这样:

redis> SORT tag:ruby :posts BY post:*->time DESC GET post:*->title GET post:*->time
GET #
1) "Windows 8 app designs" 
2) "1352620100" 
3) "12" 
4) "RethinkDB - An open-source distributed database built with love" 
5) "1352620000" 
6) "26" 
7) "Uses for cURL" 
8) "1352619600" 
9) "6" 
10) "The Nature of Ruby" 
11) "1352619200" 
12) "2"

也就是说,GET #会返回元素本身的值。


五、store参数

默认情况下SORT会直接返回排序结果,如果希望保存排序结果,可以使用STORE参 数。如希望把结果保存到sort.result键中:

redis> SORT tag:ruby :posts BY post:*->time DESC GET post:*->title GET post:*->time GET # STORE sort.result 
(integer) 12 
redis> LRANGE sort.result 0 -1 
1) "Windows 8 app designs" 
2) "1352620100" 
3) "12" 
4) "RethinkDB - An open-source distributed database built with love" 
5) "1352620000" 
6) "26" 
7) "Uses for cURL" 
8) "1352619600" 
9) "6" 
10) "The Nature of Ruby" 
11) "1352619200" 
12) "2"

保存后的键的类型为列表类型,如果键已经存在则会覆盖它。加上STORE参数后SORT 命令的返回值为结果的个数。
STORE参数常用来结合EXPIRE命令缓存排序结果,如下面的伪代码:

#判断是否存在之前排序结果的缓存 
$isCacheExists = EXISTS cache.sort 
if $isCacheExists is 1 
	#如果存在则直接返回 
	return LRANGE cache.sort, 0, -1
else #如果不存在,则使用 SORT命令排序并将结果存入 cache.sort键中作为缓存 
   $sortResult = SORT some.list STORE cache.sort 
   #设置缓存的过期时间为 10分钟 
   EXPIRE cache.sort, 600 
   #返回排序结果 
   return $sortResult

六、性能优化

SORT是Redis中最强大最复杂的命令之一,如果使用不好很容易成为性能瓶颈。SORT命 令的时间复杂度是O(n+mlog(m)),其中n表示要排序的列表(集合或有序集合)中的元素个 数,m表示要返回的元素个数。当n较大的时候SORT命令的性能相对较低,并且Redis在排序 前会建立一个长度为n 的容器来存储待排序的元素,虽然是一个临时的过程,但如果同时 进行较多的大数据量排序操作则会严重影响性能。

所以开发中使用SORT命令时需要注意以下几点。

  1. 尽可能减少待排序键中元素的数量(使N尽可能小)。
  2. 使用LIMIT参数只获取需要的数据(使M尽可能小)。
  3. 如果要排序的数据数量较大,尽可能使用STORE参数将结果缓存。