大家好,我是腾意。

Redis的列表(list)是一种线性的有序结构,可以按照元素被推入列表中的顺序来存储元素,这些元素既可以是文字数据,又可以是二进制数据,并且列表中的元素可以重复出现。

作为例子,图4-1展示了一个包含多个字符串的列表,这个列表按照从左到右的方式,依次存储了"one"、"two"、"three"、"four"这4个元素。

img

图4-1 横向表示的列表

提示 为了展示方便,本书给出的列表图片一般都会像图4-1这样只展示列表本身而忽略列表的键名,但是在需要的时候,也会如图4-2所示,将列表及其键名一并给出。

img

图4-2 完整的列表键视图

Redis为列表提供了丰富的操作命令,通过这些命令,用户可以:

将新元素推入列表的左端或者右端。

移除位于列表最左端或者最右端的元素。

移除列表最右端的元素,然后把被移除的元素推入另一个列表的左端。

获取列表包含的元素数量。

获取列表在指定索引上的单个元素,或者获取列表在指定索引范围内的多个元素。

为列表的指定索引设置新元素,或者把新元素添加到某个指定元素的前面或者后面。

对列表进行修剪,只保留指定索引范围内的元素。

从列表中移除指定元素。

执行能够阻塞客户端的推入和移除操作。

本章接下来将对以上提到的各个列表操作命令进行介绍,并说明如何使用这些命令去构建各种实用的程序。

1.1 LPUSH:将元素推入列表左端

用户可以通过LPUSH命令,将一个或多个元素推入给定列表的左端:

LPUSH list item [item item ...]

在推入操作执行完毕之后,LPUSH命令会返回列表当前包含的元素数量作为返回值。

例如,以下代码就展示了如何通过LPUSH命令将"buy some milk"、"watch tv"、"finish homework"等元素依次推入todo列表的左端:

redis> LPUSH todo "buy some milk"
(integer) 1    -- 列表现在包含1个元素

redis> LPUSH todo "watch tv"
(integer) 2    -- 列表现在包含2个元素

redis> LPUSH todo "finish homework"
(integer) 3    -- 列表现在包含3个元素

图4-3展示了以上3个LPUSH命令的执行过程:

1)在执行操作之前,todo列表为空,即不存在于数据库中。

2)执行第1个LPUSH命令,将元素"buy some milk"推入列表左端。

3)执行完第1个LPUSH命令的列表现在包含一个元素。

4)执行第2个LPUSH命令,将元素"watch tv"推入列表左端。

5)执行完第2个LPUSH命令的列表现在包含两个元素。

6)执行第3个LPUSH命令,将元素"finish homework"推入列表左端。

7)执行完第3个LPUSH命令的todo列表现在包含3个元素。

1.1.1 一次推入多个元素

LPUSH命令允许用户一次将多个元素推入列表左端:如果用户在执行LPUSH命令时给定了多个元素,那么LPUSH命令将按照元素给定的顺序,从左到右依次将所有给定元素推入列表左端。

img

图4-3 LPUSH命令执行过程

举个例子,如果用户执行以下命令:

redis> LPUSH another-todo "buy some milk" "watch tv" "finish homework"
(integer) 3

那么LPUSH命令将按照图4-4所示的顺序,将3个给定元素依次推入another-todo列表的左端。

img

图4-4 一次推入多个元素

最终,这条LPUSH命令将产生图4-5所示的列表,这个列表与前面使用3条LPUSH命令构建出的列表完全相同。

img

图4-5 another-todo列表及其包含的元素

1.1.2 其他信息

复杂度:O(N),其中N为被推入列表的元素数量。

版本要求:LPUSH命令从Redis 1.0.0版本开始可用,但是只有Redis 2.1.0或以上版本的LPUSH命令可以一次推入多个元素,Redis 2.1.0以下版本的LPUSH命令每次只能推入一个元素。

1.2 RPUSH:将元素推入列表右端

RPUSH命令和LPUSH命令类似,这两个命令执行的都是元素推入操作,唯一区别就在于LPUSH命令会将元素推入列表左端,而RPUSH命令会将元素推入列表右端:

RPUSH list item [item item ...]

在推入操作执行完毕之后,RPUSH命令会返回列表当前包含的元素数量作为返回值。

举个例子,以下代码展示了如何通过RPUSH命令将"buy some milk"、"watch tv"、"finish homework"等元素依次推入todo列表的右端:

redis> RPUSH todo "buy some milk"
(integer) 1    -- 列表现在包含1个元素

redis> RPUSH todo "watch tv"
(integer) 2    -- 列表现在包含2个元素

redis> RPUSH todo "finish homework"
(integer) 3    -- 列表现在包含3个元素

图4-6展示了以上3个RPUSH命令的执行过程:

1)在操作执行之前,todo列表为空,即不存在于数据库中。

2)执行第1个RPUSH命令,将元素"buy some milk"推入列表右端。

3)执行完第1个RPUSH命令的列表现在包含一个元素。

4)执行第2个RPUSH命令,将元素"watch tv"推入列表右端。

5)执行完第2个RPUSH命令的列表现在包含两个元素。

6)执行第3个RPUSH命令,将元素"finish homework"推入列表右端。

7)执行完第3个RPUSH命令的todo列表现在包含3个元素。

1.2.1 一次推入多个元素

与LPUSH命令一样,RPUSH命令也允许用户一次推入多个元素:如果用户在执行RPUSH命令时给定了多个元素,那么RPUSH命令将按照元素给定的顺序,从左到右依次将所有给定元素推入列表右端。

img

图4-6 RPUSH命令执行过程

举个例子,如果用户执行以下命令:

redis> RPUSH another-todo "buy some milk" "watch tv" "finish homework"
(integer) 3

那么RPUSH命令将按照图4-7展示的顺序,将3个给定元素依次推入another-todo列表的右端。

img

图4-7 一次推入多个元素

最终,这条RPUSH命令将产生图4-8所示的列表,这个列表与前面使用3条RPUSH命令构建出的列表完全相同。

img

图4-8 another-todo列表及其包含的元素

1.2.2 其他信息

复杂度:O(N),其中N为被推入列表的元素数量。

版本要求:RPUSH命令从Redis 1.0.0版本开始可用,但是只有Redis 2.1.0或以上版本的RPUSH命令可以一次推入多个元素,Redis 2.1.0以下版本的RPUSH命令每次只能推入一个元素。

1.3 LPUSHX、RPUSHX:只对已存在的列表执行推入操作

当用户调用LPUSH命令或RPUSH命令尝试将元素推入列表的时候,如果给定的列表并不存在,那么命令将自动创建一个空列表,并将元素推入刚刚创建的列表中。

例如,对于空列表list1和list2来说,执行以下命令将创建图4-9所示的两个列表:

img

图4-9 两个只包含单个元素的列表

redis>LPUSH list1"item1"

redis> LPUSH list1 "item1"
(integer) 1

redis> RPUSH list2 "item1"
(integer) 1    

除了LPUSH命令和RPUSH命令之外,Redis还提供了LPUSHX命令和RPUSHX命令:

LPUSHX list item

RPUSHX list item

这两个命令对待空列表的方式与LPUSH命令和RPUSH命令正好相反:

LPUSHX命令只会在列表已经存在的情况下,将元素推入列表左端。

RPUSHX命令只会在列表已经存在的情况下,将元素推入列表右端。

如果给定列表并不存在,那么LPUSHX命令和RPUSHX命令将放弃执行推入操作。

LPUSHX命令和RPUSHX命令在成功执行推入操作之后,将返回列表当前的长度作为返回值,如果推入操作未能成功执行,那么命令将返回0作为结果。

举个例子,如果我们对不存在的列表list3执行以下LPUSHX命令和RPUSHX命令,那么这两个推入操作都将被拒绝:

redis> LPUSHX list3 "item-x"
(integer) 0    -- 没有推入任何元素

redis> RPUSHX list3 "item-y"
(integer) 0    -- 没有推入任何元素

如果我们先使用LPUSH命令将一个元素推入list3列表中,使得list3变成非空列表,那么LPUSHX命令和RPUSHX命令就可以成功地执行推入操作:

redis> LPUSH list3 "item1"
(integer) 1    -- 推入一个元素,使得列表变为非空

redis> LPUSHX list3 "item-x"
(integer) 2    -- 执行推入操作之后,列表包含2个元素

redis> RPUSHX list3 "item-y"
(integer) 3    -- 执行推入操作之后,列表包含3个元素

图4-10展示了列表list3的整个变化过程:

1)在最初的LPUSHX命令和RPUSHX命令执行之后,list3仍然是一个空列表。

2)执行LPUSH命令,将元素"item1"推入列表中,使之变为非空。

3)执行LPUSHX命令,将元素"item-x"推入列表,使得列表包含2个元素。

4)执行RPUSHX命令,将元素"item-y"推入列表,使得列表包含3个元素。

img

图4-10 LPUSHX命令和RPUSHX命令的执行过程

1.3.1 每次只能推入单个元素

与LPUSH命令和RPUSH命令不一样,LPUSHX命令和RPUSHX命令每次只能推入一个元素,尝试向LPUSHX命令或RPUSHX命令给定多个元素将引发错误:

redis> LPUSHX list "item1" "item2" "item3"
(error) ERR wrong number of arguments for 'lpushx' command

redis> RPUSHX list "item1" "item2" "item3"
(error) ERR wrong number of arguments for 'rpushx' command

1.3.2 其他信息

复杂度:O(1)。

版本要求:LPUSHX命令和RPUSHX命令从Redis 2.2.0版本开始可用。

1.4 LPOP:弹出列表最左端的元素

用户可以通过LPOP命令移除位于列表最左端的元素,并将被移除的元素返回给用户:

LPOP list

例如,以下代码就展示了如何使用LPOP命令弹出todo列表的最左端元素:

redis> LPOP todo
"finish homework"

redis> LPOP todo
"watch tv"

redis> LPOP todo

图4-11展示了todo列表在LPOP命令执行时的整个变化过程:

1)在LPOP命令执行之前,todo列表包含3个元素。

2)执行第1个LPOP命令,从列表中弹出"finish homework"元素。

3)执行第2个LPOP命令,从列表中弹出"watch tv"元素。

4)执行第3个LPOP命令,从列表中弹出"buy some milk"元素,并使todo列表变为空。

img

图4-11 LPOP命令的执行过程

如果用户给定的列表并不存在,那么LPOP命令将返回一个空值,表示列表为空,没有元素可供弹出:

redis> LPOP empty-list
(nil)

其他信息

复杂度:O(1)。

版本要求:LPOP命令从Redis 1.0.0版本开始可用。

1.5 RPOP:弹出列表最右端的元素

用户可以通过RPOP命令移除位于列表最右端的元素,并将被移除的元素返回给用户:

RPOP list

例如,以下代码就展示了如何使用RPOP命令弹出todo列表最右端的元素:

redis> RPOP todo
"finish homework"

redis> RPOP todo
"watch tv"

redis> RPOP todo
"buy some milk"

图4-12展示了todo列表在RPOP命令执行时的整个变化过程:

1)在RPOP命令执行之前,todo列表包含3个元素。

2)执行第1个RPOP命令,从列表中弹出"finish homework"元素。

3)执行第2个RPOP命令,从列表中弹出"watch tv"元素。

4)执行第3个RPOP命令,从列表中弹出"buy some milk"元素,并使得todo列表变为空。

img

图4-12 RPOP命令的执行过程

与LPOP命令一样,如果用户给定的列表并不存在,那么RPOP命令将返回一个空值,表示列表为空,没有元素可供弹出:

redis> RPOP empty-list
(nil)

其他信息

复杂度:O(1)。

版本要求:RPOP命令从Redis 1.0.0版本开始可用。

1.6 RPOPLPUSH:将右端弹出的元素推入左端

RPOPLPUSH命令的行为和它的名字一样,首先使用RPOP命令将源列表最右端的元素弹出,然后使用LPUSH命令将被弹出的元素推入目标列表左端,使之成为目标列表的最左端元素:

RPOPLPUSH source target

RPOPLPUSH命令会返回被弹出的元素作为结果。

作为例子,以下代码展示了如何使用RPOPLPUSH命令,将列表list1的最右端元素弹出,然后将其推入列表list2的左端:

redis> RPUSH list1 "a" "b" "c"    -- 创建两个示例列表list1和list2
(integer) 3

redis> RPUSH list2 "d" "e" "f"
(integer) 3

redis> RPOPLPUSH list1 list2
"c"

redis> RPOPLPUSH list1 list2
"b"

redis> RPOPLPUSH list1 list2
"a"

图4-13展示了列表list1和list2在执行以上RPOPLPUSH命令时的变化过程:

1)在RPOPLPUSH命令执行之前,list1和list2都包含3个元素。

2)执行第1个RPOPLPUSH命令,弹出list1的最右端元素"c",并将其推入list2的左端。

3)执行第2个RPOPLPUSH命令,弹出list1的最右端元素"b",并将其推入list2的左端。

4)执行第3个RPOPLPUSH命令,弹出list1的最右端元素"a",并将其推入list2的左端。

5)在以上3个RPOPLPUSH命令执行完毕之后,list1将变为空列表,而list2则会包含6个元素。

img

图4-13 RPOPLPUSH命令的执行过程

1.6.1 源列表和目标列表相同

RPOPLPUSH命令允许用户将源列表和目标列表设置为同一个列表,在这种情况下,RPOPLPUSH命令的效果相当于将列表最右端的元素变成列表最左端的元素。

例如,以下代码展示了如何通过RPOPLPUSH命令将rotate-list列表的最右端元素变成列表的最左端元素:

redis> RPUSH rotate-list "a" "b" "c"    -- 创建一个示例列表
(integer) 3

redis> RPOPLPUSH rotate-list rotate-list
"c"

redis> RPOPLPUSH rotate-list rotate-list
"b"

redis> RPOPLPUSH rotate-list rotate-list
"a"

图4-14展示了以上3个RPOPLPUSH命令在执行时,rotate-list列表的整个变化过程:

1)在RPOPLPUSH命令执行之前,列表包含"a"、"b"、"c"3个元素。

2)执行第1个RPOPLPUSH命令,将最右端元素"c"变为最左端元素。

3)执行第2个RPOPLPUSH命令,将最右端元素"b"变为最左端元素。

4)执行第3个RPOPLPUSH命令,将最右端元素"a"变为最左端元素。

5)在以上3个RPOPLPUSH命令执行完毕之后,列表又重新变回了原样。

img

图4-14 使用RPOPLPUSH对列表元素进行轮换

正如上面展示的例子所示,通过对同一个列表重复执行RPOPLPUSH命令,我们可以创建出一个对元素进行轮换的列表,并且当我们对一个包含了N个元素的列表重复执行N次RPOPLPUSH命令之后,列表元素的排列顺序将变回原来的样子。

1.6.2 处理空列表

如果用户传给RPOPLPUSH命令的源列表并不存在,那么RPOPLPUSH命令将放弃执行弹出和推入操作,只返回一个空值表示命令执行失败:

redis> RPOPLPUSH list-x list-y
(nil)

如果源列表非空,但是目标列表为空,那么RPOPLPUSH命令将正常执行弹出操作和推入操作:

redis> RPUSH list-x "a" "b" "c"    -- 将list-x变为非空列表
(integer) 3

redis> RPOPLPUSH list-x list-y
"c"

图4-15展示了这条RPOPLPUSH命令执行之前和执行之后,list-x和list-y的变化:

1)在执行RPOPLPUSH命令之前,list-x包含3个元素,而list-y为空。

2)执行RPOPLPUSH命令,将list-x的最右端元素"c"弹出,并将其推入list-y的左端。

3)在RPOPLPUSH命令执行完毕之后,list-x将包含2个元素,而list-y则包含1个元素。

img

图4-15 RPOPLPUSH命令处理目标列表为空的例子

1.6.3 其他信息

复杂度:O(1)。

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

示例:先进先出队列

先进先出队列(first in first out queue)是一种非常常见的数据结构,一般都会包含入队(enqueue)和出队(dequeue)这两个操作,其中入队操作会将一个元素放入队列中,而出队操作则会从队列中移除最先入队的元素。

先进先出队列的应用非常广泛,各式各样的应用程序中都有使用。举个例子,很多电商网站都会在节日时推出一些秒杀活动,这些活动会放出数量有限的商品供用户抢购,秒杀系统的一个特点就是在短时间内会有大量用户同时进行相同的购买操作,如果使用事务或者锁去实现秒杀程序,那么就会因为锁和事务的重试特性而导致性能低下,并且由于重试行为的存在,成功购买商品的用户可能并不是最早执行购买操作的用户,因此这种秒杀系统实际上是不公平的。

解决上述问题的方法之一就是把用户的购买操作都放入先进先出队列里面,然后以队列方式处理用户的购买操作,这样程序就可以在不使用锁或者事务的情况下实现秒杀系统,并且得益于先进先出队列的特性,这种秒杀系统可以按照用户执行购买操作的顺序来判断哪些用户可以成功执行购买操作,因此它是公平的。

代码清单4-1展示了一个使用Redis列表实现先进先出队列的方法。

代码清单4-1 使用列表实现的先进先出队列:/list/fifo_queue.py

class FIFOqueue:

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

    def enqueue(self, item):
        """
        将给定元素放入队列,然后返回队列当前包含的元素数量作为结果
        """
        return self.client.rpush(self.key, item)

    def dequeue(self):
        """
        移除并返回队列中目前入队时间最长的元素
        """
        return self.client.lpop(self.key)

作为例子,我们可以通过执行以下代码载入并创建一个先进先出队列:

>>> from redis import Redis
>>> from fifo_queue import FIFOqueue
>>> client = Redis(decode_responses=True)
>>> q = FIFOqueue(client, "buy-request")

然后通过执行以下代码,将3个用户的购买请求依次放入队列里面:

>>> q.enqueue("peter-buy-milk")
1
>>> q.enqueue("john-buy-rice")
2
>>> q.enqueue("david-buy-keyboard")
3

最后,按照先进先出顺序,依次从队列中弹出相应的购买请求:

>>> q.dequeue()
'peter-buy-milk'
>>> q.dequeue()
'john-buy-rice'
>>> q.dequeue()
'david-buy-keyboard'

可以看到,队列弹出元素的顺序与元素入队时的顺序是完全相同的,最先是"peter-buy-milk"元素,接着是"john-buy-rice"元素,最后是"david-buy-keyboard"元素。

1.7 LLEN:获取列表的长度

用户可以通过执行LLEN命令来获取列表的长度,即列表包含的元素数量:

LLEN list

比如对于图4-16所示的几个列表来说,对它们执行LLEN命令将获得以下结果:

redis> LLEN todo
(integer) 3

redis> LLEN alphabets
(integer) 8

redis> LLEN msg-queue
(integer) 4

img

图4-16 几个不同长度的列表

对于不存在的列表,LLEN命令将返回0作为结果:

redis> LLEN not-exists-list
(integer) 0

其他信息

复杂度:O(1)。

版本要求:LLEN命令从Redis 1.0.0版本开始可用。

1.8 LINDEX:获取指定索引上的元素

Redis列表包含的每个元素都有与之对应的正数索引和负数索引:

正数索引从列表的左端开始计算,依次向右端递增:最左端元素的索引为0,左端排行第二的元素索引为1,左端排行第三的元素索引为2,以此类推。最大的正数索引为列表长度减1,即N-1。

负数索引从列表的右端开始计算,依次向左端递减:最右端元素的索引为-1,右端排行第二的元素索引为-2,右端排行第三的元素索引为-3,以此类推。最大的负数索引为列表长度的负数,即-N。

作为例子,图4-17展示了一个包含多个元素的列表,并给出了列表元素对应的正数索引和负数索引。

为了让用户可以方便地取得索引对应的元素,Redis提供了LINDEX命令:

LINDEX list index

这个命令接受一个列表和一个索引作为参数,然后返回列表在给定索引上的元素;其中给定索引既可以是正数,也可以是负数。

img

图4-17 列表的索引

比如,对于前面展示的图4-17,我们可以通过执行以下命令,取得alphabets列表在指定索引上的元素:

redis> LINDEX alphabets 0
"a"

redis> LINDEX alphabets 3
"d"

redis> LINDEX alphabets 6
"g"

redis> LINDEX alphabets -3
"f"

redis> LINDEX alphabets -7
"b"

1.8.1 处理超出范围的索引

对于一个长度为N的非空列表来说:

它的正数索引必然大于等于0,并且小于等于N-1。

它的负数索引必然小于等于-1,并且大于等于-N。

如果用户给定的索引超出了这一范围,那么LINDEX命令将返回空值,以此来表示给定索引上并不存在任何元素:

redis> LINDEX alphabets 100
(nil)

redis> LINDEX alphabets -100
(nil)

1.8.2 其他信息

复杂度:O(N),其中N为给定列表的长度。

版本要求:LINDEX命令从Redis 1.0.0版本开始可用。

1.9 LRANGE:获取指定索引范围上的元素

用户除了可以使用LINDEX命令获取给定索引上的单个元素之外,还可以使用LRANGE命令获取给定索引范围上的多个元素:

LRANGE list start end

LRANGE命令接受一个列表、一个开始索引和一个结束索引作为参数,然后依次返回列表从开始索引到结束索引范围内的所有元素,其中开始索引和结束索引对应的元素也会被包含在命令返回的元素当中。

作为例子,以下代码展示了如何使用LRANGE命令去获取alphabets列表在不同索引范围内的元素:

redis> LRANGE alphabets 0 3    -- 获取列表索引0至索引3上的所有元素
1) "a"    -- 位于索引0上的元素
2) "b"    -- 位于索引1上的元素
3) "c"    -- 位于索引2上的元素
4) "d"    -- 位于索引3上的元素

redis> LRANGE alphabets 2 6
1) "c"
2) "d"
3) "e"
4) "f"
5) "g"

redis> LRANGE alphabets -5 -1
1) "d"
2) "e"
3) "f"
4) "g"
5) "h"

redis> LRANGE alphabets -7 -4
1) "b"
2) "c"
3) "d"
4) "e"

图4-18展示了这些LRANGE命令是如何根据给定的索引范围去获取列表元素的。

1.9.1 获取列表包含的所有元素

一个快捷地获取列表包含的所有元素的方法,就是使用0作为起始索引、-1作为结束索引去调用LRANGE命令,这种方法非常适合于查看长度较短的列表:

redis> LRANGE alphabets 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"
8) "h"

img

图4-18 LRANGE命令获取索引范围内元素的过程

1.9.2 处理超出范围的索引

与LINDEX一样,LRANGE命令也需要处理超出范围的索引:

如果用户给定的起始索引和结束索引都超出了范围,那么LRANGE命令将返回空列表作为结果。

如果用户给定的其中一个索引超出了范围,那么LRANGE命令将对超出范围的索引进行修正,然后再执行实际的范围获取操作;其中超出范围的起始索引会被修正为0,而超出范围的结束索引则会被修正为-1。

以下代码展示了LRANGE命令在遇到两个超出范围的索引时,返回空列表的例子:

redis> LRANGE alphabets 50 100
(empty list or set)

redis> LRANGE alphabets -100 -50
(empty list or set)

以下代码展示了LRANGE命令在遇到只有一个超出范围的索引时,对索引进行修正并返回元素的例子:

redis> LRANGE alphabets -100 5
1) "a"  -- 位于索引0上的元素
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"  -- 位于索引5上的元素

redis> LRANGE alphabets 5 100
1) "f"  -- 位于索引5上的元素
2) "g"
3) "h"  -- 位于索引-1上的元素

在执行LRANGE alphabets-1005调用时,LRANGE命令会把超出范围的起始索引-100修正为0,然后执行LRANGE alphabets 05调用;而在执行LRANGE alphabets 5100调用时,LRANGE命令会把超出范围的结束索引100修正为-1,然后执行LRANGE alphabets 5-1调用。

1.9.3 其他信息

复杂度:O(N),其中N为给定列表的长度。

版本要求:LRANGE命令从Redis 1.0.0开始可用。

示例:分页

对于互联网上每一个具有一定规模的网站来说,分页程序都是必不可少的:新闻站点、博客、论坛、搜索引擎等,都会使用分页程序将数量众多的信息分割为多个页面,使得用户可以以页为单位浏览网站提供的信息,并以此来控制网站每次取出的信息数量。图4-19就展示了一个使用分页程序对用户发表的论坛主题进行分割的例子。

img

图4-19 论坛中的分页示例

代码清单4-2展示了一个使用列表实现分页程序的方法,这个程序可以将给定的元素有序地放入一个列表中,然后使用LRANGE命令从列表中取出指定数量的元素,从而实现分页这一概念。

代码清单4-2 使用列表实现的分页程序:/list/paging.py

class Paging:

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

    def add(self, item):
        """
        将给定元素添加到分页列表中
        """
        self.client.lpush(self.key, item)

    def get_page(self, page_number, item_per_page):
        """
        从指定页数中取出指定数量的元素
        """
        # 根据给定的page_number (页数)和item_per_page (每页包含的元素数量),计算出指定
        # 分页元素在列表中所处的索引范围。例子:如果page_number = 1,item_per_page = 
        # 10,那么程序计算得出的起始索引就是0,而结束索引则是9
        start_index = (page_number - 1) * item_per_page
        end_index = page_number * item_per_page - 1
        # 根据索引范围从列表中获取分页元素
        return self.client.lrange(self.key, start_index, end_index)

    def size(self):
        """
        返回列表目前包含的分页元素数量
        """
        return self.client.llen(self.key)

作为例子,我们可以通过执行以下代码,载入并创建出一个针对用户帖子的分页对象:

>>> from redis import Redis
>>> from paging import Paging
>>> client = Redis(decode_responses=True)
>>> topics = Paging(client, "user-topics")

并使用数字1~19作为用户帖子的ID,将它们添加到分页列表中:

>>> from redis import Redis
>>> from paging import Paging
>>> client = Redis(decode_responses=True)
>>> topics = Paging(client, "user-topics")

然后我们就可以使用分页程序,对这些帖子进行分页了:

>>> topics.get_page(1, 5)   # 以每页5个帖子的方式,取出第1页的帖子
['19', '18', '17', '16', '15']
>>> topics.get_page(2, 5)   # 以每页5个帖子的方式,取出第2页的帖子
['14', '13', '12', '11', '10']
>>> topics.get_page(1, 10)  # 以每页10个帖子的方式,取出第1页的帖子
['19', '18', '17', '16', '15', '14', '13', '12', '11', '10']

最后,我们可以通过执行以下代码,取得分页列表目前包含的元素数量:

>>> topics.size()
19

1.10 LSET:为指定索引设置新元素

用户可以通过LSET命令,为列表的指定索引设置新元素:

LSET list index new_element

LSET命令在设置成功时将返回OK。

例如,对于以下这个todo列表来说:

redis> LRANGE todo 0 -1
1) "buy some milk"
2) "watch tv"
3) "finish homework"

我们可以通过执行以下LSET命令,将todo列表索引1上的元素设置为"have lunch":

redis> LSET todo 1 "have lunch"
OK

redis> LRANGE todo 0 -1
1) "buy some milk"
2) "have lunch"  -- 新元素
3) "finish homework"

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

img

图4-20 LSET命令的执行过程

1.10.1 处理超出范围的索引

因为LSET命令只能对列表中已存在的索引进行设置,所以如果用户给定的索引超出了列表的有效索引范围,那么LSET命令将返回一个错误:

redis> LSET todo 100 "go to sleep"
(error) ERR index out of range

1.10.2 其他信息

复杂度:O(N),其中N为给定列表的长度。

版本要求:LSET命令从Redis 1.0.0版本开始可用。

1.11 LINSERT:将元素插入列表

通过使用LINSERT命令,用户可以将一个新元素插入列表某个指定元素的前面或者后面:

LINSERT list BEFORE|AFTER target_element new_element

LINSERT命令第二个参数的值可以是BEFORE或者AFTER,它们分别用于指示命令将新元素插入目标元素的前面或者后面。命令在完成插入操作之后会返回列表当前的长度。

例如,对于lst列表:

redis> LRANGE lst 0 -1
1) "a"
2) "b"
3) "c"

我们可以通过执行以下LINSERT命令,将元素"10086"插入元素"b"的前面:

redis> LINSERT lst BEFORE "b" "10086"
(integer) 4

redis> LRANGE lst 0 -1
1) "a"
2) "10086"
3) "b"
4) "c"

还可以通过执行以下LINSERT命令,将元素"12345"插入元素"c"的后面:

redis> LINSERT lst AFTER "c" "12345"
(integer) 5

redis> LRANGE lst 0 -1
1) "a"
2) "10086"
3) "b"
4) "c"
5) "12345"

图4-21展示了上述两个LINSERT命令的执行过程。

img

图4-21 LINSERT命令的执行过程

1.11.1 处理不存在的元素

为了执行插入操作,LINSERT命令要求用户给定的目标元素必须已经存在于列表当中。相反,如果用户给定的目标元素并不存在,那么LINSERT命令将返回-1表示插入失败:

redis> LINSERT lst BEFORE "not-exists-element" "new element"
(integer) -1

在插入操作执行失败的情况下,列表包含的元素将不会发生任何变化。

1.11.2 其他信息

复杂度:O(N),其中N为给定列表的长度。

版本要求:LINSERT命令从Redis 2.2.0版本开始可用。

1.12 LTRIM:修剪列表

LTRIM命令接受一个列表和一个索引范围作为参数,并移除列表中位于给定索引范围之外的所有元素,只保留给定范围之内的元素:

LTRIM list start end

LTRIM命令在执行完移除操作之后将返回OK作为结果。

例如,对于以下这个alphabets列表来说:

redis> RPUSH alphabets "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k"
(integer) 11

执行以下命令可以让列表只保留索引0到索引6范围内的7个元素:

redis> LTRIM alphabets 0 6
OK
redis> LRANGE alphabets 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"

在此之后,我们可以继续执行以下命令,让列表只保留索引3到索引5范围内的3个元素:

redis> LTRIM alphabets 3 5
OK
redis> LRANGE alphabets 0 -1
1) "d"
2) "e"
3) "f"

图4-22展示了以上两个LTRIM命令对alphabets列表进行修剪的整个过程。

img

图4-22 LTRIM命令的执行过程

1.12.1 处理负数索引

与LRANGE命令一样,LTRIM命令不仅可以处理正数索引,还可以处理负数索引。

以下代码展示了如何通过给定负数索引,让LTRIM命令只保留列表的最后5个元素:

redis> RPUSH numbers 0 1 2 3 4 5 6 7 8 9
(integer) 10

redis> LTRIM numbers -5 -1
OK

redis> LRANGE numbers 0 -1
1) "5"
2) "6"
3) "7"
4) "8"
5) "9"

1.12.2 其他信息

复杂度:O(N),其中N为给定列表的长度。

版本要求:LTRIM命令从Redis 1.0.0版本开始可用。

1.13 LREM:从列表中移除指定元素

用户可以通过LREM命令移除列表中的指定元素:

LREM list count element

count参数的值决定了LREM命令移除元素的方式:

如果count参数的值等于0,那么LREM命令将移除列表中包含的所有指定元素。

如果count参数的值大于0,那么LREM命令将从列表的左端开始向右进行检查,并移除最先发现的count个指定元素。

如果count参数的值小于0,那么LREM命令将从列表的右端开始向左进行检查,并移除最先发现的abs(count)个指定元素(abs(count)即count的绝对值)。

LREM命令在执行完毕之后将返回被移除的元素数量作为命令的返回值。

举个例子,对于以下3个包含相同元素的列表来说:

redis> RPUSH sample1 "a" "b" "b" "a" "c" "c" "a"
(integer) 7

redis> RPUSH sample2 "a" "b" "b" "a" "c" "c" "a"
(integer) 7

redis> RPUSH sample3 "a" "b" "b" "a" "c" "c" "a"
(integer) 7

执行以下命令将移除sample1列表包含的所有"a"元素:

redis> LREM sample1 0 "a"
(integer) 3  -- 移除了3个"a"元素

redis> LRANGE sample1 0 -1
1) "b"  -- 列表里面已经不再包含"a"元素
2) "b"
3) "c"
4) "c"

而执行以下命令将移除sample2列表最靠近列表左端的2个"a"元素:

redis> LREM sample2 2 "a"
(integer) 2  -- 移除了2个"a"元素

redis> LRANGE sample2 0 -1
1) "b"
2) "b"
3) "c"
4) "c"
5) "a"

因为上面的LREM命令只要求移除最先发现的2个"a"元素,所以位于列表最右端的"a"元素并没有被移除,图4-23展示了这个LREM命令的执行过程。

img

图4-23 从sample2列表中移除最靠近列表左端的2个"a"元素

最后,执行以下命令将移除sample3列表最靠近列表右端的2个"a"元素:

redis> LREM sample3 -2 "a"
(integer) 2  -- 移除了2个"a"元素

redis> LRANGE sample3 0 -1
1) "a"
2) "b"
3) "b"
4) "c"
5) "c"

因为上面的LREM调用只要求移除最先发现的2个"a"元素,所以位于列表最左端的"a"元素并没有被移除,图4-24展示了这个LREM调用的执行过程。

img

图4-24 从sample3列表中移除最靠近列表右端的2个"a"元素

其他信息

复杂度:O(N),其中N为给定列表的长度。

版本要求:LREM命令从Redis 1.0.0版本开始可用。

示例:待办事项列表

现在很多人都会使用待办事项软件(也就是通常说的TODO软件)来管理日常工作,这些软件通常会提供一些列表,用户可以将要做的事情记录在待办事项列表中,并将已经完成的事项放入已完成事项列表中。图4-25就展示了一个使用待办事项软件记录日常生活事项的例子。

img

图4-25 使用待办事项软件记录日常生活事项

代码清单4-3展示了一个使用列表实现的待办事项程序,这个程序的核心概念是使用两个列表来分别记录待办事项和已完成事项:

当用户添加一个新的待办事项时,程序就把这个事项放入待办事项列表中。

当用户完成待办事项列表中的某个事项时,程序就把这个事项从待办事项列表中移除,并放入已完成事项列表中。

代码清单4-3 代码事项程序:/list/todo_list.py

def make_todo_list_key(user_id):
    """
    存储待办事项的列表
    """
    return user_id + "::todo_list"

def make_done_list_key(user_id):
    """
    存储已完成事项的列表
    """
    return user_id + "::done_list"


class TodoList:

    def __init__(self, client, user_id):
        self.client = client
        self.user_id = user_id
        self.todo_list = make_todo_list_key(self.user_id)
        self.done_list = make_done_list_key(self.user_id)

    def add(self, event):
        """
        将指定事项添加到待办事项列表中
        """
        self.client.lpush(self.todo_list, event)

    def remove(self, event):
        """
        从待办事项列表中移除指定的事项
        """
        self.client.lrem(self.todo_list, 0, event)

    def done(self, event):
        """
        将待办事项列表中的指定事项移动到已完成事项列表,以此来表示该事项已完成
        """
        # 从待办事项列表中移除指定事项
        self.remove(event)
        # 并将它添加到已完成事项列表中
        self.client.lpush(self.done_list, event)

    def show_todo_list(self):
        """
        列出所有待办事项
        """
        return self.client.lrange(self.todo_list, 0, -1)

    def show_done_list(self):
        """
        列出所有已完成事项
        """
        return self.client.lrange(self.done_list, 0, -1)

done()方法是TodoList程序的核心,它首先会使用LREM命令从代办事项列表中移除指定的事项,然后再将该事项添加到已完成事项列表中,使得该事项可以在代办事项列表中消失,转而出现在已完成列表中。

作为例子,我们可以通过执行以下代码,创建出一个TODO列表对象:

>>> from redis import Redis
>>> from todo_list import TodoList
>>> client = Redis(decode_responses=True)
>>> todo = TodoList(client, "peter's todo")

然后通过执行以下代码,向TODO列表中加入待完成事项:

>>> todo.add("go to sleep")
>>> todo.add("finish homework")
>>> todo.add("watch tv")
>>> todo.add("have lunch")
>>> todo.add("buy some milk")
>>> todo.show_todo_list()
['buy some milk', 'have lunch', 'watch tv', 'finish homework', 'go to sleep']

当完成某件事情之后,我们可以把它从待办事项列表移动到已完成事项列表:

>>> todo.done("buy some milk")
>>> todo.show_todo_list()
['have lunch', 'watch tv', 'finish homework', 'go to sleep']
>>> todo.show_done_list()
['buy some milk']

最后,如果我们不再需要去做某件事情,那么可以把它从待办事项列表中移除:

>>> todo.remove("watch tv")
>>> todo.show_todo_list()
['have lunch', 'finish homework', 'go to sleep']

1.14 BLPOP:阻塞式左端弹出操作

BLPOP命令是带有阻塞功能的左端弹出操作,它接受任意多个列表以及一个秒级精度的超时时限作为参数:

BLPOP list [list ...] timeout

BLPOP命令会按照从左到右的顺序依次检查用户给定的列表,并对最先遇到的非空列表执行左端元素弹出操作。如果BLPOP命令在检查了用户给定的所有列表之后都没有发现可以执行弹出操作的非空列表,那么它将阻塞执行该命令的客户端并开始等待,直到某个给定列表变为非空,又或者等待时间超出给定时限为止。

当BLPOP命令成功对某个非空列表执行了弹出操作之后,它将向用户返回一个包含两个元素的数组:数组的第一个元素记录了执行弹出操作的列表,即被弹出元素的来源列表,而数组的第二个元素则是被弹出元素本身。

比如在以下这个BLPOP命令执行示例中,被弹出的元素"a"就来源于列表alphabets:

redis> BLPOP alphabets 5  -- 尝试弹出alphabets列表的最左端元素,最多阻塞5s
1) "alphabets"            -- 被弹出元素的来源列表
2) "a"                    -- 被弹出元素

如果用户使用的是redis-cli客户端,并且在执行BLPOP命令的过程中曾经被阻塞过,那么客户端还会将被阻塞的时长也打印出来:

redis> BLPOP message-queue 5
1) "message-queue"
2) "hello world!"
(1.60s)    -- 客户端执行这个命令时被阻塞了1.6s

注意,这里展示的阻塞时长只是redis-cli客户端为了方便用户而添加的额外信息,BLPOP命令返回的结果本身并不包含这一信息。

1.14.1 解除阻塞状态

正如前面所说,当BLPOP命令发现用户给定的所有列表都为空时,就会让执行命令的客户端进入阻塞状态。如果在客户端被阻塞的过程中,有另一个客户端向导致阻塞的列表推入了新的元素,那么该列表就会变为非空,而被阻塞的客户端也会随着BLPOP命令成功弹出列表元素而重新回到非阻塞状态。

作为例子,表4-1展示了一个客户端从被阻塞到解除阻塞的整个过程。

表4-1 客户端A从被阻塞到解除阻塞的整个过程

img

img

如果在同一时间,有多个客户端因为同一个列表而被阻塞,那么当导致阻塞的列表变为非空时,服务器将按照“先阻塞先服务”的规则,依次为被阻塞的各个客户端弹出列表元素。

比如表4-2就展示了一个服务器按照先阻塞先服务规则处理被阻塞客户端的例子,在这个例子中,A、B、C这3个客户端先后执行了BLPOP lst 10命令,并且都因为lst列表为空而被阻塞,如果在这些客户端被阻塞期间,客户端D执行了RPUSH lst"hello""world""again"命令,那么服务器首先会处理客户端A的BLPOP命令,并将被弹出的"hello"元素返回给它;接着处理客户端B的BLPOP命令,并将被弹出的"world"元素返回给它;最后处理客户端C的BLPOP命令,并将被弹出的"again"元素返回给它。

表4-2 先阻塞先服务器处理示例

img

最后,如果被推入列表的元素数量少于被阻塞的客户端数量,那么先被阻塞的客户端将会先解除阻塞,而未能解除阻塞的客户端则需要继续等待下次推入操作。

比如,如果有5个客户端因为列表为空而被阻塞,但是推入列表的元素只有3个,那么最先被阻塞的3个客户端将会解除阻塞状态,而剩下的2个客户端则会继续阻塞。

1.14.2 处理空列表

如果用户向BLPOP命令传入的所有列表都是空列表,并且这些列表在给定的时限之内一直没有变成非空列表,那么BLPOP命令将在给定时限到达之后向客户端返回一个空值,表示没有任何元素被弹出:

redis> BLPOP empty-list 5
(nil)
(5.04s)

1.14.3 列表名的作用

BLPOP命令之所以会返回被弹出元素的来源列表,是为了让用户在传入多个列表的情况下,知道被弹出的元素来源于哪个列表。

比如在以下这个示例中,通过BLPOP命令的回复,我们可以知道被弹出的元素来自于列表queue2,而不是queue1或者queue3:

redis> BLPOP queue1 queue2 queue3 5
1) "queue2"
2) "hello world!"

1.14.4 阻塞效果的范围

BLPOP命令的阻塞效果只对执行这个命令的客户端有效,其他客户端以及Redis服务器本身并不会因为这个命令而被阻塞。

1.14.5 其他信息

复杂度:O(N),其中N为用户给定的列表数量。

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

1.15 BRPOP:阻塞式右端弹出操作

BRPOP命令是带有阻塞功能的右端弹出操作,除了弹出的方向不同之外,其他方面都和BLPOP命令一样:

BRPOP list [list ...] timeout

作为例子,以下代码展示了如何使用BRPOP命令去尝试弹出给定列表的最右端元素:

redis> BRPOP queue1 queue2 queue3 10
1) "queue2"    -- 被弹出元素的来源列表
2) "bye bye"   -- 被弹出元素

其他信息

复杂度:O(N),其中N为用户给定的列表数量。

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

1.16 BRPOPLPUSH:阻塞式弹出并推入操作

BRPOPLPUSH命令是RPOPLPUSH命令的阻塞版本,BRPOPLPUSH命令接受一个源列表、一个目标列表以及一个秒级精度的超时时限作为参数:

BRPOPLPUSH source target timeout

根据源列表是否为空,BRPOPLPUSH命令会产生以下两种行为:

如果源列表非空,那么BRPOPLPUSH命令的行为就和RPOPLPUSH命令的行为一样,BRPOPLPUSH命令会弹出位于源列表最右端的元素,并将该元素推入目标列表的左端,最后向客户端返回被推入的元素。

如果源列表为空,那么BRPOPLPUSH命令将阻塞执行该命令的客户端,然后在给定的时限内等待可弹出的元素出现,或者等待时间超过给定时限为止。

举个例子,假设现在有list3、list4两个列表:

client-1> LRANGE list3 0 -1
1) "hello"

client-1> LRANGE list4 0 -1
1) "a"
2) "b"
3) "c"

如果我们以这两个列表作为输入执行BRPOPLPUSH命令,由于源列表list3非空,所以BRPOPLPUSH命令将不阻塞直接执行,就像RPOPLPUSH命令一样:

client-1> BRPOPLPUSH list3 list4 10
"hello"

client-1> LRANGE list3 0 -1
(empty list or set)

client-1> LRANGE list4 0 -1
1) "hello"
2) "a"
3) "b"
4) "c"

现在,由于list3为空,如果我们再次执行相同的BRPOPLPUSH命令,那么客户端client-1将被阻塞,直到我们从另一个客户端client-2向list3推入新元素为止:

client-1> BRPOPLPUSH list3 list4 10
"world"
(1.42s)  -- 被阻塞了1.42s

client-1> LRANGE list3 0 -1
(empty list or set)

client-1> LRANGE list4 0 -1
1) "world"
2) "hello"
3) "a"
4) "b"
5) "c"
client-2> RPUSH list3 "world"
(integer) 1

表4-3展示了客户端从被阻塞到解除阻塞的整个过程。

表4-3 阻塞BRPOPLPUSH命令的执行过程

img

1.16.1 处理源列表为空的情况

如果源列表在用户给定的时限内一直没有元素可供弹出,那么BRPOPLPUSH命令将向客户端返回一个空值,以此来表示此次操作没有弹出和推入任何元素:

redis> BRPOPLPUSH empty-list another-list 5
(nil)
(5.05s)  -- 客户端被阻塞了5.05s

与BLPOP命令和BRPOP命令一样,redis-cli客户端也会显示BRPOPLPUSH命令的阻塞时长。

1.16.2 其他信息

复杂度:O(1)。

版本要求:BRPOPLPUSH命令从Redis 2.2.0版本开始可用。

示例:带有阻塞功能的消息队列

在构建应用程序的时候,有时会遇到一些非常耗时的操作,比如发送邮件,将一条新微博同步给上百万个用户,对硬盘进行大量读写,执行庞大的计算等。因为这些操作非常耗时,所以如果我们直接在响应用户请求的过程中执行它们,那么用户就需要等待非常长时间。

例如,为了验证用户身份的有效性,有些网站在注册新用户的时候,会向用户给定的邮件地址发送一封激活邮件,用户只有在点击了验证邮件里面的激活链接之后,新注册的账号才能够正常使用。

下面这段伪代码展示了一个带有邮件验证功能的账号注册函数,这个函数不仅会为用户输入的用户名和密码创建新账号,还会向用户给定的邮件地址发送一封激活邮件:

def register(username, password, email):
    # 创建新账号
    create_new_account(username, password)
    # 发送激活邮件
    send_validate_email(email)
    # 向用户返回注册结果
    ui_print("账号注册成功,请访问你的邮箱并激活账号。")

因为邮件发送操作需要进行复杂的网络信息交换,所以它并不是一个快速的操作,如果我们直接在send_validate_email()函数中执行邮件发送操作,那么用户可能就需要等待较长一段时间才能看到ui_print()函数打印出的反馈信息。

为了解决这个问题,在执行send_validate_email()函数的时候,我们可以不立即执行邮件发送操作,而是将邮件发送任务放入一个队列中,然后由后台的线程负责实际执行。这样,程序只需要执行一个入队操作,就可以直接向用户反馈注册结果了,这比实际地发送邮件之后再向用户反馈结果要快得多。

代码清单4-4展示了一个使用Redis实现的消息队列,它使用RPUSH命令将消息推入队列,并使用BLPOP命令从队列中取出待处理的消息。

代码清单4-4 使用列表实现的消息队列:/list/message_queue.py

class MessageQueue:

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

    def add_message(self, message):
        """
        将一条消息放入队列中
        """
        self.client.rpush(self.queue_name, message)

    def get_message(self, timeout=0):
        """
        从队列中获取一条消息,如果暂时没有消息可用,那么就在timeout参数指定的时限内阻塞并
        等待可用消息出现。
        timeout参数的默认值为0,表示一直等待直到消息出现为止
        """
        # blpop的结果可以是None,也可以是一个包含两个元素的元组,元组的第一个元素是弹出元
        # 素的来源队列,而第二个元素则是被弹出的元素
        result = self.client.blpop(self.queue_name, timeout)
        if result is not None:
            source_queue, poped_item = result
            return poped_item

    def len(self):
        """
        返回队列目前包含的消息数量
        """
        return self.client.llen(self.queue_name)

为了使用这个消息队列,我们通常需要用到两个客户端:

一个客户端作为消息的发送者(sender),负责将待处理的消息推入队列中。

而另一个客户端作为消息的接收者(receiver)和消费者(consumer),负责从队列中取出消息,并根据消息内容进行相应的处理工作。

下面的这段代码展示了一个简单的消息接收者,在没有消息的时候,这个程序将阻塞在mq.get_message()调用上面;当有消息(邮件地址)出现时,程序就会打印出该消息并发送邮件:

>>> from redis import Redis
>>> from message_queue import MessageQueue
>>> client = Redis(decode_responses=True)
>>> mq = MessageQueue(client, 'validate user email queue')
>>> while True:
...   email_address = mq.get_message()  # 阻塞直到消息出现
...   send_email(email_address)         # 打印出邮件地址并发送邮件
...
peter@exampl.com
jack@spam.com
tom@blahblah.com

以下代码展示了消息发送者是如何将消息推入队列中的:

>>> from redis import Redis
>>> from message_queue import MessageQueue
>>> client = Redis(decode_responses=True)
>>> mq = MessageQueue(client, 'validate user email queue')
>>> mq.add_message("peter@exampl.com")
>>> mq.add_message("jack@spam.com")
>>> mq.add_message("tom@blahblah.com")

1.阻塞弹出操作的应用

下面展示的消息队列之所以使用BLPOP命令而不是LPOP命令来实现出队操作,是因为阻塞弹出操作可以让消息接收者在队列为空的时候自动阻塞,而不必手动进行休眠,从而使得消息处理程序的编写变得更为简单直接,并且还可以有效地节约系统资源。

作为对比,以下代码展示了在使用LPOP命令实现出队操作的情况下,如何实现类似上面展示的消息处理程序:

while True:
    # 尝试获取消息,如果没有消息,那么返回None
    email_address = mq.get_message()
    if email_address is not None:
        # 有消息,发送邮件
        send_email(email_address)
    else:
        # 没有消息可用,休眠100ms之后再试
        sleep(0.1)

因为缺少自动的阻塞操作,所以这个程序在没有取得消息的情况下,只能以100ms一次的间隔去尝试获取消息,如果队列为空的时间比较长,那么这个程序就会发送很多多余的LPOP命令,并因此浪费很多CPU资源和网络资源。

2.使用消息队列实现实时提醒

消息队列除了可以在应用程序的内部使用,还可以用于实现面向用户的实时提醒系统。

比如,如果我们在构建一个社交网站,那么可以使用JavaScript脚本,让客户端以异步的方式调用MessageQueue类的get_message()方法,然后程序就可以在用户被关注的时候、收到了新回复的时候或者收到新私信的时候,通过调用add_message()方法来向用户发送提醒信息。

1.17 重点回顾

Redis的列表是一种线性的有序结构,可以按照元素推入列表中的顺序来存储元素,并且列表中的元素可以重复出现。

用户可以使用LPUSH、RPUSH、RPOP、LPOP等多个命令,从列表的两端推入或者弹出元素,也可以通过LINSERT命令将新元素插入列表已有元素的前面或后面。

用户可以使用LREM命令从列表中移除指定的元素,或者直接使用LTRIM命令对列表进行修剪。

当用户传给LRANGE命令的索引范围超出了列表的有效索引范围时,LRANGE命令将对传入的索引范围进行修正,并根据修正后的索引范围来获取列表元素。

BLPOP、BRPOP和BRPOPLPUSH是阻塞版本的弹出和推入命令,如果用户给定的所有列表都为空,那么执行命令的客户端将被阻塞,直到给定的阻塞时限到达或者某个给定列表非空为止。

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

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