Bootstrap

[Redis#6] list | 命令 | 应用 | 消息队列 | 微博 Timeline

目录

List 列表

特点

2. 命令

头插和尾插

下标 range 查询

头删和尾删

LINSERT

LLEN

LREM

LTRIM

LSET

阻塞命令

BLPOP

BRPOP

操作 总结

3. 内部编码

ziplist(压缩列表)

linkedlist(链表)

✔️quicklist(快速链表) -> (现行方案)

4.使用场景

1. 消息队列

2. 分频道的消息队列

如何确定是哪个消费者“抢到”了元素?

1. 阻塞命令机制

2. 客户端处理逻辑

3. 微博 Timeline

示例

实现栈和队列:


List 列表

  • 列表类型用于 存储多个有序的字符串,如 abcde 五个元素从左到右组成一个有序列表,每个字符串称为元素,最多可存储 (2^{32}-1) 个元素。
  • 列表支持 两端插入(push)、弹出(pop、获取指定范围或索引的元素等操作
  • 列表可充当栈和队列的角色,在实际开发中应用广泛。

特点
  • 有序性:通过 索引 下标 可获取特定或范围内的元素。
  • 支持 前后 插入删除 的设计
  • 获取与删除 区别:如 lrem 1 b 删除列表中第一个 b 元素,而 lindex 4 仅获取元素,不影响列表长度。
  • 元素可重复:列表允许包含重复元素


2. 命令

头插和尾插

LPUSH

  • 从左侧插入元素,时间复杂度 (O(1)) 或 (O(N))。
  • 注意是按照键入在命令中的顺序,从左向右将命令中的元素插入到list中的
    • 例如LPUSH key 1 2 3 4,那么最后list呈现的结果为:4 3 2 1,采取的为头插

LPUSHX

  • 若键存在,则从左侧插入元素,否则返回。

RPUSH

  • 从右侧插入元素,时间复杂度 (O(1)) 或 (O(N))。

RPUSHX

  • 若键存在,则从右侧插入元素,否则返回。

下标 range 查询

LRANGE

  • 获取指定范围的元素,时间复杂度 (O(N))。
  • 注意Redis会尽可能地获取到给定区间的元素,如果给定区间非法,比如超出下标,就会尽可能地获取到对应的内容
    • Redis对于下标越界地处理方式类似于Python的切片操作

此处list有一个命名上的注意点:

  • lpushleft push,从左侧插入,即头插
  • rpushright push,从右侧插入,即尾插
  • lrangelist range,输出列表指定范围

同样是l开头,有时候表示list,有时候表示left,这个要注意。

另外的:

  • lpushxleft push exists,存在时插入

此处的x是选用了exsis中的一个字母,表示只有key存在才插入。

同样插入1 2 3 4 5,顺序与之前lpush插入刚好是反着的,因为rpush是尾插。

头删和尾删

LPOP

  • 从左侧弹出元素,时间复杂度 (O(1))。

RPOP

  • 从右侧弹出元素,时间复杂度 (O(1))。

注意:可一次删除多个

LINSERT
  • 功能:在指定位置插入元素。
  • 语法linsert key before|after pivot element
  • 说明:在元素 pivot 的前面或者后面插入 element 元素。如果有多个 pivot 元素,只在第一个 pivot 的位置插入。
  • 示例

LLEN
  • 功能:获取列表的长度。
  • 语法llen key
  • 说明:返回列表的长度,如果列表不存在则返回 0。
  • 示例:假设列表为 a b c,执行 llen mylist 返回 3。
LREM
  • 功能删除元素。
  • 语法lrem key count element
  • 说明:删除 countelement 元素。count 的取值有三种:
    • count > 0:从左往右删除。
    • count < 0:从右往左删除。
    • count = 0:删除所有元素。
  • 示例

如果传入-2 hello,此时从右往左删除,只有第一个hello被保留。

LTRIM
  • 功能:指定范围内元素保留,剩余的元素删除。
  • 语法ltrim key start stop
  • 说明:只保留 [start, stop] 闭区间内部的元素,其余的元素全部删除。
  • 示例

LSET
  • 功能修改指定下标的元素。
  • 语法lset key index element
  • 说明:将下标为 index 的元素改为 element,支持负数下标。如果下标越界,会返回一个报错。
  • 示例

第一次修改,将下标为3的元素修改为666。第二次插入,操作下标为100的元素,由于不存在,报错了。

阻塞命令

  • 先前的所有命令均为非阻塞命令,可以直接操作并立即得到结果。
  • 然而,Redis 的列表类型还提供了一些具有 阻塞性质 的命令

在多线程中,有一个生产消费模型其可以基于阻塞队列实现,主要满足以下两个性质:

  1. 如果阻塞队列满了,那么生产者阻塞
  2. 如果阻塞队列空了,那么消费者阻塞

Redis中,list只考虑队列为空的情况,也就是消费者。用户读取数据时,队列为空,那么用户陷入阻塞,直到队列有数据。

BLPOP
  • 功能读取并删除列表头部元素,如果列表为空则用户陷入阻塞。
  • 语法blpop key [key ...] timeout
  • 返回值:取出的元素或者nil

说明

  • 可以同时指定多个 key,即多个列表,只要任意一个列表有数据,就返回结果。
  • 设置超时时间 timeout为单位,超过时间则返回 nil
  • 超时时间设为 0,则一直阻塞,不会超时。
  • 阻塞发生在客户端,Redis 会将指令放入后台等待,继续处理其他请求。

示例

此处启用了两个客户端,左侧客户端blpop一个空列表,等待20s,随后陷入阻塞。接着右侧客户端插入一个元素到list9,随后左侧客户端立刻拿到数据并进行头删

BRPOP
  • 功能:读取并删除列表尾部元素,如果列表为空则用户陷入阻塞。
  • 语法brpop key [key ...] timeout
  • 说明:与 blpop 类似,但操作的是列表尾部。

操作 总结

操作类型

命令

添加

rpush、lpush、linsert

查找

lrange、lindex、llen

删除

lpop、rpop、lrem、ltrim

修改

lset

阻塞

blpop、brpop


3. 内部编码

ziplist(压缩列表)
  • 描述:一种内存紧凑的存储方式,适合存储数量较少且元素较小的列表。
  • 条件:(不用记数字,掌握思想~

  • 优点
    • 内存节省:使用连续的内存块存储数据,减少内存碎片和开销。
    • 结构简单:适合小规模数据,尤其在内存资源有限的情况下。
  • 缺点
    • 操作效率:数据量增加时,读写效率下降,线性查找特性导致操作复杂度较高。
    • 扩展性差:不适合大规模数据存储。
linkedlist(链表)
  • 描述:当列表类型无法满足 ziplist 条件时,使用 linkedlist 作为内部实现。
  • 优点
    • 头尾的插入删除非常高效。
  • 缺点
    • 中间部分的插入删除时间复杂度较高。
✔️quicklist(快速链表) -> (现行方案)
  • 描述:Redis 4.0 版本之后引入的更高效的列表编码方式,结合了 ziplistlinkedlist 的优点。
  • 结构
    • 外层列表仍然是 linkedlist 双链表结构。
    • 每个链表节点都是一个 ziplist,对中间部分的节点进行一定程度的压缩,提高效率。

在之前配置文件中的list-max-ziplist-entries和list-max-ziplist-value这两个属性由于list底层编码方式的改变,现在都不再使用了.


4.使用场景

1. 消息队列

  • 实现:Redis 可以使用 lpush + brpop 命令组合实现经典的阻塞式生产者-消费者模型队列。
  • 流程
    • 生产者:客户端使用 lpush 从列表左侧插入元素。
    • 消费者:多个消费者客户端使用 brpop 命令阻塞式地从队列中“争抢”队首元素。

  • 特点
    • 通过多个客户端来保证消费的负载均衡和高可用性。
    • 只有一个消费者能“抢到”元素。

2. 分频道的消息队列

  • 实现:Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念。
  • 流程
    • 生产者将消息推送到不同的键值(频道)。
    • 消费者:通过 brpop 不同的键值,实现订阅不同频道的理念。

  • 特点
    • 每个频道只有一个消费者能“抢到”元素。
    • 不同的消费者可以订阅不同的频道,确保某个主题的数据出现问题时不会影响其他频道。

思考

如何确定是哪个消费者“抢到”了元素?

1. 阻塞命令机制
  • brpop 命令:当消费者调用 brpop 命令时,如果指定的列表为空,消费者将进入阻塞状态,等待列表中有元素可用。
  • 多个消费者竞争:如果有多个消费者同时调用 brpop 命令,Redis 会确保只有一个消费者能够成功获取到元素。这个消费者 第一个被唤醒并成功执行 brpop 命令的 消费者。
2. 客户端处理逻辑
  • 唯一标识:每个消费者在执行 brpop 命令时,可以记录自己的唯一标识(如消费者ID)。
  • 日志记录:当消费者成功获取到元素后,可以在日志中记录这次操作,包括消费者ID、获取的元素内容和时间戳等信息。
  • 回调函数:在消费者应用中,可以设置回调函数来处理 brpop 命令的结果。回调函数中可以包含记录日志、更新状态等操作。

示例

假设我们有两个消费者(Consumer A 和 Consumer B)订阅同一个频道 key-1,生产者将消息推送到 key-1

生产者

lpush key-1 message1

消费者 A

import redis

client = redis.StrictRedis()

def handle_message(message):
    print(f"Consumer A got message: {message}")
    # 记录日志
    with open('consumer_a_log.txt', 'a') as log_file:
        log_file.write(f"Consumer A got message: {message}\n")

while True:
    message = client.brpop('key-1')
    if message:
        handle_message(message[1].decode('utf-8'))

消费者 B 同上类似

日志记录

Consumer A 的日志文件 consumer_a_log.txt

Consumer A got message: message1

Consumer B 的日志文件 consumer_b_log.txt

总结

  • 唯一标识:每个消费者有自己的唯一标识。
  • 日志记录:成功获取到元素后,记录日志。
  • 回调函数:设置回调函数处理 brpop 命令的结果。

通过上述方法,可以明确地知道是哪个消费者“抢到”了元素。


3. 微博 Timeline

  • 需求:每个用户都有属于自己的 Timeline(微博列表),需要分页展示文章列表。
  • 实现
  1. 每篇微博使⽤哈希结构存储,例如微博中3个属性:title、timestamp、content
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx

     2.向⽤⼾Timeline添加微博,user::mblogs作为微博的键

lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9

博客目录 通过 list 将每篇博客数据(hash) 组织起来了

    3.分页获取:分页获取用户的 Timeline,例如获取用户 1 的前 10 篇微博:

keylist = lrange user:1:mblogs 0 9
for key in keylist {
    hgetall key
}

问题

  • 1 + n 问题:如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线)模式批量提交命令
  • 或者微博不采用哈希类型,而是使用序列化的 字符串类型,使用 mget 获取。
  • 中间元素获取性能lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。

拆分的实现:

  • 假设某个用户发了 1w 个微博,list 长度就是 1w。
  • 就可以把这 1w 个微博拆成 10 份,每份就是 1k。
  • 如果是想获取到 5k 个左右的微博,只用读取 5 份~

Pipeline (流水线)

虽然咱们是多个 Redis 命令,但是把这些 命令合并成一个网络请求进行通信,大大降低客户端和服务端之间的交互次数了。

思考:

  • Quicklist:Quicklist 的外层是一个双向链表(linkedlist),每个节点是一个 (局部数据合并为)ziplist存储,是一种高效的列表内部编码方式。
  • Pipeline:是一种客户端技术,用于将多个命令合并成一个网络请求发送给服务器,从而减少网络往返时间,提高命令执行效率。
  • 区别:Quicklist 是一种数据结构优化,而 Pipeline 是一种网络通信优化。
实现栈和队列:
  • 同侧存取lpush + lpop 或者 rpush + rpop 为栈。
  • 异侧存取lpush + rpop 或者 rpush + lpop 为队列。
;