ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Redis数据结构之list

Redis数据结构之list

演进:3.2 之前小 list 用 ziplist、大 list 用 linkedlist;3.2 起统一为 quicklist(双向链表 + 每节点挂一块 ziplist);7.0 起节点内的 ziplist 换成 listpack。

3.2 以后OBJECT ENCODING对 list 始终返回quicklist。


一、List 数据结构

1.1 基本特点

  1. 有序:元素按插入顺序排列,支持下标访问(下标从 0 开始,支持负数:-1 表示倒数第一个);
  2. 可重复:允许相同元素;
  3. 两端高效:左/右两端的插入、弹出都是 O(1),中间操作需要遍历,是 O(N);
  4. 单列表最多 2³²−1 个元素。

1.2 quicklist 真实结构

quicklist(整个列表) ┌────────────────────────────────────────────────┐ │ head ──► node0 ◄──► node1 ◄──► node2 ◄── tail │ │ count = 元素总数 │ └────────────────────────────────────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │listpack │ │listpack │ │listpack │ │ a b c d │ │ e f g h │ │ i j k l │ ← 每个节点管理一片连续紧凑内存 └─────────┘ └─────────┘ └─────────┘

关键结构体(源码):

typedef struct quicklist { quicklistNode *head; // 头节点 quicklistNode *tail; // 尾节点 unsigned long count; // 所有节点元素总数 ... } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; // 前驱 struct quicklistNode *next; // 后继 unsigned char *entry; // 指向 listpack(旧版为 ziplist) size_t sz; // listpack 的字节大小 unsigned int count; // 本节点元素数 ... } quicklistNode;

真正负责管理一片连续空间的是quicklistNode 的 prev、next、entry(外加 sz、count 字段),而"数组"其实是每个节点里的listpack。


二、常用命令

2.1 添加操作

1)右端插入

RPUSH key value [value ...]

2)左端插入

LPUSH key value [value ...]

返回插入后列表长度。

多个元素按顺序依次插入,注意LPUSH key a b c后列表顺序是c b a(每个元素都从左边插入),而RPUSH key a b c 后列表顺序是a b c

127.0.0.1:6379> rpush list1 a b c (integer) 3 127.0.0.1:6379> lrange list1 0 -1 1) "a" 2) "b" 3) "c" 127.0.0.1:6379> flushall OK 127.0.0.1:6379> rpush key a b c 3 127.0.0.1:6379> lrange key 0 -1 1)"a" 2) "b" 3)"c"

3)在指定元素前/后插入

LINSERT key BEFORE|AFTER pivot value
  • 从左向右找第一个pivot,在其前/后插入 value;
  • 返回值:成功返回插入后的列表长度;pivot 不存在返回 -1;key 不存在返回 0;
  • 时间复杂度 O(N),需要遍历。

2.2 查找操作

1)获取指定范围(闭区间 [start, end])

LRANGE key start end
  • 超出范围时自动取有效交集,不会报错;
  • 支持负数下标,LRANGE key 0 -1取全部;
  • 复杂度 O(S+N),S 为偏移量、N 为返回数量。

2)获取指定下标元素

LINDEX key index

⚠️下标越界时返回(nil)。复杂度 O(N)(要沿节点链表 + listpack 顺序查找)。

3)获取长度

LLEN key

O(1),quicklist 头部维护 count。

2.3 删除操作

1)左端弹出

LPOP key

2)右端弹出

RPOP key

空列表返回 nil(6.2 起支持LPOP key count一次弹出多个)。

3)删除指定元素

LREM key count value
count行为
count > 0从左向右,最多删除 count 个与 value 相等的元素
count < 0从右向左,最多删除 |count| 个
count = 0删除全部与 value 相等的元素

返回实际删除的元素个数,O(N)。

4)按范围修剪(保留区间,删除其余)

LTRIM key start end

保留闭区间 [start, end] 内的元素,其余全部删除。常配合 LPUSH 实现定长列表,O(N)。

2.4 修改操作

LSET key index newValue

修改指定下标元素,这个命令 index 越界会报错(ERR index out of range),注意与 LINDEX 越界返回 nil 区分。O(N)。

2.5 阻塞式弹出 BLPOP / BRPOP

BLPOP key [key ...] timeout BRPOP key [key ...] timeout

行为规则:

  1. 按参数中 key 的从左到右顺序检查,对第一个非空列表执行弹出,立即返回[key, 弹出的值]两个元素;
  2. 所有 key 都为空时:
    • timeout > 0:阻塞等待 timeout 秒(单位秒),超时返回 nil;
    • timeout = 0:一直阻塞,直到有数据;
  3. 多个客户端同时阻塞在同一个 key 上时,数据到达按先到先得唤醒。
# 客户端 A 127.0.0.1:6379> blpop task 0 # 此时阻塞…… # 客户端 B 127.0.0.1:6379> lpush task job1 # 客户端 A 立即被唤醒并返回: 1) "task" 2) "job1"

阻塞到底发生在哪里?(重点)

  • Redis 服务器不会阻塞!执行 BLPOP 时,服务器发现列表为空,就把这个客户端挂到该 key 的阻塞等待链表上,然后继续处理其他客户端的命令;
  • 当其他客户端对该 key 执行 LPUSH/RPUSH 时,服务器检查阻塞链表,主动唤醒等待者并把数据推给它;
  • 阻塞只体现在客户端(连接上没有响应返回,客户端等待)。

三、quicklist 的内部编码细节

3.1 编码结论

  • 3.2 以后所有 list 统一使用 quicklist
  • 7.0 起 quicklist 节点的容器由 ziplist 变为 listpack,消除连锁更新风险;
  • OBJECT ENCODING key返回quicklist。

3.2 两个关键配置

# 每个 quicklist 节点的大小限制 list-max-listpack-size -2 # 旧版叫 list-max-ziplist-size # 两端各保留多少个节点不压缩 list-compress-depth 0

list-max-listpack-size 取值:

取值含义
正数 n单个节点最多存 n 个元素
-1单个节点最大 4KB
-2(默认)最大 8KB
-3 / -4 / -516KB / 32KB / 64KB

list-compress-depth(LZF 压缩):

  • 0:不压缩(默认);
  • 1:两端各留 1 个节点不压缩,中间节点用 LZF 压缩;
  • 2:两端各留 2 个不压缩,以此类推。

两端节点不压缩是因为两端操作最频繁,中间节点访问少,压缩以牺牲少量 CPU 换取内存节省。

3.3 时间复杂度汇总

命令复杂度
LPUSH / RPUSH / LPOP / RPOP / BLPOP / BRPOPO(1)
LLENO(1)
LINSERT / LREM / LTRIM / LINDEX / LSETO(N)
LRANGEO(S+N)

四、使用场景

4.1 四种经典组合

组合效果
LPUSH + LPOPStack(栈):同端进出,后进先出 LIFO
LPUSH + RPOPQueue(队列):异端进出,先进先出 FIFO
LPUSH + LTRIMCapped Collection(定长集合):只保留最新 N 条
LPUSH + BRPOPMessage Queue(阻塞消息队列)

定长列表示例(只保留最新 10 条动态):

LPUSH feeds user:1:newpost LTRIM feeds 0 9

4.2 阻塞消息队列

生产者LPUSH task job,消费者BRPOP task 0阻塞等待,天然实现任务分发。

局限(需要知道边界):

  1. 消息一旦 POP 就从 Redis 删除,没有 ACK 确认机制,消费者处理到一半宕机,消息就丢了;
  2. 没有消费组、广播、消息回溯等能力;
  3. 复杂可靠的消息流应使用 Redis 5.0 的Stream(或专业 MQ)。

4.3 与 Hash 配合:文章列表

Hash 存文章正文(每个对象一个 hash,多对象的同一类属性):

hset article:1 title xxx timestamp xxx content xxx hset article:k title yyy timestamp yyy content yyy

List 存文章 ID 顺序:

lpush user:1:articles article:1 article:2

查询时两级查找:先从 list 按时间顺序取出文章 ID,再用 ID 去 hash 里取正文:

lrange user:1:articles 0 9 # 先取最新 10 篇的 ID hgetall article:1 # 再取正文
返回列表