文章目录
- 列表(list)
- 推入和弹出操作
- 从列表的左端推入值
- 从列表的左端推入多个值
- 从列表的右端推入值
- 从列表的右端推入多个值
- 从列表的两端弹出项
- LPOP/RPOP 示例
- 长度、索引和范围操作
- 获取列表的长度
- 返回给定索引上的项
- 返回给定索引范围之内的所有项
- 示例:使用列表实现用户时间线
- 更新时间线
- 获取消息
- 用户时间线的 API 及其实现
- 用户时间线使用示例
- 插入和删除操作
- 设置指定索引上的列表项
- 在指定位置插入列表项
- 从列表中删除指定的值
- LREM 示例
- 修剪列表
- 示例:实现 LLOOGG.com 的记录储存功能
- 定长先进先出队列
- 定长先进先出队列的 API 及其实现
- 定长先进先出队列的使用示例
- 阻塞式弹出操作
- 阻塞弹出命令
- BLPOP/BRPOP 示例
- 情形一:非阻塞
- 情形二:阻塞并超时
- 情形三:阻塞并弹出情况
- BLPOP/BRPOP 的先到先服务原则
- 示例:使用列表构建消息队列
- 使用消息队列解耦消息发布和消息推送操作
- 消息队列的 API 及其实现
- 消息队列的使用示例
- 真实世界的消息队列
- 复习
列表(list)
以有序的方式储存多个可重复的值。
一个列表可以包含一个或以上数量的 项(item),每个项按照它们被推入到列表的位置来排列。每个列表项所处的位置决定了这个项的索引值(index),索引以 0 为开始,从列表的左端到右端依次 递增,位于列表最左端(表 头)的项的索引为 0 ,而位于列表最右端(表尾)的 项的索引为 N-1 ,其中 N 为列表的长度。
列表包含的项可以出现重复,它们不必是唯一的。
推入和弹出操作
了解如何向列表添加项,以及如何从列表里面 删除项。
从列表的左端推入值
LPUSH key value [value …]
将一个或以上数量的值依次推入到列表的左端,命令返回新 值被推入之后,列表目前包含的项数量。复杂度为 O(N) ,其中 N 为被推入值的数量,如果只推入一个 值,那么命令的复杂度为 O(1) 。
redis> LPUSH lst "Lua"
(integer) 1
redis> LPUSH lst "Python"
(integer) 2
redis> LPUSH lst "C"
(integer) 3
从列表的左端推入多个值
如果执行 LPUSH 命令时给定了多个值,那么各个值将按照给定时的顺序,从左到右依次地被推入到列
表的左端。
举个例子,执行命令:
redis> LPUSH lst "Lua" "Python" "C"
(integer) 3
和依次执行以下三个命令的效果一 样:
LPUSH lst "Lua"
LPUSH lst "Python"
LPUSH lst "C"
从列表的右端推入值
RPUSH key value [value …]
将一个或以上数量的值依次推入到列表的右端,命令返回新 值被推入之后,列表目前包含的 项数量。
复杂度为 O(N) ,其中 N 为被推入值的数量,如果只推入一个 值,那么命令的复杂度为 O(1) 。
redis> RPUSH lst "Clojure"
(integer) 1
redis> RPUSH lst "Ruby"
(integer) 2
redis> RPUSH lst "C"
(integer) 3
从列表的右端推入多个值
如果执行 RPUSH 命令时给定了多个值,那么各个值将按照给定时的顺序,从左到右依次地被推入到列表的右端。
举个例子,执行命令:
redis> RPUSH lst "Clojure" "Ruby" "C"
(integer) 3
和依次执行以下三个命令的效果一 样:
RPUSH lst "Clojure"
RPUSH lst "Ruby"
RPUSH lst "C
从列表的两端弹出项
LPOP/RPOP 示例
redis> RPUSH lst "Clojure" "Ruby" "C" "Python" "Lua"
(integer) 5
redis> LPOP lst
"Clojure"
redis> LPOP lst
"Ruby"
redis> RPOP lst
"Lua"
redis> RPOP lst
"Python"
长度、索引和范围操作
LLEN、LINDEX、LLRANGE
获取列表的长度
LLEN key
返回列表键 key 的长度,也即是,返回列表包含的列表 项数量。
因为 Redis 会记录每个列表的长度,所以这个命令无须遍历列表,它的复杂度为 O(1) 。
redis> LLEN lst
(integer) 5
redis> LPOP lst
"Clojure"
redis> LLEN lst
(integer) 4
返回给定索引上的项
LINDEX key index
返回列表键 key 中,指定索引 index 上的列表项。index 索引可以是正数或者负数。
复杂度为 O(N) ,N 列表的长度。
redis> LINDEX lst 1
"Ruby"
redis> LINDEX lst 4
"Lua"
redis> LINDEX lst -3
"C"
返回给定索引范围之内的所有项
LRANGE key start stop
返回列表键 key 中,从索引 start 至索引 stop 范围内的所有列表项。两个索引参数都可以是正数或者负数。
复杂度为 O(N) , N 为被返回的列表项数量。
redis> LRANGE lst 0 2
1) "Clojure"
2) "Ruby"
3) "C"
redis> LRANGE lst -3 -1
1) "C"
2) "Python"
3) "Lua
示例:使用列表实现用户时间线
更新时间线
获取消息
每当有人访问用户的时间线时,程序就会访问时间线列表,并根据列表中储存的 ID 来获取用户时间线上的消息。通过访问时间线列表中的不同范围,程序可以获取到不同时期的消息,越接近表头的消息就越新,越接近表尾的消息就越旧。
用户时间线的 API 及其实现
时间线的代码实现可以在 timeline.py 看到。
#timeline.py
# encoding: utf-8
def create_timeline_key(user_name):
"""
创建 'user::<name>::timeline' 格式的时间线键名
举个例子,输入 'huangz' 将返回键名 'user::huangz::timeline'
"""
return 'user::' + user_name + '::timeline'
class Timeline:
def __init__(self, user_name, client):
self.key = create_timeline_key(user_name)
self.client = client
def push(self, message_id):
return self.client.lpush(self.key, message_id)
def fetch_recent(self, n):
return self.client.lrange(self.key, 0, n-1)
def fetch_from_index(self, start_index, n):
return self.client.lrange(self.key, start_index, start_index+n-1)
用户时间线使用示例
# 为用户 peter 创建时间线
tl = Timeline('peter', client)
# 将消息 10086 推入至时间线最前端
tl.push(10086)
tl.fetch_recent(5)
# [10086, 10025, 9251, 8769, 8213]
tl.fetch_from_index(5, 3)
# [7925, 7000, 6928]
# 之后只要不断地调用 fetch_from_index
# 就可以继续获取更早期的消息
插入和删除操作
LSET、LINSERT、LREM、LTRIM
设置指定索引上的列表项
LSET key index value
将列表键 key 索引 index 上的列表项设置为value ,设置成功时命令返回 OK 。
如果 index 参数超过了列表的索引范围,那么命令返回一个错误。
针对表头和表尾节点进行处理时(index 为 0 或者 -1),命令的复杂度为 O(1) ;其他情况下,命令的复杂度为 O(N) ,N 为列表的长度。
redis> RPUSH lst "Clojure" "Ruby" "C" "Python" "Lua"
(integer) 5
redis> LSET lst 0 "Common Lisp"
OK
在指定位置插入列表项
LINSERT key BEFORE|AFTER pivot value
根据命令调用时传递的是 BEFORE 选项还是 AFTER 选项,将值 value 插入到指定列表项 pivot 的之前或者之后。当 pivot 不存在于列表 key 时,不执行任何操作。返回 -1 表示 pivot 不存在;返回 0 表示键 key 不存在;插入成功时则返回列表当前的长度。
复杂度为 O(N) ,N 为列表长度。
redis> RPUSH lst "Clojure" "C" "Python" "Lua"
(integer) 4
redis> LINSERT lst BEFORE "C" "Ruby"
(integer) 5
从列表中删除指定的值
LREM key count value
根据参数 count 的值,移除列表中与参数 value 相等的列表项:
• 如果 count > 0 ,那么从表头开始向表尾搜索,移除最多 count 个值为 value 的列表项。
• 如果 count < 0 ,那么从表尾开始向表 头搜索,移除最多 abs(count) 个值为 value 的列表项。
• 如果 count = 0 ,那么移除列表中所有 值为 value 的列表项。
命令返回被移除列表项的数量。
命令的复杂度为 O(N) ,N 为列表的长度。
LREM 示例
redis> RPUSH lst "app" "zoo" "spam"
"app" "zoo" "egg" "app"
(integer) 7
redis> LREM lst 0 "zoo"
(integer) 2
redis> LREM lst 1 "app"
(integer) 1
redis> LREM lst -1 "app"
(integer) 1
修剪列表
LTRIM key start stop
对一个列表进行修剪(trim),让列表只保留指定索引范 围内的列表项,而将不在范围内的其他列表项全部删除。两个索引都可以是正数或者 负数。
命令执行成功时返回 OK ,复杂度为 O(N) ,N 为被移除列表项的数量。
redis> RPUSH lst "Clojure" "Ruby" "C" "Python" "Lua"
(integer) 5
redis> LTRIM lst 0 2
OK
示例:实现 LLOOGG.com 的记录储存功能
在之前介绍 LLOOGG.com 的时候,我们提到过, LLOOGG.com 允许用户储存最新的 5 条至 10,000 条浏览记录,以便进行查看。LLOOGG.com 使用列表来储存浏览记录,当列表的长度达到了用户指定的最大长度 之后,程序每向列表推入一个新的记录,就需要从列表中弹出一个最旧的记录。
定长先进先出队列
从数据结构的角度来看, LLOOGG.com 为每个被监视网站构建的都是一个定 长先进先出队列(FixedSize First In First Out Queue),这种结构具有以下特点:
• 固定长度(定长):队列的长度(也即是队列包含的项数量)不能超过一个给定的最大值。
• 先进先出:当队列的长度到达最大值时,每向队列推入一个新值,程序就需要从队列中弹出一个最早被推入到列表里面的 值。
通过使用 Redis 列表键,我们也可以构建一个这样的定长先进先出队列。
定长先进先出队列的 API 及其实现
这个定长先进先出队列的实现代码可以在 fixed_fifo.py 文件里找到。
#fixed_fifo.py
# encoding: utf-8
class FixedFIFO:
def __init__(self, key, max_length, client):
self.key = key
self.max_length = max_length
self.client = client
def enqueue(self,item):
# 这里存在一个竞争条件:
# 如果客户端在 LPUSH 成功之后断线
# 那么队列里将有超过最大长度数量的值存在
# 等我们学习了事务之后就来修复这个竞争条件
# 将值推入列表
self.client.lpush(self.key, item)
# 如果有必要的话,进行修剪以便让列表保持在最大长度之内
self.client.ltrim(self.key, 0, self.max_length-1)
# 返回 1 表示入队成功
return 1
def dequeue(self):
return self.client.rpop(self.key)
def get_all_items(self):
定长先进先出队列的使用示例
阻塞式弹出操作
BLPOP 和 BRPOP
阻塞弹出命令
BLPOP/BRPOP 示例
redis> BLPOP empty-1 empty-2 empty-3 5 # 命令依次访问三个列表,发现它们都为空,于是阻塞
(nil) # 返回 nil 表示等待超时
(5.07s) # 客户端被阻塞的时长
redis> RPUSH lst "one" "two" "three"
(integer) 3
redis> BLPOP empty-1 empty-2 lst empty-3 5 # 命令发现 lst 非空,于是弹出
1) "lst" # 执行弹出操作的列表
2) "one" # 被弹出的项
redis> BLPOP empty-1 empty-2 empty-3 5 # 在阻塞的过程中,有列表可以执行弹出操作
1) "empty-3" # 执行弹出操作的列表
2) "hello" # 被弹出的项
(1.84s) # 客户端被阻塞的时长
情形一:非阻塞
当发现给定的列表中有至少一个非空列表 时,BLPOP 或者 BRPOP 就会立即从那个列表里面 弹出元素,在这种情况下, BLPOP 就像一个接受多参数的 LPOP 命令,而 BRPOP 就像一个接受多参数的 RPOP 命令。
情形二:阻塞并超时
如果所有给定列表都是空的,那么 BLPOP/BRPOP 将被阻塞。如果在阻塞的过程中,给定的列表一直没有新项被推入,那么当设定的超时时间到达之后,命令将向被阻塞的客户端返回 nil 。
情形三:阻塞并弹出情况
如果在客户端 X 被阻塞的过程中,有另一个客户端 Y 给㐀成客户端 X 被阻塞的列表推入了新项,那么服务器会将这个新项返 回给客户端 X。
BLPOP/BRPOP 的先到先服务原则
如果有多个客户端同时因为某个列表而被阻塞,那么当有新 值被推入到这个列表时,服务器会按照先到先服务(first in first service)原则,优先向最早被阻塞的客户端返回新值。 举个例子,假设列表 lst 为空,那么当客户端 X 执行命令 BLPOP lst timeout 时,客户端 X 将被阻塞。在此之后,客户端 Y 也执行命令 BLPOP lst timeout ,也因此被阻塞。如果这时,客户端 Z 执行命令 RPUSH lst “hello” ,将值 “hello” 推入列表 lst ,那么这个 “hello” 将被返回给客户端 X ,而不是客户端 Y ,因为客户端 X 的被阻塞时间要早于客户端 Y 的被阻塞时间。
示例:使用列表构建消息队列
每当用户(发布者)在 Twitter 上发布一条新消息时,程序需要将这条消息推送给所有关注者,也即是,将 这条消息放入到每个关注者的用户时间线里面:
• 当关注者的数量比较少时(比如一百几十个),这个操作可以立即完成;
• 相反地,当关注者的数量比 较巨大时(比如几十万个、几百万个),那么推送操作需要花 费大量时间才能完成,发送消息的用户需要等待很久才能获得响应,这也会对web 服务器的性能㐀成影响。
使用消息队列解耦消息发布和消息推送操作
为了解决这个问题,每当用户发送消息的时候,程序都会将这条消息放入到一个消息 队列(message queue)里面,然后由专门的服务器在后台负责将这条消息推送给所有关注者。注意消息队列也是一个 FIFO 队列,因为它需要优先处理推入时间最长的消息。
这种做法有几个好处:
- web 服务器可以在保存好用户发布的消息、并将消息推入到 队列之后,立即返回,这样 web 服务器就可以以最快的㏿度 对用户进行响应。
- web 服务器不会被消息推送操作影响, 这有助于提升效率和简化程序逻辑。
- 因为推送操作由专门的消息服务器负责,所以我们可以针对性地进行优化。
通过使用 Redis 的列表键,以及阻塞弹出操作,我们也可以构㐀类似的消息队列。
消息队列的 API 及其实现
这个消息队列的实现可以在 message_queue.py 找到。
#message_queue.py
# encoding: utf-8
class MessageQueue:
def __init__(self, key, client):
self.key = key
self.client = client
def enqueue(self, item):
self.client.lpush(self.key, item)
def dequeue(self, timeout):
result = self.client.brpop(self.key, timeout)
if result:
poped_list, poped_item = result
return poped_item
def length(self):
return self.client.llen(self.key)
def get_all_items(self):
return self.client.lrange(self.key, 0, -1)
消息队列的使用示例
# web 服务器
q = MessageQueue('user::message::queue', client)
message_id = create_new_message(...) # 创建并储存用户发布的新消息,并返回消息的 ID
q.enqueue(message_id) # 将消息 ID 推入队列里面
# 消息服务器
q = MessageQueue('user::message::queue', client)
while server.is_running(): # 循环
message_id = q.dequeue(0) # 等待新消息出现
# 找到消息的所有接收者
# 将消息 ID 推入他们的时间线
# ...
真实世界的消息队列
消息队列不一定要自己制㐀,因 为已经有很多使用 Redis 列表作为后端的消息队列项目,并且它们的可用性和稳定性都经过了一定时间的考验,所以我们可以考虑使用这些现成的消息队列:
Python RQ http://python-rq.org/
Resque https://github.com/resque/resque
Celery http://www.celeryproject.org/
复习
回顾一下本节提到的知识点
一个列表键可以包含任意多个项(item),每个项按照它们被推入到列表的位置来排列,并且 这些项可以是重复的。