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 命令时的变化过程:
-
在 RPOPLPUSH 命令执行之前,list1 和 list2 都包含 3 个元素。
-
执行第 1 个 RPOPLPUSH 命令,弹出 list1 的最右端元素 "c",并将其推入 list2 的左端。
-
执行第 2 个 RPOPLPUSH 命令,弹出 list1 的最右端元素 "b",并将其推入 list2 的左端。
-
执行第 3 个 RPOPLPUSH 命令,弹出 list1 的最右端元素 "a",并将其推入 list2 的左端。
-
在以上 3 个 RPOPLPUSH 命令执行完毕之后,list1 将变为空列表,而 list2 则会包含 6 个元素。

源列表和目标列表相同
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 列表的整个变化过程:
-
在 RPOPLPUSH 命令执行之前,列表包含 "a"、"b"、"c" 3 个元素。
-
执行第 1 个 RPOPLPUSH 命令,将最右端元素 "c" 变为最左端元素。
-
执行第 2 个 RPOPLPUSH 命令,将最右端元素 "b" 变为最左端元素。
-
执行第 3 个 RPOPLPUSH 命令,将最右端元素 "a" 变为最左端元素。
-
在以上 3 个 RPOPLPUSH 命令执行完毕之后,列表又重新变回了原样。

正如上面展示的例子所示,通过对同一个列表重复执行 RPOPLPUSH
命令,我们可以创建出一个对元素进行轮换的列表,并且当我们对一个包含了 N 个元素的列表重复执行 N 次 RPOPLPUSH
命令之后,列表元素的排列顺序将变回原来的样子。
处理空列表
如果用户传给 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 的变化:
-
在执行 RPOPLPUSH 命令之前,list-x 包含 3 个元素,而 list-y 为空。
-
执行 RPOPLPUSH 命令,将 list-x 的最右端元素 "c" 弹出,并将其推入 list-y 的左端。
-
在 RPOPLPUSH 命令执行完毕之后,list-x 将包含 2 个元素,而 list-y 则包含 1 个元素。

示例:先进先出队列
先进先出队列(first in first out queue)是一种非常常见的数据结构,一般都会包含入队(enqueue)和出队(dequeue)这两个操作,其中入队操作会将一个元素放入队列中,而出队操作则会从队列中移除最先入队的元素。
先进先出队列的应用非常广泛,各式各样的应用程序中都有使用。举个例子,很多电商网站都会在节日时推出一些秒杀活动,这些活动会放出数量有限的商品供用户抢购,秒杀系统的一个特点就是在短时间内会有大量用户同时进行相同的购买操作,如果使用事务或者锁去实现秒杀程序,那么就会因为锁和事务的重试特性而导致性能低下,并且由于重试行为的存在,成功购买商品的用户可能并不是最早执行购买操作的用户,因此这种秒杀系统实际上是不公平的。
解决上述问题的方法之一就是把用户的购买操作都放入先进先出队列里面,然后以队列方式处理用户的购买操作,这样程序就可以在不使用锁或者事务的情况下实现秒杀系统,并且得益于先进先出队列的特性,这种秒杀系统可以按照用户执行购买操作的顺序来判断哪些用户可以成功执行购买操作,因此它是公平的。
代码清单4-1展示了一个使用 Redis 列表实现先进先出队列的方法。
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" 元素。