目录
List 列表
- 列表类型用于 存储多个有序的字符串,如
a
、b
、c
、d
、e
五个元素从左到右组成一个有序列表,每个字符串称为元素,最多可存储 (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
有一个命名上的注意点:
lpush
:left push
,从左侧插入,即头插rpush
:right push
,从右侧插入,即尾插lrange
:list range
,输出列表指定范围
同样是l
开头,有时候表示list
,有时候表示left
,这个要注意。
另外的:
lpushx
:left 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
- 说明:删除
count
个element
元素。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 的列表类型还提供了一些具有 阻塞性质 的命令
在多线程中,有一个生产消费模型,其可以基于阻塞队列实现,主要满足以下两个性质:
- 如果阻塞队列满了,那么生产者阻塞
- 如果阻塞队列空了,那么消费者阻塞
在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(微博列表),需要分页展示文章列表。
- 实现:
- 每篇微博使⽤哈希结构存储,例如微博中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
为队列。