Redis的有序集合(sorted set)同时具有“有序”和“集合”两种性质,这种数据结构中的每个元素都由一个成员和一个与成员相关联的分值组成,其中成员以字符串方式存储,而分值则以64位双精度浮点数格式存储。
作为例子,图6-1展示了一个记录薪水数据的有序集合,而图6-2则展示了一个记录水果价格的有序集合。
图6-1 记录薪水数据的有序集合
图6-2 记录水果价格的有序集合
与集合一样,有序集合中的每个成员都是独一无二的,同一个有序集合中不会出现重复的成员。与此同时,有序集合的成员将按照它们各自的分值大小进行排序:比如,分值为3.14的成员将小于分值为10.24的成员,而分值为999的成员也会小于分值为10086的成员。有序集合的分值除了可以是数字之外,还可以是字符串"+inf"或者"-inf",这两个特殊值分别用于表示无穷大和无穷小。
需要注意的是,虽然同一个有序集合不能存储相同的成员,但不同成员的分值却可以是相同的。当两个或多个成员拥有相同的分值时,Redis将按照这些成员在字典序中的大小对其进行排列:举个例子,如果成员"apple"和成员"zero"都拥有相同的分值100,那么Redis将认为成员"apple"小于成员"zero",这是因为在字典序中,字母"a"开头的单词要小于字母"z"开头的单词。
有序集合是Redis提供的所有数据结构中最为灵活的一种,它可以以多种不同的方式获取数据,比如根据成员获取分值、根据分值获取成员、根据成员的排名获取成员、根据指定的分值范围获取多个成员等。
本章接下来将对有序集合的各个命令进行介绍,并展示如何使用这些命令实现排行榜、时间线、商品推荐和自动补全等功能。
1.1 ZADD:添加或更新成员
通过使用ZADD命令,用户可以向有序集合添加一个或多个新成员:
ZADD sorted_set score member [score member ...]
在默认情况下,ZADD命令将返回成功添加的新成员数量作为返回值。
举个例子,如果我们对不存在的键salary执行以下命令:
redis> ZADD salary 3500 "peter" 4000 "jack" 2000 "tom" 5500 "mary"
(integer) 4 -- 这个命令向有序集合新添加了4个成员
那么命令将创建出一个包含4个成员的有序集合,如图6-3所示。
图6-3 通过执行ZADD命令创建出的有序集合
1.1.1 更新已有成员的分值
ZADD命令除了可以向有序集合添加新成员之外,还可以对有序集合中已存在成员的分值进行更新:在默认情况下,如果用户在执行ZADD命令时,给定成员已经存在于有序集合中,并且给定的分值和成员现有的分值并不相同,那么ZADD命令将使用给定的新分值去覆盖现有的旧分值。
举个例子,对于图6-3所示的有序集合来说,如果我们执行以下命令:
redis> ZADD salary 5000 "tom"
(integer) 0 -- 因为这是一次更新操作,没有添加任何新成员,所以命令返回0
那么"tom"成员的分值将从原来的2000变为5000,更新后的有序集合如图6-4所示。
图6-4 更新之后的有序集合
1.1.2 指定要执行的操作
从Redis 3.0.2版本开始,Redis允许用户在执行ZADD命令时,通过使用可选的XX选项或者NX选项来显式地指示命令只执行更新操作或者只执行添加操作:
ZADD sorted_set [XX|NX] score member [score member ...]
这两个选项的功能如下:
在给定XX选项的情况下,ZADD命令只会对给定成员当中已经存在于有序集合的成员进行更新,而那些不存在于有序集合的给定成员则会被忽略。换句话说,带有XX选项的ZADD命令只会对有序集合已有的成员进行更新,而不会向有序集合添加任何新成员。
在给定NX选项的情况下,ZADD命令只会把给定成员当中不存在于有序集合的成员添加到有序集合里面,而那些已经存在于有序集合中的给定成员则会被忽略。换句话说,带有NX选项的ZADD命令只会向有序集合添加新成员,而不会对已有的成员进行任何更新。
举个例子,对于图6-4所示的有序集合来说,执行以下命令只会将已有成员"jack"的分值从原来的4000改为4500,而命令中出现的新成员"bob"则不会被添加到有序集合中:
redis> ZADD salary XX 4500 "jack" 3800 "bob"
(integer) 0
图6-5展示了命令执行之后的salary
图6-5 对成员jack的分值进行更新之后的有序集合
有序集合,注意"bob"并没有被添加到有序集合当中。
如果我们对图6-5所示的有序集合执行以下命令:
redis> ZADD salary NX 1800 "jack" 3800 "bob"
(integer) 1
那么ZADD命令将把新成员"bob"添加到有序集合里面,但并不会改变已有成员"jack"的分值,命令执行后的salary有序集合如图6-6所示。
图6-6 添加bob成员之后的salary有序集合
1.1.3 返回被修改成员的数量
在默认情况下,ZADD命令会返回新添加成员的数量作为返回值,但是从Redis 3.0.2版本开始,用户可以通过给定CH选项,让ZADD命令返回被修改(changed)成员的数量作为返回值:
ZADD sorted_set [CH] score member [score member ...]
“被修改成员”指的是新添加到有序集合的成员,以及分值被更新了的成员。
举个例子,对于图6-6所示的有序集合来说,执行以下命令将得到返回值2,表示这个命令修改了两个成员:
redis> ZADD salary CH 3500 "peter" 4000 "bob" 9000 "david"
(integer) 2
被修改的成员分别为"bob"和"david",前者的分值从原来的3800改成了4000,而后者则被添加到了有序集合中。与此相反,因为成员"peter"已经存在于有序集合当中,并且它的分值已经是3500,所以命令没有对它做任何修改。图6-7展示了这条命令执行之后的salary有序集合。
图6-7 添加david成员并修改bob成员分值之后的salary有序集合
1.1.4 其他信息
复杂度:O(M*log(N)),其中M为给定成员的数量,而N则为有序集合包含的成员数量。
版本要求:不带任何选项的ZADD命令从Redis 1.2.0版本开始可用,带有NX、XX、CH等选项的ZADD命令从Redis 3.0.2版本开始可用。Redis 2.4版本以前的ZADD命令只允许用户给定一个成员,而Redis 2.4及以上版本的ZADD命令则允许用户给定一个或多个成员。
1.2 ZREM:移除指定的成员
通过使用ZREM命令,用户可以从有序集合中移除指定的一个或多个成员以及与这些成员相关联的分值:
ZREM sorted_set member [member ...]
ZREM命令会返回被移除成员的数量作为返回值。
举个例子,通过执行以下命令,我们可以移除salary有序集合中的成员"peter":
redis> ZREM salary "peter"
(integer) 1 -- 移除了一个成员
执行以下命令将移除salary有序集合中的成员"tom"以及"jack":
redis> ZREM salary "tom" "jack"
(integer) 2 -- 移除了两个成员
图6-8展示了Redis在执行以上两个ZREM命令调用时,salary有序集合的变化过程。
[
图6-8 salary有序集合在执行ZREM命令时的变化过程
1.2.1 忽略不存在的成员
如果用户给定的某个成员并不存在于有序集合中,那么ZREM将自动忽略该成员。
比如,执行以下命令并不会导致salary集合中的任何成员被移除,因为这里给定的成员"john"、"harry"和"lily"都不存在于salary有序集合:
redis> ZREM salary "john" "harry" "lily"
(integer) 0 -- 没有任何成员被移除
1.2.2 其他信息
复杂度:O(M*log(N)),其中M为给定成员的数量,N为有序集合包含的成员数量。
版本要求:ZREM命令从Redis 1.2.0版本开始可用。Redis 2.4版本以前的ZREM命令只允许用户给定一个成员,而Redis 2.4及以上版本的ZREM命令则允许用户给定一个或多个成员。
1.3 ZSCORE:获取成员的分值
通过使用ZSCORE命令,用户可以获取与给定成员相关联的分值:
ZSCORE sorted_set member
图6-9 salary有序集合
举个例子,对于图6-9所示的有序集合来说,执行以下命令可以分别获取成员"peter"、"jack"以及"mary"的分值:
redis> ZSCORE salary "peter"
"3500"
redis> ZSCORE salary "jack"
"4000"
redis> ZSCORE salary "mary"
"5500"
相反,如果用户给定的有序集合并不存在,或者有序集合中并未包含给定的成员,那么ZSCORE命令将返回空值:
redis> ZSCORE not-exists-sorted-set not-exists-member
(nil) -- 给定的有序集合并不存在
redis> ZSCORE salary "lily"
(nil) -- salary 有序集合并未包含成员"lily"
其他信息
复杂度:O(1)。
版本要求:ZSCORE命令从Redis 1.2.0版本开始可用。
1.4 ZINCRBY:对成员的分值执行自增或自减操作
通过使用ZINCRBY命令,用户可以对有序集合中指定成员的分值执行自增操作,为其加上指定的增量:
ZINCRBY sorted_set increment member
ZINCRBY命令在执行完自增操作之后,将返回给定成员当前的分值。
图6-10 执行ZINCRBY命令之前的salary有序集合
举个例子,对于图6-10所示的有序集合来说,我们可以使用以下命令,对它的成员分值执行自增操作:
redis> ZINCRBY salary 1000 "tom" -- 将成员"tom"的分值加上1000
"3000" -- 成员"tom"现在的分值为3000
redis> ZINCRBY salary 1500 "peter" -- 将成员"peter"的分值加上1500
"5000" -- 成员"peter"现在的分值为5000
redis> ZINCRBY salary 3000 "jack" -- 将成员"jack"的分值加上3000
"7000" -- 成员"jack"现在的分值为7000
图6-11展示了salary有序集合在执行以上几个ZINCRBY命令之后的样子。
图6-11 执行ZINCRBY命令之后的salary有序集合
1.4.1 执行自减操作
因为Redis只提供了对分值执行自增操作的ZINCRBY命令,但并没有提供相应的对分值执行自减操作的命令,所以如果我们需要减少一个成员的分值,那么可以将一个负数增量传递给ZINCRBY命令,从而达到对分值执行自减操作的目的。
比如,通过执行以下命令,我们可以将成员"peter"的分值从5000修改为2000:
redis> ZINCRBY salary -3000 "peter"
"2000"
图6-12展示了在命令执行之前以及之后,salary有序集合的变化过程。
图6-12 对成员"peter"的分值执行自减操作
1.4.2 处理不存在的键或者不存在的成员
如果用户在执行ZINCRBY命令时,给定成员并不存在于有序集合中,或者给定的有序集合并不存在,那么ZINCRBY命令将直接把给定的成员添加到有序集合中,并把给定的增量设置为该成员的分值,效果相当于执行ZADD命令。
举个例子,当我们对不存在"lily"成员的salary有序集合执行以下命令时:
redis> ZINCRBY salary 1500 "lily"
"1500"
ZINCRBY命令将把"lily"成员添加到salary有序集合中,并把给定的增量1500设置为"lily"成员的分值,效果相当于执行ZADD salary 1500"lily"。
如果我们对不存在的有序集合blog-timeline执行以下命令:
redis> ZINCRBY blog-timeline 1447063985 "blog_id::10086"
"1447063985"
那么ZINCRBY命令将创建出空白的blog-timeline有序集合,并把分值为1447063985的成员"blog_id::10086"添加到这个有序集合中,效果相当于执行命令ZADD blog-timeline 1447063985"blog_id::10086"。
1.4.3 其他信息
复杂度:O(log(N)),其中N为有序集合包含的成员数量。
版本要求:ZINCRBY命令从Redis 1.2.0版本开始可用。
1.5 ZCARD:获取有序集合的大小
通过执行ZCARD命令可以取得有序集合的基数,即有序集合包含的成员数量:
ZCARD sorted_set
比如,以下代码展示了如何使用ZCARD命令去获取salary、fruit-prices和blog-timeline这3个有序集合包含的成员数量:
redis> ZCARD salary
(integer) 4 -- 这个有序集合包含4个成员
redis> ZCARD fruit-prices
(integer) 7 -- 这个有序集合包含7个成员
redis> ZCARD blog-timeline
(integer) 3 -- 这个有序集合包含3个成员
如果用户给定的有序集合并不存在,那么ZCARD命令将返回0作为结果:
redis> ZCARD not-exists-sorted-set
(integer) 0
其他信息
复杂度:O(1)。
版本要求:ZCARD命令从Redis 1.2.0版本开始可用。
1.6 ZRANK、ZREVRANK:获取成员在有序集合中的排名
通过ZRANK命令和ZREVRANK命令,用户可以取得给定成员在有序集合中的排名:
ZRANK sorted_set member
ZREVRANK sorted_set member
其中ZRANK命令返回的是成员的升序排列排名,即成员在按照分值从小到大进行排列时的排名,而ZREVRANK命令返回的则是成员的降序排列排名,即成员在按照分值从大到小进行排列时的排名。
举个例子,对于图6-13所示的有序集合来说,我们可以通过执行以下命令来获取成员"peter"和"tom"在有序集合中的升序排列排名:
redis> ZRANK salary "peter"
(integer) 0
redis> ZRANK salary "tom"
(integer) 3
图6-13 salary有序集合
而执行以下命令则可以获取他们在有序集合中的降序排列排名:
redis> ZREVRANK salary "peter"
(integer) 4
redis> ZREVRANK salary "tom"
(integer) 1
图6-14展示了salary集合的各个成员在执行ZRANK命令和ZREVRANK命令时的结果。
图6-14 salary有序集合的各个成员以及它们在执行ZRANK命令和ZREVRANK命令时的结果
1.6.1 处理不存在的键或者不存在的成员
如果用户给定的有序集合并不存在,或者用户给定的成员并不存在于有序集合当中,那么ZRANK命令和ZREVRANK命令将返回一个空值。以下是两个ZRANK命令的例子:
redis> ZRANK salary "harry"
(nil)
redis> ZRANK not-exists-sorted-set not-exists-member
(nil)
1.6.2 其他信息
复杂度:O(log(N)),其中N为有序集合包含的成员数量。
版本要求:ZRANK命令和ZREVRANK命令从Redis 2.0.0版本开始可用。
1.7 ZRANGE、ZREVRANGE:获取指定索引范围内的成员
通过ZRANGE命令和ZREVRANGE命令,用户可以以升序排列或者降序排列方式,从有序集合中获取指定索引范围内的成员:
ZRANGE sorted_set start end
ZREVRANGE sorted_set start end
其中ZRANGE命令用于获取按照分值大小实施升序排列的成员,而ZREVRANGE命令则用于获取按照分值大小实施降序排列的成员。命令中的start索引和end索引指定的是闭区间索引范围,也就是说,位于这两个索引上的成员也会包含在命令返回的结果当中。
举个例子,如果我们想要获取salary有序集合在按照升序排列成员时,位于索引0至索引3范围内的成员,那么可以执行以下命令:
redis> ZRANGE salary 0 3
1) "peter"
2) "bob"
3) "jack"
4) "tom"
图6-15展示了这个ZRANGE命令的执行过程。
图6-15 ZRANGE命令执行示意图
如果我们想要获取salary有序集合在按照降序排列成员时,位于索引2至索引4范围内的成员,那么可以执行以下命令:
redis> ZREVRANGE salary 2 4
1) "jack"
2) "bob"
3) "peter"
图6-16展示了这个ZREVRANGE命令的执行过程。
图6-16 ZREVRANGE命令的执行示意图
1.7.1 使用负数索引
与第4章中介绍过的LRANGE命令类似,ZRANGE命令和ZREVRANGE命令除了可以接受正数索引之外,还可以接受负数索引。
比如,如果我们想要以升序排列的方式获取salary有序集合的最后3个成员,那么可以执行以下命令:
redis> ZRANGE salary -3 -1
1) "jack"
2) "tom"
3) "mary"
图6-17展示了这个ZRANGE命令的执行过程。
图6-17 使用负数索引的ZRANGE命令的执行示意图
与此类似,如果我们想要以降序排列的方式获取salary有序集合的最后一个成员,那么可以执行以下命令:
redis> ZREVRANGE salary -1 -1
1) "peter"
图6-18展示了这个ZREVRANGE命令的执行过程。
最后,如果我们想要以升序排列或者降序排列的方式获取salary有序集合包含的所有成员,那么只需要将起始索引设置为0,结束索引设置为-1,然后调用ZRANGE命令或者ZREVRANGE命令即可:
redis> ZRANGE salary 0 -1 -- 以升序排列方式获取所有成员
1) "peter"
2) "bob"
3) "jack"
4) "tom"
5) "mary"
redis> ZREVRANGE salary 0 -1 -- 以降序排列方式获取所有成员
1) "mary"
2) "tom"
3) "jack"
4) "bob"
5) "peter"
图6-18 使用负数索引的ZREVRANGE命令的执行示意图
1.7.2 获取成员及其分值
在默认情况下,ZRANGE命令和ZREVRANGE命令只会返回指定索引范围内的成员,如果用户想要在获取这些成员的同时也获取与之相关联的分值,那么可以在调用ZRANGE命令或者ZREVRANGE命令的时候,给定可选的WITHSCORES选项:
ZRANGE sorted_set start end [WITHSCORES]
ZREVRANGE sorted_set start end [WITHSCORES]
以下代码展示了如何获取指定索引范围内的成员以及与这些成员相关联的分值:
redis> ZRANGE salary 0 3 WITHSCORES
1) "peter"
2) "3500" -- 成员"peter"的分值
3) "bob"
4) "3800" -- 成员"bob"的分值
5) "jack"
6) "4500" -- 成员"jack"的分值
7) "tom"
8) "5000" -- 成员"tom"的分值
redis> ZREVRANGE salary 2 4 WITHSCORES
1) "jack"
2) "4500" -- 成员"jack"的分值
3) "bob"
4) "3800" -- 成员"bob"的分值
5) "peter"
6) "3500" -- 成员"peter"的分值
1.7.3 处理不存在的有序集合
如果用户给定的有序集合并不存在,那么ZRANGE命令和ZREVRANGE命令将返回一个空列表:
redis> ZRANGE not-exists-sorted-set 0 10
(empty list or set)
redis> ZREVRANGE not-exists-sorted-set 0 10
(empty list or set)
1.7.4 其他信息
复杂度:O(log(N)+M),其中N为有序集合包含的成员数量,而M则为命令返回的成员数量。
版本要求:ZRANGE命令和ZREVRANGE命令从Redis 1.2.0版本开始可用。
示例:排行榜
我们在网上常常会看到各式各样的排行榜,比如,在音乐网站上可能会看到试听排行榜、下载排行榜、华语歌曲排行榜和英语歌曲排行榜等,而在视频网站上可能会看到观看排行榜、购买排行榜、收藏排行榜等,甚至连项目托管网站GitHub都提供了各种不同的排行榜,以此来帮助用户找到近期最受人瞩目的新项目。
代码清单6-1展示了一个使用有序集合实现的排行榜程序:
这个程序使用ZADD命令向排行榜中添加被排序的元素及其分数,并使用ZREVRANK命令去获取元素在排行榜中的排名,以及使用ZSCORE命令去获取元素的分数。
当用户不再需要对某个元素进行排序的时候,可以调用由ZREM命令实现的remove()方法,从排行榜中移除该元素。
如果用户想要修改某个被排序元素的分数,那么只需要调用由ZINCRBY命令实现的increase_score()方法或者decrease_score()方法即可。
当用户想要获取排行榜前N位的元素及其分数时,只需要调用由ZREVRANGE命令实现的top()方法即可。
代码清单6-1 使用有序集合实现的排行榜程序:/sorted_set/ranking_list.py
class RankingList:
def __init__(self, client, key):
self.client = client
self.key = key
def set_score<