大家好,我是腾意。

Redis的有序集合(sorted set)同时具有“有序”和“集合”两种性质,这种数据结构中的每个元素都由一个成员和一个与成员相关联的分值组成,其中成员以字符串方式存储,而分值则以64位双精度浮点数格式存储。

作为例子,图6-1展示了一个记录薪水数据的有序集合,而图6-2则展示了一个记录水果价格的有序集合。

img

图6-1 记录薪水数据的有序集合

img

图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所示。

img

图6-3 通过执行ZADD命令创建出的有序集合

1.1.1 更新已有成员的分值

ZADD命令除了可以向有序集合添加新成员之外,还可以对有序集合中已存在成员的分值进行更新:在默认情况下,如果用户在执行ZADD命令时,给定成员已经存在于有序集合中,并且给定的分值和成员现有的分值并不相同,那么ZADD命令将使用给定的新分值去覆盖现有的旧分值。

举个例子,对于图6-3所示的有序集合来说,如果我们执行以下命令:

redis> ZADD salary 5000 "tom"
(integer) 0    -- 因为这是一次更新操作,没有添加任何新成员,所以命令返回0

那么"tom"成员的分值将从原来的2000变为5000,更新后的有序集合如图6-4所示。

img

图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

img

图6-5  对成员jack的分值进行更新之后的有序集合

有序集合,注意"bob"并没有被添加到有序集合当中。

如果我们对图6-5所示的有序集合执行以下命令:

redis> ZADD salary NX 1800 "jack" 3800 "bob"
(integer) 1

那么ZADD命令将把新成员"bob"添加到有序集合里面,但并不会改变已有成员"jack"的分值,命令执行后的salary有序集合如图6-6所示。

img

图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有序集合。

img

图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有序集合的变化过程。

img

图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

img

图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命令在执行完自增操作之后,将返回给定成员当前的分值。

img

图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命令之后的样子。

img

图6-11 执行ZINCRBY命令之后的salary有序集合

1.4.1 执行自减操作

因为Redis只提供了对分值执行自增操作的ZINCRBY命令,但并没有提供相应的对分值执行自减操作的命令,所以如果我们需要减少一个成员的分值,那么可以将一个负数增量传递给ZINCRBY命令,从而达到对分值执行自减操作的目的。

比如,通过执行以下命令,我们可以将成员"peter"的分值从5000修改为2000:

redis> ZINCRBY salary -3000 "peter"
"2000"

图6-12展示了在命令执行之前以及之后,salary有序集合的变化过程。

img

图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

img

图6-13 salary有序集合

而执行以下命令则可以获取他们在有序集合中的降序排列排名:

redis> ZREVRANK salary "peter"
(integer) 4

redis> ZREVRANK salary "tom"
(integer) 1

图6-14展示了salary集合的各个成员在执行ZRANK命令和ZREVRANK命令时的结果。

img

图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命令的执行过程。

img

图6-15 ZRANGE命令执行示意图

如果我们想要获取salary有序集合在按照降序排列成员时,位于索引2至索引4范围内的成员,那么可以执行以下命令:

redis> ZREVRANGE salary 2 4
1) "jack"
2) "bob"
3) "peter"

图6-16展示了这个ZREVRANGE命令的执行过程。

img

图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命令的执行过程。

img

图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"

img

图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(self, item, score):
        """
        为排行榜中的指定元素设置分数,不存在的元素会被添加到排行榜中
        """
        self.client.zadd(self.key, {item:score})

    def get_score(self, item):
        """
        获取排行榜中指定元素的分数
        """
        return self.client.zscore(self.key, item)

    def remove(self, item):
        """
        从排行榜中移除指定的元素
        """
        self.client.zrem(self.key, item)

    def increase_score(self, item, increment):
        """
        将给定元素的分数增加increment分
        """
        self.client.zincrby(self.key, increment, item)

    def decrease_score(self, item, decrement):
        """
        将给定元素的分数减少decrement分
        """
        # 因为Redis没有直接提供能够减少元素分值的命令
        # 所以这里通过传入一个负数减量来达到减少分值的目的
        self.client.zincrby(self.key, 0-decrement, item)

    def get_rank(self, item):
        """
        获取给定元素在排行榜中的排名
        """
        rank = self.client.zrevrank(self.key, item)
        # 因为Redis元素的排名是以0为开始的,
        # 而现实世界中的排名通常以1为开始,
        # 所以这里在返回排名之前会执行加1操作
        if rank is not None:
            return rank+1

    def top(self, n, with_score=False):
        """
        获取排行榜中得分最高的n个元素,
        如果可选的with_score参数的值为True,那么将元素的分数(分值)也一并返回
        """
        return self.client.zrevrange(self.key, 0, n-1, withscores=with_score)

举个例子,我们可以通过执行以下代码,创建出一个记录歌曲下载次数的排行榜:

>>> from redis import Redis
>>> from ranking_list import RankingList
>>> client = Redis(decode_responses=True)
>>> ranking = RankingList(client, "music download ranking")

接着通过以下代码记录歌曲的名字及其下载次数:

>>> ranking.set_score("ninelie", 3500)
>>> ranking.set_score("StarRingChild", 2700)
>>> ranking.set_score("RE:I AM", 3300)
>>> ranking.set_score("Your voice", 2200)
>>> ranking.set_score("theDOGS", 1800)

然后通过以下代码获取指定歌曲的下载次数,并获知它在排行榜中的位置:

>>> ranking.get_score("ninelie")
3500.0
>>> ranking.get_rank("ninelie")
1

最后还可以通过以下代码获取排行榜前5位的歌曲:

>>> ranking.top(5)
['ninelie', 'RE:I AM', 'StarRingChild', 'Your voice', 'theDOGS']
>>>
>>> ranking.top(5, True)  # 在获取榜单的同时显示歌曲的下载次数
[('ninelie', 3500.0), ('RE:I AM', 3300.0), ('StarRingChild', 2700.0), ('Your voice', 2200.0), ('theDOGS', 1800.0)]

1.8 ZRANGEBYSCORE、ZREVRANGEBYSCORE:获取指定分值范围内的成员

通过使用ZRANGEBYSCORE命令或者ZREVRANGEBYSCORE命令,用户可以以升序排列或者降序排列的方式获取有序集合中分值介于指定范围内的成员:

ZRANGEBYSCORE sorted_set min max

ZREVRANGEBYSCORE sorted_set max min

命令的min参数和max参数分别用于指定用户想要获取的成员的最小分值和最大分值。

不过需要注意的是,ZRANGEBYSCORE命令和ZREVRANGEBYSCORE命令接受min参数和max参数的顺序正好相反:ZRANGEBYSCORE命令先接受min参数然后再接受max参数,而ZREVRANGEBYSCORE命令则是先接受max参数然后再接受min参数。

作为例子,以下代码展示了如何通过ZRANGEBYSCORE命令,以升序排列方式从salary有序集合中获取分值介于3800~5000的成员:

redis> ZRANGEBYSCORE salary 3800 5000
1) "bob"
2) "jack"
3) "tom"

图6-19展示了这个ZRANGEBYSCORE命令的执行过程。

与此类似,以下代码展示了如何通过ZREVRANGEBYSCORE命令,以降序排列方式从salary有序集合中获取分值介于5000~3000的成员:

redis> ZREVRANGEBYSCORE salary 5000 3000
1) "tom"
2) "jack"
3) "bob"
4) "peter"

img

图6-19 ZRANGEBYSCORE命令的执行示意图

图6-20展示了这个ZREVRANGEBYSCORE命令的执行过程。

img

图6-20 ZREVRANGEBYSCORE命令的执行示意图

1.8.1 获取成员及其分值

与ZRANGE命令和ZREVRANGE命令类似,ZRANGEBYSCORE命令和ZREVRANGEBYSCORE命令也可以通过在执行时给定可选的WITHSCORES选项来同时获取成员及其分值:

ZRANGEBYSCORE sorted_set min max [WITHSCORES]

ZREVRANGEBYSCORE sorted_set max min [WITHSCORES]

以下代码展示了两个使用WITHSCORES选项的例子:

redis> ZRANGEBYSCORE salary 3800 5000 WITHSCORES
1) "bob"
2) "3800"    -- 成员"bob"的分值
3) "jack"
4) "4500"    -- 成员"jack"的分值
5) "tom"
6) "5000"    -- 成员"tom"的分值

redis> ZREVRANGEBYSCORE salary 5000 3000 WITHSCORES
1) "tom"
2) "5000"    -- 成员"tom"的分值
3) "jack"
4) "4500"    -- 成员"jack"的分值
5) "bob"
6) "3800"    -- 成员"bob"的分值
7) "peter"
8) "3500"    -- 成员"peter"的分值

1.8.2 限制命令返回的成员数量

在默认情况下,ZRANGEBYSCORE命令和ZREVRANGEBYSCORE命令会直接返回给定分值范围内的所有成员,但如果范围内的成员数量较多,或者我们只需要范围内的其中一部分成员,那么可以使用可选的LIMIT选项来限制命令返回的成员数量:

ZRANGEBYSCORE sorted_set min max [LIMIT offset count]

ZREVRANGEBYSCORE sorted_set max min [LIMIT offset count]

LIMIT选项接受offset和count两个参数作为输入,其中offset参数用于指定命令在返回结果之前需要跳过的成员数量,而count参数则用于指示命令最多可以返回多少个成员。

举个例子,假设我们想要以升序排列方式获取salary有序集合中分值介于3000~5000的第一个成员,那么可以执行以下命令:

redis> ZRANGEBYSCORE salary 3000 5000 LIMIT 0 1
1) "peter"

在这个命令中,offset参数的值为0,表示命令不需要跳过任何成员;而count参数的值为1,表示命令只需要返回一个成员即可。

如果我们想要以升序排列方式,获取salary有序集合中分值介于3000~5000的第二个和第三个成员,那么可以执行以下命令:

redis> ZRANGEBYSCORE salary 3000 5000 LIMIT 1 2
1) "bob"
2) "jack"    

在这个命令中,offset参数的值为1,表示命令需要跳过指定分值范围内的第一个成员,count参数的值为2,表示命令需要在跳过第一个成员之后,获取接下来的两个成员,而这两个成员就是位于指定分值范围内的第二个和第三个成员。

1.8.3 使用开区间分值范围

在默认情况下,ZRANGEBYSCORE命令和ZREVRANGEBYSCORE命令接受的分值范围都是闭区间分值范围,也就是说,分值等于用户给定最大分值或者最小分值的成员也会被包含在结果当中。

举个例子,如果我们执行命令:

redis> ZRANGEBYSCORE salary 3500 5000 WITHSCORES
1) "peter"
2) "3500"
3) "bob"
4) "3800"
5) "jack"
6) "4500"
7) "tom"
8) "5000"

那么分值等于3500或者5000的成员也会被包含在结果当中。

如果用户想要定义的是开区间而不是闭区间,那么可以在给定分值范围时,在分值参数的前面加上一个单括号“(”,这样,具有给定分值的成员就不会出现在命令返回的结果当中。

举个例子,以下命令只会返回分值大于3500且小于5000的成员,但并不会返回分值等于3500或者等于5000的成员:

redis> ZRANGEBYSCORE salary (3500 (5000 WITHSCORES
1) "bob"
2) "3800"
3) "jack"
4) "4500"

以下命令只会返回分值大于等于3500且小于5000的成员,但并不会返回分值等于5000的成员:

redis> ZRANGEBYSCORE salary 3500 (5000 WITHSCORES
1) "peter"
2) "3500"
3) "bob"
4) "3800"
5) "jack"
6) "4500"

以下命令只会返回分值大于3500且小于等于5000的成员,但并不会返回分值等于3500的成员:

redis> ZRANGEBYSCORE salary (3500 5000 WITHSCORES
1) "bob"
2) "3800"
3) "jack"
4) "4500"
5) "tom"
6) "5000"

1.8.4 使用无限值作为范围

ZRANGEBYSCORE命令和ZREVRANGEBYSCORE命令的min参数和max参数除了可以是普通的分值或者带有(符号的分值之外,还可以是特殊值+inf或者-inf,前者用于表示无穷大,而后者则用于表示无穷小:当我们只想定义分值范围的上限或者下限,而不是同时定义分值范围的上限和下限时,+inf和-inf就可以派上用场。

比如,如果我们想要获取salary有序集合中所有分值小于5000的成员,那么可以执行以下命令:

redis> ZRANGEBYSCORE salary -inf (5000 WITHSCORES
1) "peter"
2) "3500"
3) "bob"
4) "3800"
5) "jack"
6) "4500"

这个命令调用只定义了分值范围的上限,而没有定义分值范围的下限,因此命令将返回有序集合中所有分值低于给定上限的成员。

如果我们想要获取salary有序集合中所有分值大于4000的成员,那么可以执行以下命令:

redis> ZRANGEBYSCORE salary (4000 +inf WITHSCORES
1) "jack"
2) "4500"
3) "tom"
4) "5000"
5) "mary"
6) "5500"

与之前的例子正好相反,这次的命令调用只定义了分值范围的下限,但是没有定义分值范围的上线,因此命令将返回有序集合中所有分值高于给定下限的成员。

1.8.5 其他信息

复杂度:ZRANGEBYSCORE命令和ZREVRANGEBYSCORE命令的复杂度都是O(log(N)+M),其中N为有序集合包含的成员数量,而M则为命令返回的成员数量。

版本要求:ZRANGEBYSCORE命令从Redis 1.0.5版本开始可用,ZREVRANGEBYSCORE命令从Redis 2.2.0版本开始可用。

1.9 ZCOUNT:统计指定分值范围内的成员数量

通过使用COUNT命令,用户可以统计出有序集合中分值介于指定范围之内的成员数量:

ZCOUNT sorted_set min max

比如,我们可以通过执行以下命令,统计出salary有序集合中分值介于3000~5000之间的成员数量:

redis> ZCOUNT salary 3000 5000
(integer) 4    -- 有序集合里面有4个成员的分值介于3000~5000之间

图6-21展示了这个ZCOUNT命令统计出的4个成员。

img

图6-21 分值介于3000~5000之间的4个成员

1.9.1 分值范围的格式

ZCOUNT命令接受的分值范围格式和ZRANGEBYSCORE命令接受的分值范围格式完全相同:用户可以在执行ZCOUNT命令时,使用+inf表示无穷大分值,使用-inf表示无穷小分值,或者使用单括号(定义开区间分值范围。

举个例子,如果我们想要统计salary有序集合中分值小于5000的成员有多少个,那么只需要执行以下代码即可:

redis> ZCOUNT salary -inf (5000
(integer) 3

1.9.2 其他信息

复杂度:O(log(N)),其中N为有序集合包含的成员数量。

版本要求:ZCOUNT命令从Redis 2.0.0版本开始可用。

示例:时间线

在互联网上,有很多网站都会根据内容的发布时间来对内容进行排序,比如:

博客系统会按照文章发布时间的先后,把最近发布的文章放在前面,而发布时间较早的文章则放在后面,这样访客在浏览博客的时候,就可以先阅读最新的文章,然后再阅读较早的文章。

新闻网站会按照新闻的发布时间,把最近发生的新闻放在网站的前面,而早前发生的新闻则放在网站的后面,这样当用户访问该网站的时候,就可以第一时间查看到最新的新闻报道。

诸如微博和Twitter这样的微博客都会把用户最新发布的消息放在页面的前面,而稍早之前发布的消息则放在页面的后面,这样用户就可以通过向后滚动网页,查看最近一段时间自己关注的人都发表了哪些动态。

类似的情形还有很多。通过对这类行为进行抽象,我们可以创建出代码清单6-2所示的时间线程序:

这个程序会把被添加到时间线里面的元素用作成员,与元素相关联的时间戳用作分值,将元素和它的时间戳添加到有序集合中。

因为时间线中的每个元素都有一个与之相关联的时间戳,所以时间线中的元素将按照时间戳的大小进行排序。

通过对时间线中的元素执行ZREVRANGE命令或者ZREVRANGEBYSCORE命令,用户可以以分页的方式按顺序取出时间线中的元素,或者从时间线中取出指定时间区间内的元素。

代码清单6-2 使用有序集合实现的时间线程序:/sorted_set/timeline.py

class Timeline:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def add(self, item, time):
        """
        将元素添加到时间线中
        """
        self.client.zadd(self.key, {item:time})

    def remove(self, item):
        """
        从时间线中移除指定元素
        """
        self.client.zrem(self.key, item)

    def count(self):
        """
        返回时间线包含的元素数量
        """
        return self.client.zcard(self.key)

    def pagging(self, number, count, with_time=False):
        """
        按照每页count个元素计算,取出时间线第number页上的所有元素,
        这些元素将根据时间戳逆序排列。
        如果可选参数with_time的值为True,那么元素对应的时间戳也会一并被返回。
        注意:number参数的起始值是1而不是0
        """
        start_index = (number - 1)*count
        end_index = number*count-1
        return self.client.zrevrange(self.key, start_index, end_index, withscores=
        with_time)

    def fetch_by_time_range(self, min_time, max_time, number, count, with_time=False):
        """
        按照每页count个元素计算,获取指定时间段第number页上的所有元素,
        这些元素将根据时间戳逆序排列。
        如果可选参数with_time的值为True,那么元素对应的时间戳也会一并被返回。
        注意:number参数的起始值是1而不是0
        """
        start_index = (number-1)*count
        return self.client.zrevrangebyscore(self.key, max_time, min_time, start_
                                            index, count, withscores=with_time)

作为例子,让我们来学习一下如何使用这个时间线程序来存储和管理一系列博客文章。首先,我们需要载入相关的函数库,并创建一个时间线对象:

>>> from redis import Redis
>>> from timeline import Timeline
>>> client = Redis(decode_responses=True)
>>> blogs = Timeline(client, "blog_timelie")

通过blogs对象,我们可以把表6-1所示的10篇博客文章全部添加到时间线中:

>>> blogs.add("Switching from macOS: The Basics", 1477965600)
>>> blogs.add("Recent Loki Updates", 1477929600)
>>> blogs.add("What's Web Team Up To?", 1477645200)
>>> blogs.add("We've Joined the Snap Format TOB!", 1475618400)
>>> blogs.add("Loki Release Follow Up", 1474549200)
>>> blogs.add("Our Gtk+ Stylesheet Has Moved", 1473642000)
>>> blogs.add("Loki 0.4 Stable Release!", 1473404400)
>>> blogs.add("The Store is Back!", 1472068800)
>>> blogs.add("New Open Source Page On Our Website", 1470664800)
>>> blogs.add("We're back from the Snappy Sprint!", 1469664000)

表6-1 一些博客文章

img

在此之后,我们可以通过调用count()方法来获取时间线目前包含的文章数量:

>>> blogs.count()
10

也可以按照每页5篇文章的方式,按顺序获取位于时间线第1页的博客文章以及这些博客文章的发布时间:

>>> blogs.pagging(1, 5, with_time=True)
[('Switching from macOS: The Basics', 1477965600.0), ('Recent Loki Updates', 1477929600.0), ("What's Web Team Up To?", 1477645200.0), ("We've Joined the Snap Format TOB!", 1475618400.0), ('Loki Release Follow Up', 1474549200.0)]

或者以类似的方法,获取位于时间线第2页的博客文章:

>>> blogs.pagging(2, 5, with_time=True)
[('Our Gtk+ Stylesheet Has Moved', 1473642000.0), ('Loki 0.4 Stable Release!', 147340
4400.0), ('The Store is Back!', 1472068800.0), ('New Open Source Page On Our Website', 
1470664800.0), ("We're back from the Snappy Sprint!", 1469664000.0)]

除了按照时间顺序获取博客文章之外,我们还可以通过指定时间区间的方式获取指定时间段内发布的博客文章。比如,以2016年9月1日0时0分0秒的时间戳1472659200为起点,2016年9月30日23时59分59秒的时间戳1475251199为终点,调用fetch_by_time_range()方法,就可以找出9月份发布的所有博客文章:

>>> blogs.fetch_by_time_range(1472659200, 1475251199, 1, 5, with_time=True)
[('Loki Release Follow Up', 1474549200.0), ('Our Gtk+ Stylesheet Has Moved', 14736
42000.0), ('Loki 0.4 Stable Release!', 1473404400.0)]

同样,以2016年11月1日0时0分0秒的时间戳1477929600为起点,2016年11月30日23时59分59秒的时间戳1480521599为终点,调用fetch_by_time_range()方法,就可以找出11月份发布的所有博客文章:

>>> blogs.fetch_by_time_range(1477929600, 1480521599, 1, 5, with_time=True)
[('Switching from macOS: The Basics', 1477965600.0), ('Recent Loki Updates', 1477
929600.0)]

1.10 ZREMRANGEBYRANK:移除指定排名范围内的成员

ZREMRANGEBYRANK命令可以从升序排列的有序集合中移除位于指定排名范围内的成员,然后返回被移除成员的数量:

ZREMRANGEBYRANK sorted_set start end

与Redis的其他很多范围型命令一样,ZREMRANGEBYRANK命令接受的也是一个闭区间范围,也就是说,排名为start和end的成员也将被移除。

作为例子,以下代码展示了如何移除salary有序集合中升序排名0~3位的4个成员:

redis> ZREMRANGEBYRANK salary 0 3
(integer) 4    -- 这个命令移除了4个成员

图6-22展示了ZREMRANGEBYRANK命令执行前后,salary有序集合发生的变化。

img

图6-22 salary有序集合的变化

1.10.1 使用负数排名

传给ZREMRANGEBYRANK命令的排名参数除了可以是正数之外还可以是负数。

举个例子,假如我们需要从salary有序集合中移除排名倒数前3位的成员,那么只需要执行以下命令即可:

ZREMRANGEBYRANK salary -3 -1

1.10.2 其他信息

复杂度:O(log(N)+M),其中N为有序集合包含的成员数量,M为被移除的成员数量。

版本要求:ZREMRANGEBYRANK命令从Redis 2.0.0版本开始可用。

1.11 ZREMRANGEBYSCORE:移除指定分值范围内的成员

ZREMRANGEBYSCORE命令可以从有序集合中移除位于指定分值范围内的成员,并在移除操作执行完毕返回被移除成员的数量:

ZREMRANGEBYSCORE sorted_set min max

ZREMRANGEBYSCORE命令接受的分值范围与ZRANGEBYSCORE命令和ZCOUNT命令接受的分值范围一样,都默认为闭区间分值范围,但用户可以使用(符号定义闭区间,或者使用+inf和-inf表示正无限分值或者负无限分值。

作为例子,以下代码展示了如何使用ZREMRANGEBYSCORE命令移除salary有序集合中分值介于3000~4000的成员:

redis> ZREMRANGEBYSCORE salary 3000 4000
(integer) 2    -- 有2个成员被移除了

图6-23展示了salary有序集合在执行ZREMRANGEBYSCORE命令过程中的变化。

img

图6-23 salary有序集合执行ZREMRANGEBYSCORE命令的过程

其他信息

复杂度:O(log(N)+M),其中N为有序集合包含的成员数量,M为被移除成员的数量。

版本要求:ZREMRANGEBYSCORE命令从Redis 1.2.0版本开始可用。

1.12 ZUNIONSTORE、ZINTERSTORE:有序集合的并集运算和交集运算

与集合一样,Redis也为有序集合提供了相应的并集运算命令ZUNIONSTORE和交集运算命令ZINTERSTORE,这两个命令的基本格式如下:

ZUNIONSTORE destination numbers sorted_set [sorted_set ...]

ZINTERSTORE destination numbers sorted_set [sorted_set ...]

其中,命令的numbers参数用于指定参与计算的有序集合数量,之后的一个或多个sorted_set参数则用于指定参与计算的各个有序集合键,计算得出的结果则会存储到destination参数指定的键中。ZUNIONSTORE命令和ZINTERSTORE命令都会返回计算结果包含的成员数量作为返回值。

举个例子,对于图6-24所示的两个有序集合sorted_set1和sorted_set2来说,我们可以通过执行以下命令计算出它们的并集,并将其存储到键union-result-1中:

redis> ZUNIONSTORE union-result-1 2 sorted_set1 sorted_set2
(integer) 5    -- 这个并集包含了5个成员

图6-25展示了union-result-1有序集合包含的各个成员,其中成员c的分值3是根据sorted_set1和sorted_set2这两个有序集合中的成员c的分值相加得出的。

img

图6-24 两个有序集合

img

图6-25 union-result-1有序集合

除此之外,我们还可以通过执行以下命令计算出sorted_set1和sorted_set2的交集,并将这个交集存储到键inter-result-1中:

redis> ZINTERSTORE inter-result-1 2 sorted_set1 sorted_set2
(integer) 1    -- 这个交集只包含了一个成员

图6-26展示了inter-result-1有序集合包含的各个成员。与计算并集时的情况一样,在计算交集时,交集成员c的分值也是根据sorted_set1和sorted_set2这两个有序集合中成员c的分值相加得来的。

img

图6-26 inter-result-1有序集合

1.12.1 指定聚合函数

Redis为ZUNIONSTORE命令和ZINTERSTORE命令提供了可选的AGGREGATE选项,通过这个选项,用户可以决定使用哪个聚合函数来计算结果有序集合成员的分值:

[AGGREGATE SUM|MIN|MAX]

ZINTERSTORE destination numbers sorted_set [sorted_set ...] [AGGREGATE SUM|MIN|MAX]

AGGREGATE选项的值可以是SUM、MIN或者MAX中的一个,表6-2展示了这3个聚合函数的不同作用。

表6-2 各个聚合函数及其作用

img

举个例子,对于图6-27所示的3个有序集合ss1、ss2和ss3来说,使用SUM作为聚合函数进行交集计算,将得出一个分值为8的成员a:

img

图6-27 3个有序集合

redis> ZINTERSTORE agg-sum 3 ss1 ss2 ss3 AGGREGATE SUM
(integer) 1

redis> ZRANGE agg-sum 0 -1 WITHSCORES
1) "a"
2) "8"

这个分值是通过将1、2、5这3个分值相加得出的,如图6-28所示。

img

图6-28 使用SUM聚合函数计算出的交集

使用MIN作为聚合函数进行交集计算,将得出一个分值为1的成员a:

redis> ZINTERSTORE agg-min 3 ss1 ss2 ss3 AGGREGATE MIN
(integer) 1

redis> ZRANGE agg-min 0 -1 WITHSCORES
1) "a"
2) "1"

这个分值是通过从1、2、5这3个分值中选出最小值得出的,如图6-29所示。

img

图6-29 使用MIN聚合函数计算出的交集

最后,使用MAX作为聚合函数进行交集计算,将得出一个分值为5的成员a:

redis> ZINTERSTORE agg-max 3 ss1 ss2 ss3 AGGREGATE MAX
(integer) 1

redis> ZRANGE agg-max 0 -1 WITHSCORES
1) "a"
2) "5"

这个分值是通过从1、2、5这3个分值中选出最大值得出的,如图6-30所示。

img

图6-30 使用MAX聚合函数计算出的交集

在没有显式地使用AGGREGATE选项指定聚合函数的情况下,ZUNIONSTORE和ZINTERSTORE

默认使用SUM作为聚合函数。换句话说,以下这两条并集计算命令具有相同效果:

ZUNIONSTORE destination numbers sorted_set [sorted_set ...]

ZUNIONSTORE destination numbers sorted_set [sorted_set ...] AGGREGATE SUM

而以下这两条交集计算命令也具有相同效果:

ZINTERSTORE destination numbers sorted_set [sorted_set ...]

ZINTERSTORE destination numbers sorted_set [sorted_set ...] AGGREGATE SUM

1.12.2 设置权重

在默认情况下,ZUNIONSTORE和ZINTERSTORE将直接使用给定有序集合的成员分值去计算结果有序集合的成员分值,但是在有需要的情况下,用户也可以通过可选的WEIGHTS参数为各个给定有序集合的成员分值设置权重:

ZUNIONSTORE destination numbers sorted_set [sorted_set ...] [WEIGHTS weight [weight ...]]

ZINTERSTORE destination numbers sorted_set [sorted_set ...] [WEIGHTS weight [weight ...]]

在使用WEIGHTS选项时,用户需要为每个给定的有序集合分别设置一个权重,命令会将这个权重与成员的分值相乘,得出成员的新分值,然后执行聚合计算;与此相反,如果用户在使用WEIGHTS选项时,不想改变某个给定有序集合的分值,那么只需要将那个有序集合的权重设置为1即可。

img

图6-31 3个有序集合

举个例子,如果我们对图6-31所示的3个有序集合执行以下命令:

ZUNIONSTORE weighted-result 3 wss1 wss2 wss3 WEIGHTS 3 5 1

那么wss1有序集合成员"a"的分值2将被乘以3,变为6;wss2有序集合成员"b"的分值4则会被乘以5,变为20;wss3有序集合成员的分值3则会保持不变;通过进行并集计算,命令最终将得出图6-32所示的结果有序集合weighted-result。

img

图6-32 weighted-result有序集合

1.12.3 使用集合作为输入

ZUNIONSTORE和ZINTERSTORE除了可以使用有序集合作为输入之外,还可以使用集合作为输入:在默认情况下,这两个命令将把给定集合看作所有成员的分值都为1的有序集合来进行计算。如果有需要,用户也可以使用WEIGHTS选项来改变给定集合的分值,比如,如果你希望某个集合所有成员的分值都被看作10而不是1,那么只需要在执行命令时把那个集合的权重设置为10即可。

img

图6-33 集合和有序集合

举个例子,对于图6-33所示的集合和有序集合来说,我们可以执行以下命令,对它们进行并集计算,并将计算结果存储到mixed有序集合中:

redis> ZUNIONSTORE mixed 2 alphabets another-alphabets
(integer) 5

图6-34展示了mixed有序集合示例。

img

图6-34 mixed有序集合

1.12.4 其他信息

复杂度:ZUNIONSTORE命令的复杂度为O(Nlog(N)),其中N为所有给定有序集合的成员总数量。ZINTERSTORE命令的复杂度为O(Nlog(N)*M),其中N为所有给定有序集合中,基数最小的那个有序集合的基数,而M则是给定有序集合的数量。

版本要求:ZUNIONSTORE命令和ZINTERSTORE命令从Redis 2.0.0版本开始可用。

示例:商品推荐

在浏览网上商城的时候,我们常常会看到类似“购买此商品的顾客也同时购买”这样的商品推荐功能,如图6-35所示。

img

图6-35 网上商城的商品购买推荐示例

从抽象的角度来讲,这些推荐功能实际上都是通过记录用户的访问路径来实现的:如果用户在对一个目标执行了类似浏览或者购买这样的操作之后,也对另一个目标执行了相同的操作,那么程序就会对这次操作的访问路径进行记录和计数,然后程序就可以通过计数结果来知道用户在对指定目标执行了某个操作之后,还会对哪些目标执行相同的操作。

代码清单6-3展示了一个使用以上原理实现的路径统计程序:

每当用户从起点origin对终点destination进行一次访问,程序都会使用ZINCRBY命令对存储着起点origin访问记录的有序集合的destination成员执行一次分值加1操作。

在此之后,程序只需要对存储着origin访问记录的有序集合执行ZREVRANGE命令,就可以知道用户在访问了起点origin之后,最经常访问的目的地有哪些。

代码清单6-3 使用有序集合实现的访问路径记录程序:/sorted_set/path.py

def make_record_key(origin):
    return "forward_to_record::{0}".format(origin)

class Path:

    def __init__(self, client):
        self.client = client

    def forward_to(self, origin, destination):
        """
        记录一次从起点origin到目的地destination的访问
        """
        key = make_record_key(origin)
        self.client.zincrby(key, 1, destination)

    def pagging_record(self, origin, number, count, with_time=False):
        """
        按照每页count个目的地计算,
        从起点origin的访问记录中取出位于第number页的访问记录,
        其中所有访问记录均按照访问次数从多到少进行排列。
        如果可选的with_time参数的值为True,那么将具体的访问次数也一并返回
        """
        key = make_record_key(origin)
        start_index = (number-1)*count
        end_index = number*count-1
        return self.client.zrevrange(key, start_index, end_index, withscores=
        with_time, score_cast_func=int) # score_cast_func=int用于将成员的分值从浮
 # 点数转换为整数

以下代码展示了如何使用Path程序在一个图书网站上实现“看了这本书的顾客也看了以下这些书”的功能:

>>> from redis import Redis
>>> from path import Path
>>> client = Redis(decode_responses=True)
>>> see_also = Path(client)
>>> see_also.forward_to("book1", "book2")      # 从book1到book2的访问为3次
>>> see_also.forward_to("book1", "book2")
>>> see_also.forward_to("book1", "book2")
>>> see_also.forward_to("book1", "book3")      # 从book1到book3的访问为2次
>>> see_also.forward_to("book1", "book3")
>>> see_also.forward_to("book1", "book4")      # 从book1到book4和book5的访问各为1次
>>> see_also.forward_to("book1", "book5")
>>> see_also.forward_to("book1", "book6")      # 从book1到book6的访问为2次
>>> see_also.forward_to("book1", "book6")
>>> see_also.pagging_record("book1", 1, 5)     # 展示顾客在看了book1之后,最常看的其他书
['book2', 'book6', 'book3', 'book5', 'book4']
>>> see_also.pagging_record("book1", 1, 5, with_time=True)  # 将查看的次数也列出来
[('book2', 3), ('book6', 2), ('book3', 2), ('book5', 1), ('book4', 1)]

1.13 ZRANGEBYLEX、ZREVRANGEBYLEX:返回指定字典序范围内的成员

正如本章开头所说,对于拥有不同分值的有序集合成员来说,成员的大小将由分值决定,至于分值相同的成员,它们的大小则由该成员在字典序中的大小决定。

这种排列规则的一个特例是,当有序集合的所有成员都拥有相同的分值时,有序集合的成员将不再根据分值进行排序,而是根据字典序进行排序。在这种情况下,本章前面介绍的根据分值对成员进行操作的命令,比如ZRANGEBYSCORE、ZCOUNT和ZREMRANGEBYSCORE等,都将不再适用。

为了让用户可以对字典序排列的有序集合执行类似ZRANGEBYSCORE这样的操作,Redis提供了相应的ZRANGEBYLEX、ZREVRANGEBYLEX、ZLEXCOUNT和ZREMRANGEBYLEX命令,这些命令可以分别对字典序排列的有序集合执行升序排列的范围获取操作、降序排列的范围获取操作、统计位于字典序指定范围内的成员数量以及移除位于字典序指定范围内的成员,本章接下来将分别对这些命令进行介绍。

首先,让我们来学习一下ZRANGEBYLEX命令,这个命令可以从字典序排列的有序集合中获取位于字典序指定范围内的成员:

ZRANGEBYLEX sorted_set min max

命令的min参数和max参数用于指定用户想要获取的字典序范围,它们的值可以是以下4种值之一:

带有[符号的值表示在结果中包含与给定值具有同等字典序大小的成员。

带有(符号的值表示在结果中不包含与给定值具有同等字典序大小的成员。

加号+表示无穷大。

减号-表示无穷小。

举个例子,对于图6-36所示的words有序集合来说,如果我们想要通过ZRANGEBYLEX

命令获取words有序集合包含的所有成员,那么只需要将min参数的值设置为-,max参数的值设置为+即可:

redis> ZRANGEBYLEX words - +
1) "address"
2) "after"
3) "apple"
4) "bamboo"
5) "banana"
6) "bear"
7) "book"
8) "candy"
9) "cat"
10) "client"

img

图6-36 words有序集合

如果我们想要获取words有序集合中所有以字母"a"开头的成员,那么只需要将min参数的值设置为[a,max参数的值设置为(b即可:

redis> ZRANGEBYLEX words [a (b
1) "address"
2) "after"
3) "apple"

如果我们想要获取words有序集合中所有字典序小于字母"c"的成员,那么只需要将min参数的值设置为-,max参数的值设置为(c即可:

redis> ZRANGEBYLEX words - (c
1) "address"
2) "after"
3) "apple"
4) "bamboo"
5) "banana"
6) "bear"
7) "book"

1.13.1 ZREVRANGEBYLEX

ZREVRANGEBYLEX命令是逆序版的ZRANGEBYLEX命令,它会以逆字典序的方式返回指定范围内的成员:

ZREVRANGEBYLEX sorted_set max min

需要注意的是,与ZRANGEBYLEX命令先接受min参数后接受max参数的做法正好相反,ZREVRANGEBYLEX命令是先接受max参数,然后再接受min参数的。除此之外,这两个命令的min参数和max参数能够接受的值是完全相同的。

作为例子,以下代码展示了如何以逆字典序的方式返回有序集合中所有以字母"a"和字母"b"开头的成员:

redis> ZREVRANGEBYLEX words (c [a
1) "book"
2) "bear"
3) "banana"
4) "bamboo"
5) "apple"
6) "after"
7) "address"

1.13.2 限制命令返回的成员数量

与有序集合的其他范围型获取命令一样,ZRANGEBYLEX和ZREVRANGEBYLEX也可以通过可选的LIMIT选项来限制命令返回的成员数量:

ZRANGEBYLEX sorted_set min max [LIMIT offset count]

ZREVRANGEBYLEX sorted_set max min [LIMIT offset count]

作为例子,以下代码展示了如何以逆字典序的方式返回有序集合中第一个以字母"b"开头的成员:

redis> ZRANGEBYLEX words [b + LIMIT 0 1
1) "bamboo"

1.13.3 其他信息

复杂度:ZRANGEBYLEX命令和ZREVRANGEBYLEX命令的复杂度都为O(log(N)+M),其中N为有序集合包含的元素数量,而M则为命令返回的成员数量。

版本要求:ZRANGEBYLEX命令和ZREVRANGEBYLEX命令从Redis 2.8.9版本开始可用。

1.14 ZLEXCOUNT:统计位于字典序指定范围内的成员数量

对于按照字典序排列的有序集合,用户可以使用ZLEXCOUNT命令统计有序集合中位于字典序指定范围内的成员数量:

ZLEXCOUNT sorted_set min max

ZLEXCOUNT命令的min参数和max参数的格式与ZRANGEBYLEX命令接受的min参数和max参数的格式完全相同。

举个例子,通过执行以下命令,我们可以统计出words有序集合中以字母"a"开头的成员数量:

redis> ZLEXCOUNT words [a (b
(integer) 3    -- 这个有序集合中有3个以字母a开头的成员

或者使用以下命令,统计出有序集合中字典序大于等于字母"b"的成员数量:

redis> ZLEXCOUNT words [b +
(integer) 7    -- 这个有序集合中有7个成员的字典序大于等于字母b

图6-37展示了被以上两个ZLEXCOUNT命令统计出的有序集合成员。

img

图6-37 被统计的有序集合成员

其他信息

复杂度:O(log(N)),其中N为有序集合包含的成员数量。

版本要求:ZLEXCOUNT命令从Redis 2.8.9版本开始可用。

1.15 ZREMRANGEBYLEX:移除位于字典序指定范围内的成员

对于按照字典序排列的有序集合,用户可以使用ZREMRANGEBYLEX命令去移除有序集合中位于字典序指定范围内的成员:

ZREMRANGEBYLEX sorted_set min max

这个命令的min参数和max参数的格式与ZRANGEBYLEX命令以及ZLEXCOUNT命令接受的min参数和max参数的格式完全相同。ZREMRANGEBYLEX命令在移除用户指定的成员之后,将返回被移除成员的数量作为命令的返回值。

作为例子,以下代码展示了如何移除words有序集合中所有以字母"b"开头的成员:

redis> ZREMRANGEBYLEX words [b (c
(integer) 4    -- 有4个成员被移除了

图6-38展示了words有序集合在ZREMRANGEBYLEX命令执行前后发生的变化。

img

图6-38 ZREMRANGEBYLEX命令的执行过程

其他信息

复杂度:O(log(N)+M),其中N为有序集合包含的成员数量,M为被移除成员的数量。

版本要求:ZREMRANGEBYLEX命令从Redis 2.8.9版本开始可用。

示例:自动补全

包含大量信息的网站常常会在搜索或者查找功能上提供自动补全特性,这一特性可以帮助用户更快速地找到他们想要的信息。比如,当我们在搜索引擎中输入“黄”字的时候,搜索引擎的自动补全特性就会列出一些比较著名的以“黄”字开头的人或者物,以便用户可以更快速地找到相关信息,如图6-39所示。

img

图6-39  搜索引擎通过自动补全功能展示用户可能感兴趣的结果

代码清单6-4展示了一个使用有序集合实现的自动补全程序,这个程序可以提供类似图6-39所示的自动补全效果。

代码清单6-4 使用有序集合实现的自动补全程序:/sorted_set/auto_complete.py

class AutoComplete:

    def __init__(self, client):
        self.client = client

    def feed(self, content, weight=1):
        """
        根据用户输入的内容构建自动补全结果,
        其中content参数为内容本身,可选的weight参数用于指定内容的权重值
        """
        for i in range(1, len(content)):
            key = "auto_complete::" + content[:i]
            self.client.zincrby(key, weight, content)

    def hint(self, prefix, count):
        """
        根据给定的前缀prefix,获取count个自动补全结果
        """
        key = "auto_complete::" + prefix
        return self.client.zrevrange(key, 0, count-1)

这个自动补全程序的feed()方法接受给定的内容和权重值作为参数,并以此来构建自动补全结果。比如,如果我们调用feed("黄晓朋",5000),那么程序将拼接出以下3个键:

auto_complete::
auto_complete::黄晓
auto_complete::黄晓朋

然后通过执行以下这3个命令,将自动补全结果“黄晓朋”及其权重5000添加到相应的有序集合里面:

ZINCRBY  auto_complete::  5000  "黄晓朋"
ZINCRBY  auto_complete::黄晓  5000  "黄晓朋"
ZINCRBY  auto_complete::黄晓朋  5000  "黄晓朋"

这样做的结果是,程序会把所有“黄”字开头的名字按权重大小有序地存储到auto_complete::黄这个有序集合里面,而以“黄晓”开头的名字则会按照权重大小有序地存储在auto_complete::黄晓这个有序集合里面,诸如此类。

相对地,当我们想要找出所有以“黄”字开头的名字时,只需要调用hint()方法,程序就会使用ZREVRANGE命令从auto_complete::黄有序集合中取出相应的自动补全结果。

作为例子,现在让我们载入这个自动补全程序:

>>> from redis import Redis
>>> from auto_complete import AutoComplete
>>> client = Redis(decode_responses=True)
>>> ac = AutoComplete(client)

然后向程序输入一些名字以及这些名字的权重:

>>> ac.feed("黄健宏", 30)
>>> ac.feed("黄健强", 3000)
>>> ac.feed("黄晓朋", 5000)
>>> ac.feed("张三", 2500)
>>> ac.feed("李四", 1700)

在此之后,如果我们以“黄”字为前缀调用hint()方法,程序就会列出所有以“黄”字开头的名字:

>>> for name in ac.hint("", 10):
...   print(name)
... 
黄晓朋
黄健强
黄健宏

接着,如果我们以“黄健”二字为前缀调用hint()方法,那么程序将列出两个以“黄健”二字为开头的名字:

>>> for name in ac.hint("黄健", 10):
...   print(name)
... 
黄健强
黄健宏

再次提醒一下,因为hint()方法是按照权重的大小有序地返回结果的,所以权重较高的“黄健强”会排在前面,而权重较低的“黄健宏”则会排在后面。

1.16 ZPOPMAX、ZPOPMIN:弹出分值最高和最低的成员

ZPOPMAX和ZPOPMIN是Redis 5.0版本新添加的两个命令,分别用于移除并返回有序集合中分值最大和最小的N个元素:

ZPOPMAX sorted_set [count]
ZPOPMIN sorted_set [count]

其中被移除元素的数量可以通过可选的count参数来指定。如果用户没有显式地给定count参数,那么命令默认只会移除一个元素。

举个例子,对于图6-40所示的有序集合来说,我们可以通过执行以下两个命令,分别移除有序集合中分值最大和最小的元素:

redis> ZPOPMAX salary
1) "mary"  -- 被移除元素的成员
2) "5500"  -- 被移除元素的分值

redis> ZPOPMIN salary
1) "peter"
2) "3500"

执行上述命令之后的salary有序集合如图6-41所示。

img

图6-40 存储薪水数据的salary有序集合

img

图6-41  弹出分值最大元素和分值最小元素之后的salary有序集合

其他信息

复杂度:O(N),其中N为命令移除的元素数量。

版本要求:ZPOPMAX命令和ZPOPMIN命令从Redis 5.0.0版本开始可用。

1.17 BZPOPMAX、BZPOPMIN:阻塞式最大/最小元素弹出操作

BZPOPMAX命令和BZPOPMIN命令分别是ZPOPMAX命令以及ZPOPMIN命令的阻塞版本,这两个阻塞命令都接受任意多个有序集合和一个秒级精度的超时时限作为参数:

BZPOPMAX sorted_set [sorted_set ...] timeout

BZPOPMIN sorted_set [sorted_set ...] timeout

接收到参数的BZPOPMAX命令和BZPOPMIN命令会依次检查用户给定的有序集合,并从它遇到的第一个非空有序集合中弹出指定的元素。如果命令在检查了所有给定有序集合之后都没有发现可弹出的元素,那么它将阻塞执行命令的客户端,并在给定的时限之内等待可弹出的元素出现,直到等待时间超过给定时限为止。用户可以通过将超时时限设置为0来让命令一直阻塞,直到可弹出的元素出现为止。

BZPOPMAX命令和BZPOPMIN命令在成功弹出元素时将返回一个包含3个项的列表,这3个项分别为被弹出元素所在的有序集合、被弹出元素的成员以及被弹出元素的分值。与此相反,如果这两个命令因为等待超时而未能弹出任何元素,那么它们将返回一个空值作为结果。

举个例子,对于以下3个有序集合来说:

redis> ZRANGE ss1 0 -1 WITHSCORES
(empty list or set)

redis> ZRANGE ss2 0 -1 WITHSCORES
1) "a"
2) "1"
3) "b"
4) "2"

redis> ZRANGE ss3 0 -1 WITHSCORES
1) "c"
2) "3"

如果我们对它们执行以下BZPOPMAX命令,那么命令将跳过空集ss1,并弹出第一个非空有序集合ss2的最大元素:

redis> BZPOPMAX ss1 ss2 ss3 10
1) "ss2"  -- 被弹出元素所在的有序集合
2) "b"    -- 被弹出元素的成员
3) "2"    -- 被弹出元素的分值

与此类似,如果我们继续执行BZPOPMAX命令,那么命令将继续弹出第一个非空有序集合ss2的最大元素:

redis> BZPOPMAX ss1 ss2 ss3 10
1) "ss2"
2) "a"
3) "1"

现在,当ss1和ss2都变成空集之后,如果我们再次执行BZPOPMAX命令,那么命令将跳过空集ss1和ss2,弹出第一个非空有序集合ss3的最大元素:

redis> BZPOPMAX ss1 ss2 ss3 10
1) "ss3"
2) "c"
3) "3"

最后,因为此时3个有序集合均已变成空集,所以如果我们再次执行BZPOPMAX命令,那么命令将在阻塞10s之后返回空值:

redis> BZPOPMAX ss1 ss2 ss3 10
(nil)
(10.05s)

除了BZPOPMAX命令弹出的是最大元素而BZPOPMIN命令弹出的是最小元素之外,这两个命令接受参数的方式以及返回值的方式完全相同。

其他信息

复杂度:O(N),其中N为用户给定的有序集合数量。

版本要求:BZPOPMAX命令和BZPOPMIN命令从Redis 5.0.0版本开始可用。

1.18 重点回顾

有序集合同时拥有“有序”和“集合”两种性质,集合性质保证有序集合只会包含各不相同的成员,而有序性质则保证了有序集合中的所有成员都会按照特定的顺序进行排列。

在一般情况下,有序集合成员的大小由分值决定,而分值相同的成员的大小则由成员在字典序中的大小决定。

成员的分值除了可以是数字之外,还可以是表示无穷大的"+inf"或者表示无穷小的"-inf"。

ZADD命令从Redis 3.0.2版本开始,可以通过给定可选项来决定执行添加操作或是执行更新操作。

因为Redis只提供了对成员分值执行加法计算的ZINCRBY命令,而没有提供相应的减法计算命令,所以我们只能通过向ZINCRBY命令传入负数增量来对成员分值执行减法计算。

ZINTERSTORE命令和ZUNIONSTORE命令除了可以使用有序集合作为输入之外,还可以使用集合作为输入。在默认情况下,这两个命令会把集合的成员看作分值为1的有序集合成员来计算。

当有序集合的所有成员都拥有相同的分值时,用户可以通过ZRANGEBYLEX、ZLEXCOUNT、

ZREMRANGEBYLEX等命令,按照字典序对有序集合中的成员进行操作。

版权声明:如无特殊说明,文章均为本站原创,版权所有,转载需注明本文链接

本文链接:http://www.bianchengvip.com/article/redis-sorted-set/