Redis 入门必学:List 列表类型完全指南
文章目录
- 引言
- 一、List 的基本特性:有序、可重复、双端操作
- 1.1 什么是 List?
- 1.2 List 的三大特点
- 1.3 索引规则
- 二、基础操作命令:从增删到改查
- 2.1 从两端添加元素
- LPUSH:从左侧添加(头插法)
- RPUSH:从右侧添加(尾插法)
- LPUSHX / RPUSHX:仅在键存在时才添加
- 2.2 查看元素
- LRANGE:获取指定范围的元素
- LINDEX:获取指定索引的元素
- LLEN:获取列表长度
- 2.3 从两端删除元素
- LPOP:从左侧弹出(返回并删除头部元素)
- RPOP:从右侧弹出(返回并删除尾部元素)
- 2.4 删除指定元素:LREM
- 2.5 裁剪列表:LTRIM
- 2.6 插入与修改:LINSERT 与 LSET
- LINSERT:在指定位置前后插入元素
- LSET:修改指定索引的元素
- 三、命令小结:一表掌握所有 List 命令
- 四、阻塞版本命令:BLPOP 与 BRPOP
- 4.1 命令语法
- 4.2 三种场景演示
- 4.3 多键监听
- 4.4 多客户端竞争
- 五、内部编码:Redis 是如何存储 List 的?
- 5.1 编码的演进历程
- 5.2 编码切换演示
- 六、实战场景 1:实现栈和队列
- 6.1 一句话记忆法则
- 6.2 栈(Stack)
- 6.3 队列(Queue)
- 七、实战场景 2:阻塞式消息队列
- 7.1 生产者-消费者模型
- 7.2 多频道消息队列
- 八、实战场景 3:微博 Timeline
- 8.1 问题分析
- 8.2 实现步骤
- 8.3 潜在问题与优化
- 九、常见误区与避坑指南
- ❌ 误区 1:List 的所有操作都是 O(1)
- ❌ 误区 2:阻塞命令会阻塞整个 Redis 服务器
- ❌ 误区 3:LRANGE 获取大范围元素是安全的
- ❌ 误区 4:重复向空列表 LPUSHX / RPUSHX 会报错
- 总结与下一步行动
- 下一步行动建议
- 参考命令速查
引言
在前面的文章中,我们已经学习了 Redis 的 String(字符串)和 Hash(哈希)类型。这两个数据结构已经能应对不少场景,但遇到下面这类需求时,你会发现它们都不够顺手:
- 需要记录用户最新的 10 条浏览历史,还要支持分页查看
- 需要实现一个消息队列,支持多个消费者阻塞等待新任务
- 需要构建一个社交网络的 Timeline 流,按时间倒序展示内容
这些需求都有一个共同特征:数据天然有序,且需要频繁在首尾操作。这正是 Redis List(列表)类型的主场。
在本文中,我们将系统学习:
- List 的核心特性:有序、可重复、双端操作
- 13 个常用命令的完整用法(含 LREM、LTRIM、LSET)
- 阻塞版本命令(BLPOP / BRPOP)的原理与实践
- 内部编码:ziplist → linkedlist → quicklist 的演进
- 3 个实战应用场景:栈、消息队列、Timeline 流
💡 核心概念
Redis List 是一个有序的字符串集合,元素可以重复,支持从两端快速插入(push)和弹出(pop),还能按索引范围获取元素。一个列表最多可存储 2³² − 1 个元素。
一、List 的基本特性:有序、可重复、双端操作
1.1 什么是 List?
我们可以把 Redis List 想象成一支队伍排成一列:每个人(元素)有明确的前后顺序,同一个人可以出现在队伍的不同位置(允许重复),新人可以从队伍的最前面或最后面加入。
lpush → rpush → ┌─────┬─────┬─────┬─────┬─────┐ │ a │ b │ c │ d │ e │ └─────┴─────┴─────┴─────┴─────┘ lpop ← rpop ← 正索引: 0 1 2 3 4 负索引: -5 -4 -3 -2 -11.2 List 的三大特点
| 特点 | 说明 | 生活中的类比 |
|---|---|---|
| 有序 | 元素按插入顺序排列,可以通过索引下标访问任意元素 | 排队购票的队伍,每个人有固定位置 |
| 可重复 | 同一个值可以多次出现在 List 中 | 同名的人可以同时排队 |
| 双端操作 | 可以从左侧(头部)或右侧(尾部)快速增删 | 队伍两端都可以进人、出人 |
1.3 索引规则
Redis List 支持正索引和负索引两种访问方式:
- 正索引:从 0 开始,从左向右递增(0 = 第一个元素)
- 负索引:从 -1 开始,从右向左递减(-1 = 最后一个元素)
# 正索引访问第一个元素redis>LINDEX mylist0"a"# 负索引访问最后一个元素redis>LINDEX mylist-1"e"📌 区分获取和删除
这是 List 学习中新手最容易混淆的两个概念:
LINDEX:只是读取元素,列表长度不变LPOP/RPOP:删除并返回元素,列表长度 -1LREM:删除列表中匹配的元素,可能删除多个
二、基础操作命令:从增删到改查
2.1 从两端添加元素
LPUSH:从左侧添加(头插法)
LPUSH key element[element...]- 版本:1.0.0 起
- 时间复杂度:O(k),k 为插入元素个数
- 返回值:插入后的列表长度
示例:
redis>LPUSH mylist"world"(integer)1redis>LPUSH mylist"hello"(integer)2redis>LRANGE mylist0-1# 查看所有元素1)"hello"2)"world"📌 关键提示
一次插入多个元素时,它们是依次从左侧插入的,所以最终顺序和命令中写的顺序相反:redis>LPUSH mylist"a""b""c"(integer)3redis>LRANGE mylist0-11)"c"2)"b"3)"a"
RPUSH:从右侧添加(尾插法)
RPUSH key element[element...]- 版本:1.0.0 起
- 时间复杂度:O(k),k 为插入元素个数
- 返回值:插入后的列表长度
示例:
redis>RPUSH mylist"one"(integer)1redis>RPUSH mylist"two""three"(integer)3redis>LRANGE mylist0-11)"one"2)"two"3)"three"LPUSHX / RPUSHX:仅在键存在时才添加
LPUSHX key element[element...]RPUSHX key element[element...]- 版本:2.0.0 起
- 区别于 LPUSH/RPUSH:如果 key 不存在,命令什么也不做,返回 0
redis>LPUSHX emptylist"hello"(integer)0# key 不存在,插入失败redis>LPUSH mylist"world"(integer)1redis>LPUSHX mylist"hello"(integer)2# key 存在,插入成功redis>LRANGE mylist0-11)"hello"2)"world"2.2 查看元素
LRANGE:获取指定范围的元素
LRANGE key start stop- 版本:1.0.0 起
- 时间复杂度:O(s+n),s 为 start 偏移量,n 为范围长度
- 区间为左闭右闭,超出范围的索引会自动修正
redis>RPUSH mylist"one""two""three""four""five"(integer)5redis>LRANGE mylist02# 前 3 个1)"one"2)"two"3)"three"redis>LRANGE mylist-2-1# 后 2 个1)"four"2)"five"redis>LRANGE mylist0-1# 全部1)"one"2)"two"3)"three"4)"four"5)"five"LINDEX:获取指定索引的元素
LINDEX key index- 版本:1.0.0 起
- 时间复杂度:O(n),n 为索引偏移量(注意:不是 O(1)!)
redis>RPUSH mylist"hello""world"(integer)2redis>LINDEX mylist0"hello"redis>LINDEX mylist-1"world"redis>LINDEX mylist3# 索引越界(nil)LLEN:获取列表长度
LLEN key- 版本:1.0.0 起
- 时间复杂度:O(1)
redis>RPUSH mylist"a""b""c"(integer)3redis>LLEN mylist(integer)32.3 从两端删除元素
LPOP:从左侧弹出(返回并删除头部元素)
LPOP keyRPOP:从右侧弹出(返回并删除尾部元素)
RPOP keyredis>RPUSH mylist"one""two""three""four""five"(integer)5redis>LPOP mylist"one"redis>LPOP mylist"two"redis>RPOP mylist"five"redis>LRANGE mylist0-11)"three"2)"four"2.4 删除指定元素:LREM
LREM key count value- 版本:1.0.0 起
- 时间复杂度:O(k),k 为元素个数
count的含义:- count > 0:从左向右删除最多 count 个值为 value 的元素
- count < 0:从右向左删除最多 |count| 个值为 value 的元素
- count = 0:删除所有值为 value 的元素
redis>RPUSH mylist"a""b""a""c""a""d"(integer)6redis>LREM mylist2"a"# 从左删除 2 个 "a"(integer)2redis>LRANGE mylist0-11)"b"2)"c"3)"a"# 只剩 1 个 "a"4)"d"redis>LREM mylist-1"b"# 从右删除 1 个 "b"(integer)1redis>LREM mylist0"a"# 删除所有 "a"(integer)12.5 裁剪列表:LTRIM
LTRIM key start stop- 版本:1.0.0 起
- 时间复杂度:O(k),k 为元素个数
- 只保留 [start, stop] 范围内的元素,其余全部删除
redis>RPUSH mylist"a""b""c""d""e"(integer)5redis>LTRIM mylist02# 只保留前 3 个"OK"redis>LRANGE mylist0-11)"a"2)"b"3)"c"💡 实用技巧
可以用LTRIM来限制列表最大长度。例如保留最新 100 条记录:redis>LPUSH logs"new_entry"(integer)101redis>LTRIM logs099# 只保留前 100 条"OK"
2.6 插入与修改:LINSERT 与 LSET
LINSERT:在指定位置前后插入元素
LINSERT key BEFORE|AFTER pivot element- 版本:2.2.0 起
- 时间复杂度:O(n),n 是 pivot 到列表头尾的距离
redis>RPUSH mylist"Hello""World"(integer)2redis>LINSERT mylist BEFORE"World""There"(integer)3redis>LRANGE mylist0-11)"Hello"2)"There"3)"World"LSET:修改指定索引的元素
LSET key index element- 版本:1.0.0 起
- 时间复杂度:O(n),n 是索引偏移量(注意:不是 O(1)!)
redis>RPUSH mylist"one""two""three"(integer)3redis>LSET mylist0"first""OK"redis>LRANGE mylist0-11)"first"2)"two"3)"three"三、命令小结:一表掌握所有 List 命令
| 操作类型 | 命令 | 功能 | 时间复杂度 |
|---|---|---|---|
| 添加 | LPUSH key value [value ...] | 从左侧添加 | O(k),k 是元素个数 |
RPUSH key value [value ...] | 从右侧添加 | O(k) | |
LPUSHX key value [value ...] | 键存在时从左侧添加 | O(k) | |
RPUSHX key value [value ...] | 键存在时从右侧添加 | O(k) | |
LINSERT key BEFORE|AFTER pivot value | 在 pivot 前后插入 | O(n),n 是到首尾的距离 | |
| 查找 | LRANGE key start end | 获取范围元素 | O(s+n),s 是偏移量 |
LINDEX key index | 获取索引元素 | O(n) | |
LLEN key | 获取列表长度 | O(1) | |
| 删除 | LPOP key | 从左侧弹出 | O(1) |
RPOP key | 从右侧弹出 | O(1) | |
LREM key count value | 删除指定元素 | O(k) | |
LTRIM key start end | 保留范围,其余删除 | O(k) | |
| 修改 | LSET key index value | 修改索引处元素 | O(n) |
| 阻塞 | BLPOP key [key ...] timeout | 阻塞式左侧弹出 | O(1) |
BRPOP key [key ...] timeout | 阻塞式右侧弹出 | O(1) |
四、阻塞版本命令:BLPOP 与 BRPOP
这是 List 类型最强大的特性之一。BLPOP和BRPOP是LPOP和RPOP的阻塞版本,它们的核心区别在于:当列表为空时,不会立即返回 nil,而是等待直到超时或新元素到来。
4.1 命令语法
BLPOP key[key...]timeoutBRPOP key[key...]timeout- 版本:1.0.0 起
- 时间复杂度:O(1)
timeout = 0:无限等待,直到有元素timeout > 0:等待指定秒数后超时返回 nil
4.2 三种场景演示
场景 1:列表不为空 → 立即返回
此时阻塞版本和非阻塞版本表现一致,都立即返回元素:
redis>RPUSH list1"a""b""c"(integer)3redis>BLPOP list101)"list1"# 返回的键名2)"a"# 弹出的元素场景 2:列表为空且超时前无新元素 → 等待超时
redis>BLPOP empty_list5(nil)(5.00s)# 等了 5 秒后返回 nil场景 3:列表为空但等待期间有新元素 → 立即返回
# 客户端 1:阻塞等待redis>BLPOP task:queue0(阻塞中...)# 客户端 2:放入新元素redis>LPUSH task:queue"new_task"(integer)1# 客户端 1:立即获得元素1)"task:queue"2)"new_task"4.3 多键监听
BLPOP和BRPOP可以同时监听多个键。Redis 会从左到右依次检查每个键,一旦有任一列表存在元素,立即弹出并返回:
# 同时监听 3 个队列redis>BLPOP queue1 queue2 queue30如果queue2和queue3都有元素,会返回queue2的(因为它在参数列表中更靠左)。
4.4 多客户端竞争
如果多个客户端同时对同一个键执行BLPOP,第一个执行命令的客户端会获得弹出的元素。这与单线程模型一致——Redis 保证原子性。
五、内部编码:Redis 是如何存储 List 的?
5.1 编码的演进历程
Redis List 的内部编码经历了三个阶段的发展:
| 编码 | 适用条件 | 优势 | 劣势 | 引入版本 |
|---|---|---|---|---|
| ziplist(压缩列表) | 元素数 < 512 且元素 < 64 字节 | 内存极度紧凑 | 元素多时性能下降 | 早期 |
| linkedlist(双向链表) | 不满足 ziplist 条件时 | 灵活,插入删除快 | 内存占用大 | 早期 |
| quicklist(快速列表) | 默认使用(取代前两者) | 结合两者优势 | — | 3.2+ |
⚠️ 重要更新(Redis 3.2+)
Redis 3.2 引入quicklist作为 List 的新内部编码,它结合了ziplist的紧凑性和linkedlist的灵活性。在此版本之后,ziplist和linkedlist实际上已被quicklist取代——但对外 API 和命令行为完全不变,用户无需关心底层实现。
⚠️ 配置参数(仅对旧版有效)
list-max-ziplist-entries:ziplist 最大元素数(默认 512)list-max-ziplist-value:ziplist 单元素最大字节数(默认 64)当哈希类型满足 ziplist 条件时,Redis 也会使用 ziplist 编码,这在设计上实现了"一处优化、多处受益"。
5.2 编码切换演示
# 场景 1:少量小元素 → 使用 ziplist(或新版 quicklist)redis>RPUSH listkey e1 e2 e3"OK"redis>OBJECT ENCODING listkey"quicklist"# 3.2+ 版本# 场景 2:存在超过 64 字节的元素 → 转换编码redis>RPUSH listkey"this is a very long string that exceeds 64 bytes limit...""OK"redis>OBJECT ENCODING listkey"quicklist"# 场景 3:元素个数超过 512 → 转换编码redis>RPUSH listkey e1 e2 e3... e513# 513 个元素"OK"redis>OBJECT ENCODING listkey"quicklist"🔧 最佳实践
quicklist的出现使得编码切换的代价大大降低。但对于内存敏感的场景,保持较小的元素大小和合理的元素数量仍然是好习惯。
六、实战场景 1:实现栈和队列
6.1 一句话记忆法则
List 的插入和弹出方向组合,能轻松实现两种经典数据结构:
同侧存取 = 栈(LIFO 后进先出) 异侧存取 = 队列(FIFO 先进先出)6.2 栈(Stack)
# 入栈:LPUSH(从左侧插入)redis>LPUSH stack"A"(integer)1redis>LPUSH stack"B"(integer)2redis>LPUSH stack"C"(integer)3# 出栈:LPOP(从左侧弹出)redis>LPOP stack# 后进先出"C"redis>LPOP stack"B"redis>LPOP stack"A"6.3 队列(Queue)
# 入队:RPUSH(从右侧插入)redis>RPUSH queue"A"(integer)1redis>RPUSH queue"B"(integer)2redis>RPUSH queue"C"(integer)3# 出队:LPOP(从左侧弹出)redis>LPOP queue# 先进先出"A"redis>LPOP queue"B"redis>LPOP queue"C"七、实战场景 2:阻塞式消息队列
7.1 生产者-消费者模型
利用LPUSH+BRPOP的组合,可以轻松实现一个经典的阻塞式生产者-消费者消息队列:
生产者(LPUSH) Redis List 消费者(BRPOP) │ │ ├── task1 ──→ ┌─────┐ ──→ BRPOP ──→ 消费者A "抢到" task1 │ │queue│ │ ├── task2 ──→ │ │ ──→ BRPOP ──→ 消费者B "抢到" task2 │ └─────┘ │ └── task3 ──→ ──→ BRPOP ──→ 消费者C "抢到" task3伪代码实现(Python):
importredis r=redis.Redis(host='localhost',port=6379,db=0)defproduce_task(task_content):"""生产者:向队列添加任务"""r.lpush('task:queue',task_content)print(f"[生产者] 任务已添加:{task_content}")defconsume_task():"""消费者:阻塞等待并处理任务"""whileTrue:# 阻塞等待,timeout=0 表示无限等待result=r.brpop('task:queue',timeout=0)ifresult:_,task=result# result 是 (key, value) 元组print(f"[消费者] 开始处理:{task}")# 实际处理逻辑...7.2 多频道消息队列
通过使用不同的 key 作为"频道",BRPOP可以同时监听多个队列,实现简单的频道订阅效果:
# 消费者同时监听 sports 和 tech 频道redis>BRPOP channel:sports channel:tech0🔧 最佳实践
Redis 的 List 消息队列适合轻量级场景。如果你的系统需要消息持久化、消息确认(ACK)、消息回溯等高级功能,建议考虑 RabbitMQ、Kafka 等专业的消息中间件。
八、实战场景 3:微博 Timeline
8.1 问题分析
每个用户有自己的微博 Timeline,需要按时间倒序(最新的在前面)分页展示。Timeline 有两个关键需求:
- 有序:按发布时间排列,最新的在前面
- 分页:支持按范围获取,每次只加载一部分
Redis List 天然满足这两个需求。
8.2 实现步骤
第一步:每条微博用 Hash 结构存储
redis>HMSET mblog:1 title"Redis 学习笔记"timestamp1717300000content"今天学了 Redis List...""OK"redis>HMSET mblog:2 title"周末计划"timestamp1717301000content"去爬山...""OK"第二步:向用户 Timeline 添加微博(左插 → 最新在前)
redis>LPUSH user:1:mblogs mblog:2 mblog:1(integer)2第三步:分页获取微博列表
# 获取第 1 页(前 10 条),即最新的 10 条redis>LRANGE user:1:mblogs091)"mblog:2"2)"mblog:1"# 获取第 2 页redis>LRANGE user:1:mblogs1019第四步:根据列表中的 ID,去 Hash 中查询每条微博详情
defget_timeline(uid,page=1,page_size=10):"""获取用户 Timeline"""key=f"user:{uid}:mblogs"start=(page-1)*page_size end=start+page_size-1# 第一步:获取微博 ID 列表blog_ids=r.lrange(key,start,end)# 第二步:批量获取每条微博的详情blogs=[]forblog_idinblog_ids:blog=r.hgetall(blog_id)blogs.append(blog)returnblogs8.3 潜在问题与优化
⚠️ 1+N 查询问题:
如果一页返回 10 条微博,需要 1 次LRANGE+ 10 次HGETALL= 11 次网络往返。
✅ 优化方案:
- 使用Pipeline(流水线)模式,将 10 次
HGETALL打包成一次网络请求 - 或将微博数据改为JSON 字符串直接存在 List 中,用一次
LRANGE搞定,但缺点是单独更新某条微博比较麻烦
⚠️ 获取中间元素性能差:
LRANGE在获取列表两端的元素时表现较好,但获取中间范围的元素会变慢。如果用户频繁翻到很靠后的页数,性能会下降。
✅ 优化方案:
- 将大列表拆分为多个小列表(如按天或按周拆分)
- 对于需要复杂排序或随机访问的场景,考虑使用 Sorted Set(有序集合)
九、常见误区与避坑指南
❌ 误区 1:List 的所有操作都是 O(1)
事实:只有两端的增删(LPUSH / RPUSH / LPOP / RPOP)和 LLEN 是 O(1)。LINDEX、LINSERT、LSET都是 O(n),在大列表上慎用。
# ❌ 避免:在大列表中频繁使用 LINDEX 获取中间元素redis>LINDEX big_list50000# 要遍历前面 50000 个元素# ✅ 推荐:尽量在两端操作,或将中间元素的需求改用 Sorted Set❌ 误区 2:阻塞命令会阻塞整个 Redis 服务器
事实:BLPOP/BRPOP只会阻塞执行该命令的那个客户端,Redis 服务器本身可以继续处理其他客户端的请求。这就是单线程 I/O 多路复用的巧妙之处。
❌ 误区 3:LRANGE 获取大范围元素是安全的
事实:LRANGE 0 -1在一个有数万甚至数十万元素的列表上执行,会返回海量数据,可能阻塞 Redis 数秒。
正确做法:
- 控制单列表的元素数量(建议 < 1 万)
- 使用
LTRIM定期清理旧数据,保持列表不会无限增长 - 分页获取数据,不要一次性拉取全部
❌ 误区 4:重复向空列表 LPUSHX / RPUSHX 会报错
事实:LPUSHX/RPUSHX在 key 不存在时不会报错,只是静默返回 0。这是设计特性,不是 bug。
总结与下一步行动
本文我们系统学习了 Redis List(列表)类型,从基本命令到阻塞机制,从内部编码到实战应用:
- ✅List 的三大特性:有序、可重复、双端操作
- ✅13 个常用命令:从基础增删(LPUSH/RPUSH/LPOP/RPOP)到高级操作(LREM/LTRIM/LSET/LINSERT)
- ✅阻塞版本命令:BLPOP/BRPOP 实现生产者-消费者模型
- ✅内部编码演进:ziplist → linkedlist → quicklist(3.2+)
- ✅三个实战场景:栈/队列、消息队列、微博 Timeline
下一步行动建议
🔨动手练习:
- 用
LPUSH+LPOP实现一个浏览历史栈,用LTRIM限制最多保留 20 条 - 用
RPUSH+BLPOP实现一个简单的任务队列 - 用
OBJECT ENCODING观察编码,尝试让值超过 64 字节触发切换
- 用
📖扩展阅读:
- 下一篇文章我们将学习Set(集合)类型——无序去重的数据结构
- 了解
RPOPLPUSH命令:实现可靠队列 + 备份
🧪挑战任务:
设计一个简单的微博/朋友圈 Timeline 系统,要求支持:- 发布内容(用 Hash 存储单条内容,ID 存入 List)
- 分页查看 Timeline(最新在前)
- 仅保留最近 500 条(用 LTRIM 裁剪)
💡 下一站预告
List 是有序可重复的,但如果你需要"去重"呢?比如标签系统、好友共同关注——这些场景需要的是Set(集合)。我们在下一篇文章中见!
参考命令速查
# 添加元素LPUSH key value[value...]# 从左侧添加RPUSH key value[value...]# 从右侧添加LPUSHX key value[value...]# 键存在时从左侧添加RPUSHX key value[value...]# 键存在时从右侧添加LINSERT key BEFORE|AFTER pivot value# 在 pivot 前后插入# 查看元素LRANGE key start end# 获取范围,左闭右闭LINDEX key index# 获取索引处元素,支持负索引LLEN key# 获取列表长度# 删除元素LPOP key# 从左侧弹出RPOP key# 从右侧弹出LREM key count value# 删除匹配元素,count>0左删/ <0右删/ =0全删LTRIM key start end# 只保留范围内,其余删除# 修改元素LSET key index value# 修改索引处元素# 阻塞操作BLPOP key[key...]timeout# 阻塞式左侧弹出,0=无限等待BRPOP key[key...]timeout# 阻塞式右侧弹出