集合类型提供了强大的集合操作命令,但是如果需要排序就要用到有序集合类型。Redis的作者在设计Redis的命令时就考虑到了不同数据结果类型的使用场景,对于不常用到或者在不损失过多性能的前提下可以使用现有命令来实现功能,Redis就不会单独提供命令来实现。这一原则使得Redis在拥有强大功能的同时保持相对精简的命令。
有序集合常见的使用场景是大数据排序,如游戏玩家的排行榜,所以很少会需要获得键中的全部数据。同样Redis认为开发者在做完交集、并集运算后不需要直接获得全部结果,而是会希望将结果存入新的键中一遍后续处理。这解释了为什么排序集合只有ZINTERSTORE和ZUNIONSTORE命令而没有ZINTER和ZUNION命令。
当然实际使用中确实会遇到需要获得集合运算结果的情况,除了等待Redis加入相关命令,我们还可以使用MULTI,ZINTERSTROE,ZRANGE,DEL和EXEC这5个命令自己实现ZINTER:
MULTI
ZINTERSTORE tempKey ...
ZRANGE tempKey ...
DEL tempKey
EXEC
1. SORT命令
127.0.0.1:6379> SORT tag:ruby:posts
1)"2"
2)"6"
3)"12"
4)"26"
除了集合类型,SORT命令还可以对列表类型和有序集合类型进行排序:
127.0.0.1:6379> LPUSH mylist 4 2 6 1 3 7
(integer) 6
127.0.0.1:6379> SORT mylist
1) "1"
2) "2"
3) "3"
4) "4"
5) "6"
6) "7"
在对有序集合类型排序时会忽略元素的分值,只针对元素自身值进行排序。例如,对有序集合排序:
127.0.0.1:6379> ZADD myzset 50 2 40 3 20 1 60 5
(integer) 4
127.0.0.1:6379> SORT myzset
1) "1"
2) "2"
3) "3"
4) "5"
除了可以排列数字之外,SORT命令还可以通过ALPHA参数实现按照字典顺序排列非数字元素,例如:
127.0.0.1:6379> LPUSH mylistalpha a c e d B C A
(integer) 7
127.0.0.1:6379> SORT mylistalpha
(error) ERR One or more scores can't be converted into double
127.0.0.1:6379> SORT mylistalpha ALPHA
1) "A"
2) "B"
3) "C"
4) "a"
5) "c"
6) "d"
7) "e"
如果没有加ALPHA参数的话,SORT命令会尝试将所有元素转换成双精度浮点数来比较,如果无法转换会提示错误。另一个参数是DESC,即从大到小排列。
127.0.0.1:6379> SORT mylistalpha ALPHA DESC
1) "e"
2) "d"
3) "c"
4) "a"
5) "C"
6) "B"
7) "A"
SORT命令还支持LIMIT参数来返回指定范围的结果,用法和SQL语句一样,
LIMIT offset count,表示跳过前offset个元素并获取之后的count个元素。
SORT mylistalpha ALPHA DESC LIMIT 3 2
1) "a"
2) "C"
2. BY参数
127.0.0.1:6379>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的顺序。
除了散列类型之外,参考键还可以使字符串类型,例如:
127.0.0.1:6379> LPUSH sortbylist 2 1 3
(integer) 3
127.0.0.1:6379> SET itemscore:1 50
OK
127.0.0.1:6379> SET itemscore:2 100
OK
127.0.0.1:6379> SET itemscore:3 -10
OK
127.0.0.1:6379> SORT sortbylist BY itemscore:* DESC
1) "2"
2) "1"
3) "3"
当参考键名不包含”*“时(即常量键名,与元素无关),SORT命令将不会执行排序操作,因为Redis认为这种情况没有意义(因为所有要比较的值都意义),如果几个元素的参考键相同,则SORT命令会再比较元素本身的值来决定元素的顺序。例如:
27.0.0.1:6379> LPUSH sortbylist 4
(integer) 4
127.0.0.1:6379> SET itemscore:4 50
OK
127.0.0.1:6379> LRANGE sortbylist 0 -1
1) "4"
2) "3"
3) "1"
4) "2"
127.0.0.1:6379> SORT sortbylist BY anytext
1) "4"
2) "3"
3) "1"
4) "2"
127.0.0.1:6379> SORT sortbylist BY itemscore:* DESC
1) "2"
2) "4"
3) "1"
4) "3"
上例中anytext是常量键名(甚至anytext键可以不存在),此时SORT的结果与LRANGE的结果相同,没有执行排序操作。在不需要排序但需要借助SORT命令获得与元素相关的数据时,常量键名很有用。而元素”4“的参考键itemscore:4的值和元素”1“的参考键itemscore:1的值都是50,所以SORE命令还会比较”4“和”1“,元素本身的大小决定两者的顺序。
当某个元素的参考键不存在,会默认参考键的值为0:
127.0.0.1:6379> LPUSH sortbylist 5
(integer) 5
127.0.0.1:6379> SORT sortbylist BY itemscore:* DESC
1) "2"
2) "4"
3) "1"
4) "5"
5) "3"
127.0.0.1:6379>
参考键虽然只支持散列类型,但是”*“只能在”->"符号前面(即键名部分)才有用,在“->"后(即字段名部分)会被当成字段名本身而不会作为占位符被元素的值替换,即常量键名。但实际运行时,会发现有趣的结果:
<pre name="code" class="plain"><pre name="code" class="plain">127.0.0.1:6379> SORT sortbylist BY somekey->somefield:*
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
前面提高了当参考键名是常量键名时SORT命令将不会执行排序操作,然而上例中确实进行了排序,而且只对元素本身进行排序。这是因为Redis判断参考键名是不是常量键名的方式是判断参考键名中是否含有”*“,而somekey->somefield:* 中包含”*“,所以不是常量键名。所以在排序的时候Redis对每个元素都会读取somekey中的somefile*字段(”*“不会被替换),无论是否获取其值,每个元素的参考值都相同,所以Redis会按照元素本身的大小排序。
3. GET参数
根据发布时间对文章ID排好序之后,可用HGET命令来获取文章的标题,而一种更简单的操作室借助SORT命令的参数GET。GET不影响排序,它的作用是使SORT命令的返回结果不再是元素自身的值,而是GET参数中指定的键值。GET参数的规则与BY参数一样,GET参数也支持字符串类型和散列类型的键,并使用”*“作为占位符。要实现排名后直接返回文章ID对应的标题,可以这样:
127.0.0.1:6379> SORT tag:ruby:posts BY post:*->time DESC GET post:*->title
1)"Windows 8 app designs"
2)"RethinkDB"
3)"Uses for cURL"
4)"The Nature of Ruby"
在一个SORT命令中可以有多个GET参数(而BY参数只能有一个),所以还可以这样:
</pre><pre name="code" class="plain"><pre name="code" class="plain"><span style="font-family: Arial, Helvetica, sans-serif;"> SORT tag:ruby:posts BY post:*->time DESC GET post:*->title GET post:*->time</span>
1)"Windows 8 app designs"2)"1419020100"3)"RethinkDB"4)"141902000"5)"Uses for cURL"6)"1419019600"7)"The Nature of Ruby"8)“1419019200"
如果还要返回文章ID怎么办?用GET # 即可,也就是说GET #返回元素本身的值。
4. STOR参数
127.0.0.1:6379>SORT tag:ruby:posts BY post:*->time GET post:*->title GET post:*->time GET # STORE sort.result
(intger)12
保存后的键的类型为列表类型,如果键已经存在则会覆盖它,加上STORE参数后SORT命令会返回结果的个数。
$isCacheExists = EXISTS cache.sort
if $isCacheExist
return LRANGE cache.sort,0,-1
else
sortResult = SORT some.list STORE cache.sort
EXPIRE cache.sort 60
return $sortResult
SORT是Redis中最强大最复杂的命令之一,如果使用不好很容易成为性能的瓶颈。SORT命令的时间复杂度为O(n+mlogm),其中n表示要排序的列表(集合或者有序集合)中元素的个数,m表示要返回的元素的个数。当n较大的时候SORT命令的性能相对较低,并且Redis在排序前会建立一个长度为n的容器来存储待排序的元素(有一个例外是当键的类型为有序集合且参考键为常量键名时容器大小为m而不是n),虽然是一个临时过程,但如果同时进行较多大数据量排序操作则会严重影响性能。
- 尽量减少待排序键中元素的数量(使n尽可能小)
- 使用LIMIT参数来只取需要的数据
- 如果要排的数据量较大,尽可能使用STORE参数将结果缓存