redis列表键(数据结构篇)

列表(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 队列,因为它需要优先处理推入时间最长的消息。
在这里插入图片描述
这种做法有几个好处:

  1. web 服务器可以在保存好用户发布的消息、并将消息推入到 队列之后,立即返回,这样 web 服务器就可以以最快的㏿度 对用户进行响应。
  2. web 服务器不会被消息推送操作影响, 这有助于提升效率和简化程序逻辑。
  3. 因为推送操作由专门的消息服务器负责,所以我们可以针对性地进行优化。
    通过使用 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),每个项按照它们被推入到列表的位置来排列,并且 这些项可以是重复的。
在这里插入图片描述

发布了252 篇原创文章 · 获赞 151 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39885372/article/details/104240393