文章目录
-
- [Redis List 列表:双端队列与消息队列的完美实现](#Redis List 列表:双端队列与消息队列的完美实现)
- 一、前言
- [二、List 是什么](#二、List 是什么)
-
- [2.1 基本概念](#2.1 基本概念)
- [2.2 List 的三个核心特点](#2.2 List 的三个核心特点)
- [三、List 的基本命令](#三、List 的基本命令)
-
- [3.1 LPUSH 和 RPUSH ------ 插入元素](#3.1 LPUSH 和 RPUSH —— 插入元素)
- [3.2 LPUSHX 和 RPUSHX ------ 仅在 key 存在时插入](#3.2 LPUSHX 和 RPUSHX —— 仅在 key 存在时插入)
- [3.3 LRANGE ------ 获取指定范围的元素](#3.3 LRANGE —— 获取指定范围的元素)
- [3.4 LPOP 和 RPOP ------ 弹出元素](#3.4 LPOP 和 RPOP —— 弹出元素)
- [3.5 LINDEX ------ 获取指定索引的元素](#3.5 LINDEX —— 获取指定索引的元素)
- [3.6 LLEN ------ 获取列表长度](#3.6 LLEN —— 获取列表长度)
- [3.7 LINSERT ------ 在指定位置插入元素](#3.7 LINSERT —— 在指定位置插入元素)
- 四、阻塞版本命令
-
- [4.1 阻塞命令的核心思想](#4.1 阻塞命令的核心思想)
- [4.2 三种情况对比](#4.2 三种情况对比)
- [4.3 BLPOP 和 BRPOP](#4.3 BLPOP 和 BRPOP)
- [4.4 多客户端竞争](#4.4 多客户端竞争)
- 五、命令速查表
- 六、内部编码
-
- [6.1 早期版本:ziplist 和 linkedlist](#6.1 早期版本:ziplist 和 linkedlist)
- [6.2 Redis 3.2 之后:quicklist](#6.2 Redis 3.2 之后:quicklist)
- 七、典型使用场景
-
- [7.1 消息队列(Message Queue)](#7.1 消息队列(Message Queue))
- [7.2 分频道消息队列](#7.2 分频道消息队列)
- [7.3 微博 Timeline(时间线)](#7.3 微博 Timeline(时间线))
- [7.4 栈和队列](#7.4 栈和队列)
- 八、总结
-
- [8.1 使用注意事项](#8.1 使用注意事项)
Redis List 列表:双端队列与消息队列的完美实现
一、前言
💬 这一篇讲什么:Redis 五种数据类型中的第三种 ------ List 列表
🚀 核心内容:
- List 是什么?有什么特点?
- List 的全部命令,包括阻塞版本的妙用
- List 的内部编码演进历史
- List 在消息队列、时间线等场景中的实战应用
上一篇学完了 Hash 类型,这一篇进入第三种数据类型 ------ List。List 是一个非常灵活的数据结构,既可以当栈用,也可以当队列用,在消息队列、时间线、最新动态等场景下都有广泛应用。
二、List 是什么
2.1 基本概念
List 类型用来存储多个有序的字符串。可以把它想象成一个双端队列,可以从左边插入、从右边插入,也可以从左边弹出、从右边弹出。
text
列表:user:1:messages
left right
↓ ↓
[ a ] ← [ b ] ← [ c ] ← [ d ] ← [ e ]
索引: 0 1 2 3 4
倒数: -5 -4 -3 -2 -1
一个列表最多可以存储 2³² - 1 个元素(超过 40 亿)。
2.2 List 的三个核心特点
特点一:元素有序。 可以通过索引下标获取某个元素或某个范围的元素。索引从 0 开始,支持负数索引(-1 表示最后一个元素,-2 表示倒数第二个)。
特点二:元素可以重复。 同一个值可以在列表中出现多次,这和 Set 不同。
text
列表:user:1:messages
[ a ] ← [ b ] ← [ c ] ← [ e ] ← [ a ] ← [ d ]
↑
重复的 a 元素
特点三:支持双端操作。 可以从左边(头部)或右边(尾部)进行插入和弹出,这让 List 既可以当栈用(同侧插入弹出),也可以当队列用(异侧插入弹出)。
三、List 的基本命令
3.1 LPUSH 和 RPUSH ------ 插入元素
LPUSH 从左侧(头部)插入一个或多个元素;RPUSH 从右侧(尾部)插入。
语法:
bash
LPUSH key element [element ...]
RPUSH key element [element ...]
时间复杂度:插入一个元素 O(1),插入 N 个元素 O(N)。
返回值:插入后列表的长度。
示例:
bash
# LPUSH:从左侧插入(头插)
redis> LPUSH mylist "world"
(integer) 1
redis> LPUSH mylist "hello"
(integer) 2
redis> LRANGE mylist 0 -1
1) "hello" # 后插入的在前面
2) "world"
# RPUSH:从右侧插入(尾插)
redis> RPUSH mylist2 "world"
(integer) 1
redis> RPUSH mylist2 "hello"
(integer) 2
redis> LRANGE mylist2 0 -1
1) "world" # 先插入的在前面
2) "hello"
注意插入顺序的区别:LPUSH 是头插,后插入的元素会排在前面;RPUSH 是尾插,先插入的元素排在前面。
3.2 LPUSHX 和 RPUSHX ------ 仅在 key 存在时插入
LPUSHX 和 RPUSHX 只在 key 已经存在的情况下才插入元素,如果 key 不存在,不做任何操作。
语法:
bash
LPUSHX key element [element ...]
RPUSHX key element [element ...]
示例:
bash
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSHX mylist "Hello"
(integer) 2
redis> LPUSHX myotherlist "Hello"
(integer) 0 # myotherlist 不存在,未插入
redis> LRANGE mylist 0 -1
1) "Hello"
2) "World"
redis> LRANGE myotherlist 0 -1
(empty array)
这个命令在某些场景下很有用,比如只想往已有的列表追加数据,而不想创建新列表。
3.3 LRANGE ------ 获取指定范围的元素
获取列表中从 start 到 stop 索引范围内的所有元素,左闭右闭。支持负数索引。
语法:
bash
LRANGE key start stop
时间复杂度:O(N),N 为返回元素的数量。
示例:
bash
redis> RPUSH mylist "one" "two" "three"
(integer) 3
redis> LRANGE mylist 0 0
1) "one"
redis> LRANGE mylist -3 2
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist -100 100
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist 5 10
(empty array)
LRANGE key 0 -1 可以获取列表的全部元素,这是一个常用的操作。
3.4 LPOP 和 RPOP ------ 弹出元素
LPOP 从左侧(头部)弹出元素;RPOP 从右侧(尾部)弹出元素。弹出后元素会从列表中删除。
语法:
bash
LPOP key
RPOP key
时间复杂度:O(1)
返回值 :弹出的元素,列表为空时返回 nil。
示例:
bash
redis> RPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> LPOP mylist
"one"
redis> RPOP mylist
"five"
redis> LRANGE mylist 0 -1
1) "two"
2) "three"
3) "four"
3.5 LINDEX ------ 获取指定索引的元素
获取列表中指定索引位置的元素。支持负数索引。
语法:
bash
LINDEX key index
时间复杂度:O(N),N 为索引到头部或尾部的距离。
返回值 :对应位置的元素,索引越界返回 nil。
示例:
bash
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LINDEX mylist 0
"Hello"
redis> LINDEX mylist -1
"World"
redis> LINDEX mylist 3
(nil)
3.6 LLEN ------ 获取列表长度
获取列表中元素的个数。
语法:
bash
LLEN key
时间复杂度:O(1)
示例:
bash
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LLEN mylist
(integer) 2
3.7 LINSERT ------ 在指定位置插入元素
在列表中某个元素的前面或后面插入新元素。
语法:
bash
LINSERT key <BEFORE | AFTER> pivot element
时间复杂度:O(N),N 为列表长度。
返回值:插入后列表的长度;如果 pivot 不存在返回 -1。
示例:
bash
redis> RPUSH mylist "Hello"
(integer) 1
redis> RPUSH mylist "World"
(integer) 2
redis> LINSERT mylist BEFORE "World" "There"
(integer) 3
redis> LRANGE mylist 0 -1
1) "Hello"
2) "There"
3) "World"
四、阻塞版本命令
4.1 阻塞命令的核心思想
BLPOP 和 BRPOP 是 LPOP 和 RPOP 的阻塞版本。两者的核心区别在于:
非阻塞版本 :列表为空时立即返回 nil。
阻塞版本:列表为空时,客户端会阻塞等待,直到:
- 有其他客户端向列表中插入了元素(立即返回该元素)
- 超过了指定的超时时间(返回
nil)
这个特性让 Redis List 可以非常优雅地实现生产者-消费者模式的消息队列。
4.2 三种情况对比
情况一:列表不为空
bash
列表:user:1:messages = [x, y, z]
LPOP user:1:messages → 立即返回 "x"
BLPOP user:1:messages 5 → 立即返回 "x"
两者行为一致
情况二:列表为空,超时时间内没有新元素
bash
列表:user:1:messages = []
LPOP user:1:messages → 立即返回 nil
BLPOP user:1:messages 5 → 阻塞 5 秒后返回 nil
两者行为不同
情况三:列表为空,超时时间内有新元素插入
bash
列表:user:1:messages = []
客户端 A:BLPOP user:1:messages 5 (开始阻塞等待)
客户端 B:RPUSH user:1:messages "x" (3 秒后插入元素)
客户端 A:立即得到 "x",不再等待剩余的 2 秒
LPOP 做不到这一点,它只会立即返回 nil
4.3 BLPOP 和 BRPOP
语法:
bash
BLPOP key [key ...] timeout
BRPOP key [key ...] timeout
参数说明:
timeout:超时时间,单位秒。0 表示永久阻塞,直到有元素为止。- 可以同时监听多个 key,从左到右依次检查,哪个 key 先有元素就从哪个弹出。
时间复杂度:O(1)
返回值 :一个数组,包含 key 名称和弹出的元素;超时返回 nil。
示例:
bash
redis> DEL list1 list2
(integer) 0
redis> RPUSH list1 a b c
(integer) 3
redis> BLPOP list1 list2 0
1) "list1" # 从哪个 key 弹出的
2) "a" # 弹出的元素
# 列表为空时阻塞
redis> DEL list1
(integer) 1
redis> BLPOP list1 5
# 阻塞 5 秒...
(nil)
4.4 多客户端竞争
如果多个客户端同时对同一个 key 执行 BLPOP,只有最先执行命令的客户端会得到弹出的元素,其他客户端继续等待。
text
客户端 A:BLPOP mylist 0 (先执行,进入等待)
客户端 B:BLPOP mylist 0 (后执行,也进入等待)
客户端 C:RPUSH mylist "x"(插入元素)
结果:客户端 A 得到 "x",客户端 B 继续等待
这个特性保证了消息队列场景下的负载均衡:多个消费者同时监听一个队列,每条消息只会被一个消费者处理。
五、命令速查表
| 操作类型 | 命令 | 时间复杂度 |
|---|---|---|
| 添加 | LPUSH key element [...] |
O(k),k 为元素个数 |
RPUSH key element [...] |
O(k),k 为元素个数 | |
| `LINSERT key BEFORE | AFTER pivot element` | |
| 查找 | LRANGE key start end |
O(s+n),s 为 start 偏移量 |
LINDEX key index |
O(n),n 为索引偏移量 | |
LLEN key |
O(1) | |
| 删除 | LPOP key |
O(1) |
RPOP key |
O(1) | |
| 阻塞 | BLPOP key [key ...] timeout |
O(1) |
BRPOP key [key ...] timeout |
O(1) |
六、内部编码
6.1 早期版本:ziplist 和 linkedlist
Redis 早期版本中,List 的内部编码有两种:
ziplist(压缩列表):当同时满足以下条件时使用:
- 列表元素个数小于
list-max-ziplist-entries配置(默认 512) - 每个元素的长度都小于
list-max-ziplist-value配置(默认 64 字节)
ziplist 是一种紧凑的顺序存储结构,内存利用率高,但插入删除性能较差。
linkedlist(链表):当不满足 ziplist 条件时使用。linkedlist 是标准的双向链表,插入删除快,但内存占用大。
6.2 Redis 3.2 之后:quicklist
Redis 3.2 引入了 quicklist,它是 ziplist 和 linkedlist 的混合体:
text
quicklist = linkedlist of ziplists
[ziplist1] ↔ [ziplist2] ↔ [ziplist3] ↔ ...
quicklist 把列表分成多个小的 ziplist 节点,用链表串起来。这样既保留了 ziplist 的内存紧凑性,又避免了单个 ziplist 过大导致的性能问题。
现在查看 List 的内部编码,会看到:
bash
127.0.0.1:6379> lpush mylist a b c
(integer) 3
127.0.0.1:6379> object encoding mylist
"quicklist"
这个升级对用户完全透明,命令用法没有任何变化,但性能得到了显著提升。
七、典型使用场景
7.1 消息队列(Message Queue)
利用 LPUSH + BRPOP 实现经典的生产者-消费者模式。
text
生产者 Redis List 消费者
│ │ │
├─ LPUSH queue msg1 ──→ [msg1] │
├─ LPUSH queue msg2 ──→ [msg2, msg1] │
│ │ │
│ │ ←── BRPOP queue 0 ───────┤
│ │ 返回 msg1 │
│ [msg2] │
│ │ ←── BRPOP queue 0 ───────┤
│ │ 返回 msg2 │
│ [] │
│ │ ←── BRPOP queue 0 ───────┤
│ │ 阻塞等待... │
核心代码:
python
# 生产者
def produce_message(message):
redis.lpush("task:queue", message)
# 消费者(可以启动多个)
def consume_messages():
while True:
# 阻塞等待,0 表示永久阻塞
result = redis.brpop("task:queue", 0)
if result:
queue_name, message = result
process_message(message)
多个消费者同时运行时,每条消息只会被一个消费者处理,天然实现了负载均衡。
7.2 分频道消息队列
通过不同的 key 模拟不同的频道,不同的消费者订阅不同的频道。
text
生产者 ──→ LPUSH channel:news msg
──→ LPUSH channel:sports msg
──→ LPUSH channel:tech msg
消费者 A ──→ BRPOP channel:news 0
消费者 B ──→ BRPOP channel:sports channel:tech 0 (同时监听两个频道)
消费者 C ──→ BRPOP channel:tech 0
7.3 微博 Timeline(时间线)
每个用户都有自己的微博列表(Timeline),需要分页展示。
数据结构设计:
bash
# 每条微博用 Hash 存储
HSET mblog:1 title "标题" timestamp 1476536196 content "内容"
HSET mblog:2 title "标题" timestamp 1476536200 content "内容"
...
# 用户的 Timeline 用 List 存储微博 ID
LPUSH user:1001:timeline mblog:1 mblog:3 mblog:5
发布新微博:
python
def publish_blog(user_id, blog_content):
# 创建微博对象
blog_id = generate_blog_id()
redis.hset(f"mblog:{blog_id}", mapping={
"title": blog_content["title"],
"timestamp": time.time(),
"content": blog_content["content"]
})
# 插入用户 Timeline(最新的在最前面)
redis.lpush(f"user:{user_id}:timeline", f"mblog:{blog_id}")
分页获取 Timeline:
python
def get_timeline(user_id, page, page_size):
start = (page - 1) * page_size
end = start + page_size - 1
# 获取这一页的微博 ID 列表
blog_ids = redis.lrange(f"user:{user_id}:timeline", start, end)
# 批量获取微博详情(可以用 Pipeline 优化)
blogs = []
for blog_id in blog_ids:
blog = redis.hgetall(blog_id)
blogs.append(blog)
return blogs
潜在问题与优化:
问题一:1+N 查询。 如果一页显示 20 条微博,需要执行 1 次 LRANGE + 20 次 HGETALL,总共 21 次请求。
优化方案 :使用 Pipeline 批量提交命令,或者直接把微博内容序列化成 JSON 字符串存在 List 里,用 LRANGE 一次取出。
问题二:LRANGE 获取中间元素性能差。 List 是链表结构,获取中间位置的元素需要遍历,性能不如两端。
优化方案:如果 Timeline 特别长,可以考虑分段存储,或者只保留最近的 N 条记录。
7.4 栈和队列
List 的双端操作特性让它可以同时充当栈和队列:
栈(Stack):同侧插入弹出,后进先出(LIFO)
bash
# 用 LPUSH + LPOP 实现栈
LPUSH stack "a"
LPUSH stack "b"
LPUSH stack "c"
LPOP stack # 返回 "c"(后进先出)
队列(Queue):异侧插入弹出,先进先出(FIFO)
bash
# 用 LPUSH + RPOP 实现队列
LPUSH queue "a"
LPUSH queue "b"
LPUSH queue "c"
RPOP queue # 返回 "a"(先进先出)
八、总结
现在你已经掌握了:
✅ List 是什么:有序、可重复、支持双端操作的字符串列表
✅ 核心命令:LPUSH/RPUSH(插入)、LPOP/RPOP(弹出)、LRANGE(范围查询)、LINDEX(索引查询)、LLEN(长度)
✅ 阻塞命令:BLPOP/BRPOP,列表为空时阻塞等待,是实现消息队列的核心
✅ 内部编码演进:ziplist + linkedlist → quicklist(3.2 之后)
✅ 典型场景:消息队列、分频道队列、微博 Timeline、栈和队列
8.1 使用注意事项
| 注意事项 | 说明 |
|---|---|
| LRANGE 获取全部元素要小心 | 列表很长时会阻塞,考虑分页或用 SCAN |
| LINDEX 访问中间元素性能差 | List 是链表,访问中间位置需要遍历 |
| 阻塞命令 timeout=0 要谨慎 | 永久阻塞可能导致连接资源耗尽 |
| 多消费者竞争是公平的 | 先执行 BLPOP 的客户端先得到元素 |
| 同侧操作是栈,异侧操作是队列 | LPUSH+LPOP=栈,LPUSH+RPOP=队列 |
下一篇预告:Redis Set 集合与 Zset 有序集合 ------ 集合的交并差运算、标签系统实现、排行榜系统的完整方案,以及有序集合的分数排序机制。