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

目录

[List 列表](#List 列表)

特点

[2. 命令](#2. 命令)

头插和尾插

[下标 range 查询](#下标 range 查询)

头删和尾删

LINSERT

LLEN

LREM

LTRIM

LSET

阻塞命令

BLPOP

BRPOP

[操作 总结](#操作 总结)

[3. 内部编码](#3. 内部编码)

ziplist(压缩列表)

linkedlist(链表)

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

4.使用场景

[1. 消息队列](#1. 消息队列)

[2. 分频道的消息队列](#2. 分频道的消息队列)

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

[1. 阻塞命令机制](#1. 阻塞命令机制)

[2. 客户端处理逻辑](#2. 客户端处理逻辑)

[3. 微博 Timeline](#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 版本之后引入的更高效的列表编码方式,结合了 ziplist 和 linkedlist 的优点。
  • 结构
    • 外层列表仍然是 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 为队列。
相关推荐
爬树的小蚂蚁7 分钟前
k8s 1.28 二进制安装与部署
云原生·容器·kubernetes
用户67570498850213 分钟前
hyperf-throttle-requests,一个超牛的 PHP 限流神器
后端·php
洪大宇30 分钟前
开发一套ERP 第八弹 RUst 插入数据
linux
考虑考虑34 分钟前
JDK8加载拓展包
java·后端·java ee
树懒_Zz39 分钟前
TiDB 架构
架构·tidb
想和我重名?43 分钟前
Linux下的wlan0控制
linux·嵌入式
LeonNo111 小时前
Linux:systemd进程管理【1】
linux·运维·服务器
Holen&&Beer1 小时前
【Ubuntu】安装 Anaconda
linux·运维·ubuntu
小狗爱吃黄桃罐头1 小时前
Ubuntu-20.04安装 terminator
linux·运维·ubuntu
帅得不敢出门1 小时前
vim插件管理器vim-plug替代vim-bundle
linux·编辑器·vim