排序
对于排序的场景,在业务中会大量使用到,对于Redis,如果使用了有序集合,那么排序问题很容易解决,并且得分可以根据实际的业务,由时间、点赞、费用、排行等等进行转化,支持的业务范围也比较广泛。
除了有序集合的排序外,Redis另外提供了排序的指令SORT
。SORT指令可以针对列表类型、集合类型、有序集合类型的键进行排序,同时借助BY
指令,Redis的SORT...BY
指令可以完成一部分类似MYSQL中的关系查询排序效果。
SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern...]] [ASC|DESC] [ALPHA] [STORE destinion]
SORT排序比较简单,排序之后立即将结果返回,以列表的形式返回。假定有一个资源ID列表,资源ID使用MYSQL的自增主键,从1开始累增。如果将这些ID放置到了集合中,那么在使用按入库时间降序排列时,使用集合是无法做到的,此时可以使用SORT。使用列表效果也一致。参考下面的例子。
# 结合
SADD resourceids 2 3 1 5 4
# (integer) 5
SORT resourceids # 默认为ASC
# 1) "1"
# 2) "2"
# 3) "3"
# 4) "4"
# 5) "5"
SORT resourceids DESC
# 1) "5"
# 2) "4"
# 3) "3"
# 4) "2"
# 5) "1"
# 列表
LPUSH lst 2 3 1 5 4
# (integer) 5
SORT lst DESC
# 1) "5"
# 2) "4"
# 3) "3"
# 4) "2"
# 5) "1"
从上列中看出,使用SORT指令时,列表、集合或者是有序集合,指令功能基本是一致的。而对有序集合使用SORT进行排序时,将会忽略SCORE,只对元素字面值进行排序,此时效果和集合或列表使用方式及表现是一致的。
ZADD zlst 123 2 125 3 120 1 125 5 124 4
# (integer) 5
SORT zlst DESC
# 1) "5"
# 2) "4"
# 3) "3"
# 4) "2"
# 5) "1"
一般情况下资源类的ID都是数字,使用SORT进行排序能完成快速排序的目的,但有时候也会有字符串类的ID集合,Redis命令也可以通过指定ALPHA属性实现字符串排序。这个时候只是按非数字字符串本身进行排序,会丢掉数据库自增的顺序属性。如果SORT没有携带ALPHA参数,那么SORT仍然会使用数字类型排序,如果是非数字字符串集合,那么将会返回错误信息。
LPUSH alphaLst hello world lua redis c# java JAVA
# (integer) 7
SORT alphaLst ALPHA
# 1) "c#"
# 2) "hello"
# 3) "java"
# 4) "JAVA"
# 5) "lua"
# 6) "redis"
# 7) "world"
当结果较多时,排序时,可以使用LIMIT参数进行限制返回的数据数量,可以实现分页效果,该参数的使用方式同MySQL的LIMIT基本一致。offset从0开始
SORT lst DESC
# 1) "5"
# 2) "4"
# 3) "3"
# 4) "2"
# 5) "1"
SORT lst LIMIT 0 2 DESC
# 1) "5"
# 2) "4"
SORT lst LIMIT 2 2 DESC
# 3) "3"
# 4) "2"
如果资源ID已经存储到了列表中,如上例中的[1,2,3,4,5]五个资源,同时使用散列数据结构存储了这些资源的详情,同时详情中包含了一个点击得分的字段score,该字段为数字类型。如果有需求要实现按用户的点击得分进行排序,此时需要借助BY参数实现。
# 资源详情散列数据结构及数据如下,散列的key为`resource:id`,散列的field为字段名,这里只有title和score
[
"resource:5" : {
title: "efg",
score: 125
} ,
"resource:4" : {
title: "bcd",
score: 230
} ,
"resource:3" : {
title: "ddd"
score: 125
} ,
"resource:2" : {
title: "ggg",
score: 100
} ,
"resource:1" : {
title: "fff",
score: 156
}
]
要实现资源点击得分倒序排列,采用如下的指令。当排序中参考字段某几个元素相同时,如本例中5、3得分都是125,那么在排序时,遇到相同的,会再次参考元素本身进行排序
SORT lst BY resource:*->score DESC
# 1) "4"
# 2) "1"
# 3) "5"
# 4) "3"
# 5) "2"
BY的使用方法为:BY pattern
,pattern为要进行排序的参考的键。该字段的标识方法为:键名->字段名
,可以是散列类型,也可以是字符串类型。当出现BY参数时,Redis会使用列表中元素,去替换BY后面字段中的*
,并根据实际的字段获取值,参与到排序中,如上述示例中,将分别获取resource:1
、resource:2
、resource:3
、resource:4
、resource:5
等散列中的score
字段的值,并参与到排序中。如果参考字段时散列类型那么对于通配符*
只能存在于->之前。如果使用字符串作为参考字段时,ID列表不需要变更,只要将score存储到字符串中,其数据如下:
[
"strscore:5": 125,
"strscore:4": 230,
"strscore:3": 125,
"strscore:2": 100,
"strscore:1": 156,
]
排序指令如下,得到和散列结构详情一样的结果。
SORT lst BY strscore:* DESC
# 1) "4"
# 2) "1"
# 3) "5"
# 4) "3"
# 5) "2"
当参考键中不包含*
时,常量键值,此时SORT将不会进行排序。
SORT lst BY a DESC
# 1) "2"
# 2) "3"
# 3) "1"
# 4) "5"
# 5) "4"
如果参考字段中某个值不存在时,会以默认值0
进行替代,上例中抹去resource:1
字段,在进行排序
DEL strscore:1
# (integer) 1
SORT lst BY strscore:* DESC
# 1) "4"
# 2) "5"
# 3) "3"
# 4) "2"
# 5) "1"
GET参数可以应用于SORT BY结构中,用于指定返回的信息。这个功能在做列表时非常有用,如果有需求要显示点击排行列表,那么一方面需要按点击排序,另外一方面需要返回资源名称以及资源ID;如果可以在SORT指令中一并返回,就不需要业务先做排序,拿到排序后的ID集合,在根据ID获取资源信息,取出title信息。
SORT指令的GET实现了这个功能,根据字符串或散列对象分别获取,其规则为
# 散列
GET key:*->field
get resource:*->title
# 字符串
GET key:*
# 获取列表中的ID
GET #
根据之前的数据结构,从散列表中resource:*
和集合resourceids
中按评分进行排序,并获取id、title和评分的列表。
SORT resourceids BY resource:*->score DESC GET resource:*->title GET resource:*->score GET # LIMIT 0 2
# 1) "bcd"
# 2) "230"
# 3) "4"
# 4) "efg"
# 5) "125"
# 6) "5"
如果有几个GET,如上面例子中有三个GET,那么返回结果列表中,每一个结果就有几条显示,(3条,title、score、id)。
一般情况下,SORT主要用于对列表进行排序,获取部分结果,如排行榜之类的。这时显示的一般是列表,不需要返回跟多的详情信息,并且有时间要求(排行榜每一小时更新一次)。这种情况不需要业务系统每次都排序,获取字段组装数据,SORT指令提供给了最后一个参数STORE
,该参数可以将结果存储到指定的键中,同时设置有效期后,将可以满足上述的业务需求。其参数使用方式为:... STORE key
,加了该参数后,其返回结果将不在是列表,而是存储的结果数量,以列表数据存储。
SORT resourceids BY resource:*->score DESC GET resource:*->title GET resource:*->score GET # LIMIT 0 2 STORE my_top
# (integer) 6
LRANGE my_top 0 -1
# 1) "bcd"
# 2) "230"
# 3) "4"
# 4) "efg"
# 5) "125"
# 6) "5"
SORT指令是目前为止使用到的最复杂的指令,且提供了类似MySQL一样的关联查询操作。在某些操作的时候,可能会导致查询缓慢,由于Redis时单线程,也可能一个指令导致Redis的阻塞。
SORT指令的时间负责度:
O(n+mlogm)
,n为要排序的列表数量,m为要SORT返回的元素数量(LIMIT),随着n的增大,该指令的效率降低。同时排序时,Redis也会建立一个长度为n的临时列表存储这些元素,从这方面看数据越大,性能就越慢。为了提高性能,只能从这两方面入手:降低n的数量,或者降低m的数量(LIMIT可解决);数量较大时,使用STORE参数将结果缓存,在一定程度上可以减少STORE的调用次数。