
文章目录引言一、List 的基本特性有序、可重复、双端操作1.1 什么是 List1.2 List 的三大特点1.3 索引规则二、基础操作命令从增删到改查2.1 从两端添加元素LPUSH从左侧添加头插法RPUSH从右侧添加尾插法LPUSHX / RPUSHX仅在键存在时才添加2.2 查看元素LRANGE获取指定范围的元素LINDEX获取指定索引的元素LLEN获取列表长度2.3 从两端删除元素LPOP从左侧弹出返回并删除头部元素RPOP从右侧弹出返回并删除尾部元素2.4 删除指定元素LREM2.5 裁剪列表LTRIM2.6 插入与修改LINSERT 与 LSETLINSERT在指定位置前后插入元素LSET修改指定索引的元素三、命令小结一表掌握所有 List 命令四、阻塞版本命令BLPOP 与 BRPOP4.1 命令语法4.2 三种场景演示4.3 多键监听4.4 多客户端竞争五、内部编码Redis 是如何存储 List 的5.1 编码的演进历程5.2 编码切换演示六、实战场景 1实现栈和队列6.1 一句话记忆法则6.2 栈Stack6.3 队列Queue七、实战场景 2阻塞式消息队列7.1 生产者-消费者模型7.2 多频道消息队列八、实战场景 3微博 Timeline8.1 问题分析8.2 实现步骤8.3 潜在问题与优化九、常见误区与避坑指南❌ 误区 1List 的所有操作都是 O(1)❌ 误区 2阻塞命令会阻塞整个 Redis 服务器❌ 误区 3LRANGE 获取大范围元素是安全的❌ 误区 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 最后一个元素# 正索引访问第一个元素redisLINDEX mylist0a# 负索引访问最后一个元素redisLINDEX mylist-1e 区分获取和删除这是 List 学习中新手最容易混淆的两个概念LINDEX只是读取元素列表长度不变LPOP/RPOP删除并返回元素列表长度 -1LREM删除列表中匹配的元素可能删除多个二、基础操作命令从增删到改查2.1 从两端添加元素LPUSH从左侧添加头插法LPUSH key element[element...]版本1.0.0 起时间复杂度O(k)k 为插入元素个数返回值插入后的列表长度示例redisLPUSH mylistworld(integer)1redisLPUSH mylisthello(integer)2redisLRANGE mylist0-1# 查看所有元素1)hello2)world 关键提示一次插入多个元素时它们是依次从左侧插入的所以最终顺序和命令中写的顺序相反redisLPUSH mylistabc(integer)3redisLRANGE mylist0-11)c2)b3)aRPUSH从右侧添加尾插法RPUSH key element[element...]版本1.0.0 起时间复杂度O(k)k 为插入元素个数返回值插入后的列表长度示例redisRPUSH mylistone(integer)1redisRPUSH mylisttwothree(integer)3redisLRANGE mylist0-11)one2)two3)threeLPUSHX / RPUSHX仅在键存在时才添加LPUSHX key element[element...]RPUSHX key element[element...]版本2.0.0 起区别于 LPUSH/RPUSH如果 key 不存在命令什么也不做返回 0redisLPUSHX emptylisthello(integer)0# key 不存在插入失败redisLPUSH mylistworld(integer)1redisLPUSHX mylisthello(integer)2# key 存在插入成功redisLRANGE mylist0-11)hello2)world2.2 查看元素LRANGE获取指定范围的元素LRANGE key start stop版本1.0.0 起时间复杂度O(sn)s 为 start 偏移量n 为范围长度区间为左闭右闭超出范围的索引会自动修正redisRPUSH mylistonetwothreefourfive(integer)5redisLRANGE mylist02# 前 3 个1)one2)two3)threeredisLRANGE mylist-2-1# 后 2 个1)four2)fiveredisLRANGE mylist0-1# 全部1)one2)two3)three4)four5)fiveLINDEX获取指定索引的元素LINDEX key index版本1.0.0 起时间复杂度O(n)n 为索引偏移量注意不是 O(1)redisRPUSH mylisthelloworld(integer)2redisLINDEX mylist0helloredisLINDEX mylist-1worldredisLINDEX mylist3# 索引越界(nil)LLEN获取列表长度LLEN key版本1.0.0 起时间复杂度O(1)redisRPUSH mylistabc(integer)3redisLLEN mylist(integer)32.3 从两端删除元素LPOP从左侧弹出返回并删除头部元素LPOP keyRPOP从右侧弹出返回并删除尾部元素RPOP keyredisRPUSH mylistonetwothreefourfive(integer)5redisLPOP mylistoneredisLPOP mylisttworedisRPOP mylistfiveredisLRANGE mylist0-11)three2)four2.4 删除指定元素LREMLREM key count value版本1.0.0 起时间复杂度O(k)k 为元素个数count的含义count 0从左向右删除最多 count 个值为 value 的元素count 0从右向左删除最多 |count| 个值为 value 的元素count 0删除所有值为 value 的元素redisRPUSH mylistabacad(integer)6redisLREM mylist2a# 从左删除 2 个 a(integer)2redisLRANGE mylist0-11)b2)c3)a# 只剩 1 个 a4)dredisLREM mylist-1b# 从右删除 1 个 b(integer)1redisLREM mylist0a# 删除所有 a(integer)12.5 裁剪列表LTRIMLTRIM key start stop版本1.0.0 起时间复杂度O(k)k 为元素个数只保留 [start, stop] 范围内的元素其余全部删除redisRPUSH mylistabcde(integer)5redisLTRIM mylist02# 只保留前 3 个OKredisLRANGE mylist0-11)a2)b3)c 实用技巧可以用LTRIM来限制列表最大长度。例如保留最新 100 条记录redisLPUSH logsnew_entry(integer)101redisLTRIM logs099# 只保留前 100 条OK2.6 插入与修改LINSERT 与 LSETLINSERT在指定位置前后插入元素LINSERT key BEFORE|AFTER pivot element版本2.2.0 起时间复杂度O(n)n 是 pivot 到列表头尾的距离redisRPUSH mylistHelloWorld(integer)2redisLINSERT mylist BEFOREWorldThere(integer)3redisLRANGE mylist0-11)Hello2)There3)WorldLSET修改指定索引的元素LSET key index element版本1.0.0 起时间复杂度O(n)n 是索引偏移量注意不是 O(1)redisRPUSH mylistonetwothree(integer)3redisLSET mylist0firstOKredisLRANGE mylist0-11)first2)two3)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(sn)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等待指定秒数后超时返回 nil4.2 三种场景演示场景 1列表不为空 → 立即返回此时阻塞版本和非阻塞版本表现一致都立即返回元素redisRPUSH list1abc(integer)3redisBLPOP list101)list1# 返回的键名2)a# 弹出的元素场景 2列表为空且超时前无新元素 → 等待超时redisBLPOP empty_list5(nil)(5.00s)# 等了 5 秒后返回 nil场景 3列表为空但等待期间有新元素 → 立即返回# 客户端 1阻塞等待redisBLPOP task:queue0阻塞中...# 客户端 2放入新元素redisLPUSH task:queuenew_task(integer)1# 客户端 1立即获得元素1)task:queue2)new_task4.3 多键监听BLPOP和BRPOP可以同时监听多个键。Redis 会从左到右依次检查每个键一旦有任一列表存在元素立即弹出并返回# 同时监听 3 个队列redisBLPOP 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.2Redis 3.2 引入quicklist作为 List 的新内部编码它结合了ziplist的紧凑性和linkedlist的灵活性。在此版本之后ziplist和linkedlist实际上已被quicklist取代——但对外 API 和命令行为完全不变用户无需关心底层实现。⚠️ 配置参数仅对旧版有效list-max-ziplist-entriesziplist 最大元素数默认 512list-max-ziplist-valueziplist 单元素最大字节数默认 64当哈希类型满足 ziplist 条件时Redis 也会使用 ziplist 编码这在设计上实现了一处优化、多处受益。5.2 编码切换演示# 场景 1少量小元素 → 使用 ziplist或新版 quicklistredisRPUSH listkey e1 e2 e3OKredisOBJECT ENCODING listkeyquicklist# 3.2 版本# 场景 2存在超过 64 字节的元素 → 转换编码redisRPUSH listkeythis is a very long string that exceeds 64 bytes limit...OKredisOBJECT ENCODING listkeyquicklist# 场景 3元素个数超过 512 → 转换编码redisRPUSH listkey e1 e2 e3... e513# 513 个元素OKredisOBJECT ENCODING listkeyquicklist 最佳实践quicklist的出现使得编码切换的代价大大降低。但对于内存敏感的场景保持较小的元素大小和合理的元素数量仍然是好习惯。六、实战场景 1实现栈和队列6.1 一句话记忆法则List 的插入和弹出方向组合能轻松实现两种经典数据结构同侧存取 栈LIFO 后进先出 异侧存取 队列FIFO 先进先出6.2 栈Stack# 入栈LPUSH从左侧插入redisLPUSH stackA(integer)1redisLPUSH stackB(integer)2redisLPUSH stackC(integer)3# 出栈LPOP从左侧弹出redisLPOP stack# 后进先出CredisLPOP stackBredisLPOP stackA6.3 队列Queue# 入队RPUSH从右侧插入redisRPUSH queueA(integer)1redisRPUSH queueB(integer)2redisRPUSH queueC(integer)3# 出队LPOP从左侧弹出redisLPOP queue# 先进先出AredisLPOP queueBredisLPOP queueC七、实战场景 2阻塞式消息队列7.1 生产者-消费者模型利用LPUSHBRPOP的组合可以轻松实现一个经典的阻塞式生产者-消费者消息队列生产者LPUSH Redis List 消费者BRPOP │ │ ├── task1 ──→ ┌─────┐ ──→ BRPOP ──→ 消费者A 抢到 task1 │ │queue│ │ ├── task2 ──→ │ │ ──→ BRPOP ──→ 消费者B 抢到 task2 │ └─────┘ │ └── task3 ──→ ──→ BRPOP ──→ 消费者C 抢到 task3伪代码实现Pythonimportredis rredis.Redis(hostlocalhost,port6379,db0)defproduce_task(task_content):生产者向队列添加任务r.lpush(task:queue,task_content)print(f[生产者] 任务已添加:{task_content})defconsume_task():消费者阻塞等待并处理任务whileTrue:# 阻塞等待timeout0 表示无限等待resultr.brpop(task:queue,timeout0)ifresult:_,taskresult# result 是 (key, value) 元组print(f[消费者] 开始处理:{task})# 实际处理逻辑...7.2 多频道消息队列通过使用不同的 key 作为频道BRPOP可以同时监听多个队列实现简单的频道订阅效果# 消费者同时监听 sports 和 tech 频道redisBRPOP channel:sports channel:tech0 最佳实践Redis 的 List 消息队列适合轻量级场景。如果你的系统需要消息持久化、消息确认ACK、消息回溯等高级功能建议考虑 RabbitMQ、Kafka 等专业的消息中间件。八、实战场景 3微博 Timeline8.1 问题分析每个用户有自己的微博 Timeline需要按时间倒序最新的在前面分页展示。Timeline 有两个关键需求有序按发布时间排列最新的在前面分页支持按范围获取每次只加载一部分Redis List 天然满足这两个需求。8.2 实现步骤第一步每条微博用 Hash 结构存储redisHMSET mblog:1 titleRedis 学习笔记timestamp1717300000content今天学了 Redis List...OKredisHMSET mblog:2 title周末计划timestamp1717301000content去爬山...OK第二步向用户 Timeline 添加微博左插 → 最新在前redisLPUSH user:1:mblogs mblog:2 mblog:1(integer)2第三步分页获取微博列表# 获取第 1 页前 10 条即最新的 10 条redisLRANGE user:1:mblogs091)mblog:22)mblog:1# 获取第 2 页redisLRANGE user:1:mblogs1019第四步根据列表中的 ID去 Hash 中查询每条微博详情defget_timeline(uid,page1,page_size10):获取用户 Timelinekeyfuser:{uid}:mblogsstart(page-1)*page_size endstartpage_size-1# 第一步获取微博 ID 列表blog_idsr.lrange(key,start,end)# 第二步批量获取每条微博的详情blogs[]forblog_idinblog_ids:blogr.hgetall(blog_id)blogs.append(blog)returnblogs8.3 潜在问题与优化⚠️ 1N 查询问题如果一页返回 10 条微博需要 1 次LRANGE 10 次HGETALL 11 次网络往返。✅ 优化方案使用Pipeline流水线模式将 10 次HGETALL打包成一次网络请求或将微博数据改为JSON 字符串直接存在 List 中用一次LRANGE搞定但缺点是单独更新某条微博比较麻烦⚠️ 获取中间元素性能差LRANGE在获取列表两端的元素时表现较好但获取中间范围的元素会变慢。如果用户频繁翻到很靠后的页数性能会下降。✅ 优化方案将大列表拆分为多个小列表如按天或按周拆分对于需要复杂排序或随机访问的场景考虑使用 Sorted Set有序集合九、常见误区与避坑指南❌ 误区 1List 的所有操作都是 O(1)事实只有两端的增删LPUSH / RPUSH / LPOP / RPOP和 LLEN 是 O(1)。LINDEX、LINSERT、LSET都是 O(n)在大列表上慎用。# ❌ 避免在大列表中频繁使用 LINDEX 获取中间元素redisLINDEX big_list50000# 要遍历前面 50000 个元素# ✅ 推荐尽量在两端操作或将中间元素的需求改用 Sorted Set❌ 误区 2阻塞命令会阻塞整个 Redis 服务器事实BLPOP/BRPOP只会阻塞执行该命令的那个客户端Redis 服务器本身可以继续处理其他客户端的请求。这就是单线程 I/O 多路复用的巧妙之处。❌ 误区 3LRANGE 获取大范围元素是安全的事实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 → quicklist3.2✅三个实战场景栈/队列、消息队列、微博 Timeline下一步行动建议动手练习用LPUSHLPOP实现一个浏览历史栈用LTRIM限制最多保留 20 条用RPUSHBLPOP实现一个简单的任务队列用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# 删除匹配元素count0左删/ 0右删/ 0全删LTRIM key start end# 只保留范围内其余删除# 修改元素LSET key index value# 修改索引处元素# 阻塞操作BLPOP key[key...]timeout# 阻塞式左侧弹出0无限等待BRPOP key[key...]timeout# 阻塞式右侧弹出