【Redis篇】List 列表:双端队列与消息队列的完美实现

文章目录

    • [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 存在时插入

LPUSHXRPUSHX 只在 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 阻塞命令的核心思想

BLPOPBRPOPLPOPRPOP 的阻塞版本。两者的核心区别在于:

非阻塞版本 :列表为空时立即返回 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 有序集合 ------ 集合的交并差运算、标签系统实现、排行榜系统的完整方案,以及有序集合的分数排序机制。

相关推荐
Cloud_Shy6181 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第一章 Item 4 - 6)
android·数据库·论文阅读·python
土狗TuGou1 小时前
SQL内功笔记 · 第7篇:CTE&临时表&递归
数据库·笔记·后端·sql·mysql
XiYang-DING1 小时前
【Spring】日志
java·数据库·spring
我是唐青枫2 小时前
MySQL EXISTS 详解:存在性判断、NOT EXISTS 与实战示例
数据库·mysql
努力发光的程序员2 小时前
面试官与程序员谢飞机的3轮Java大厂面试问答实录:涵盖Spring Boot、微服务与数据库技术
java·jvm·spring boot·redis·面试·hibernate·microservices
我叫张小白。2 小时前
Redis的缓存雪崩、击穿、穿透和解决方案
数据结构·redis·fastapi·缓存穿透·缓存击穿·雪崩·热点key问题
weixin_468466852 小时前
Airtable 零基础快速上手与实战指南
数据库·人工智能·python·深度学习·ai·大模型
凯瑟琳.奥古斯特2 小时前
10道数据库原理精选题
开发语言·数据库·职场和发展·数据库开发
Rick19932 小时前
Redis 高频面试 10 题
数据库·redis·面试