ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Redis 初阶】List 类型深度解析:双端队列的设计智慧与实战应用

【Redis 初阶】List 类型深度解析:双端队列的设计智慧与实战应用 草莓熊Lotso个人主页❄️个人专栏:《C知识分享》 《Linux 入门到实践零基础也能懂》✨生活是默默的坚持毅力是永久的享受 博主简介文章目录前言一. List 类型基本特性1.1 核心定义1.2 三个关键特性1.3 栈与队列的天然适配二. List 核心命令全解2.1 两端插入lpush /rpush2.2 条件插入lpushx /rpushx2.3 范围查询lrange2.4 两端弹出lpop /rpop2.5 索引操作lindex /lset/llen2.6 指定位置插入linsert2.7 按值删除lrem2.8 区间裁剪ltrim2.9 阻塞弹出blpop /brpop2.10 命令小结三. List 底层编码实现3.1 早期方案ziplist linkedlist3.2 演进方案quicklist3.3 配置与粒度控制四. List 典型应用场景4.1 一对多关联关系存储4.2 简单阻塞消息队列4.3 分频道消息队列4.4 社交产品 Timeline4.5 选型口诀结尾前言前面我们依次拆解了 String 和 Hash 两种核心类型今天我们来看 Redis 里最灵活的数据结构 ——List。很多人对 List 的印象停留在 “可以当数组用”但它既能做栈、又能做队列还能实现阻塞消息队列底层更是从 ziplist 链表演进到了 quicklist藏着非常多时空权衡的设计智慧。本文顺着基础特性→核心命令→底层编码→业务场景的完整脉络逐个拆解 List 的常用命令与踩坑点深入 quicklist 的实现原理再结合四个经典业务场景讲透实战用法带你从 “会用命令” 到理解设计本质。一. List 类型基本特性1.1 核心定义List 是 Redis 中的有序字符串列表底层类似双端队列元素按插入顺序排列允许重复值。一个 List 最多可以存储 2^32 个元素支持从两端插入、弹出元素也支持按索引、按范围读取。它的 “有序” 需要特别说明这里的有序指的是元素的位置顺序是确定的元素的先后位置有意义颠倒之后列表就不等价了不是指按数值大小排序的有序这点要结合上下文区分不要和 zset 的排序有序混淆。1.2 三个关键特性位置有序支持正负下标最左侧元素下标为 0依次向右递增同时支持负下标-1 代表倒数第一个元素-2 代表倒数第二个以此类推使用起来非常灵活。读写操作语义分离获取元素和删除元素是完全独立的操作lindex只读取元素不改变列表长度lpop会弹出并删除元素。这一点和很多语言的队列设计一致但使用时要注意区分避免误删数据。元素允许重复和 Hash、Set 的去重特性不同List 中的元素可以重复出现这也是它适合做队列、时间线的原因之一。1.3 栈与队列的天然适配因为 List 两端增删都是 O (1) 复杂度所以它可以很方便地模拟两种经典数据结构栈同侧存取比如lpush lpop先进后出队列异侧存取比如lpush rpop先进先出。二. List 核心命令全解2.1 两端插入lpush /rpush这两个是最基础的插入命令分别从列表左侧头部和右侧尾部插入元素支持一次插入多个时间复杂度为 O (k)k 为插入元素个数。# 左侧头插依次插入1、2、3最终顺序是 3 2 1127.0.0.1:6379lpush key123(integer)3# 右侧尾插依次插入4、5最终顺序是 3 2 1 4 5127.0.0.1:6379rpush key45(integer)5返回值是插入完成后列表的总长度。批量插入可以有效减少网络 IO 次数是常用的优化手段。2.2 条件插入lpushx /rpushx和 push 功能一致但有一个前提只有 key 存在时才执行插入如果 key 不存在直接返回 0不会自动创建。# key不存在插入失败127.0.0.1:6379lpushx key2123(integer)0这类命令主要用于业务上的幂等控制确保只往已有的列表里追加数据。2.3 范围查询lrangelrange用来查询列表中指定区间的元素是最常用的查询命令。LRANGE key start stop区间是左闭右闭包含 start 和 stop 两个位置的元素支持负下标lrange key 0 -1可以查询列表全部元素下标越界不会报错会自动裁剪到合法范围尽可能返回能取到的元素。127.0.0.1:6379lrange key0-11)32)23)14)45)5# 下标超出也不会崩溃只返回有效内容127.0.0.1:6379lrange key01001)32)23)14)45)5这里做个横向对比C 中下标越界是未定义行为可能崩溃、可能返回脏数据完全看运气Java 中下标越界会直接抛出异常能及时发现问题Redis 选择了最 “鲁棒” 的方式尽可能返回有效数据。 三种设计没有绝对的好坏只是取舍不同C 追求极致性能Java 追求快速失败Redis 追求服务可用性。2.4 两端弹出lpop /rpop弹出命令会移除并返回列表一端的元素时间复杂度 O (1)。# 左侧弹出127.0.0.1:6379lpop key3# 右侧弹出127.0.0.1:6379rpop key5注意一个版本差异Redis 5 中 pop 命令不支持 count 参数一次只能弹一个从 Redis 6.2 开始新增了 count 参数可以一次弹出多个元素。版本不同写法要注意适配。列表为空时pop 命令会立即返回 nil不会等待。2.5 索引操作lindex /lset/llenlindex按索引获取元素LINDEX key index根据下标读取元素时间复杂度是O(N)N 是索引距离两端的长度。 很多人会误以为它和数组下标一样是 O (1)这是非常常见的误区。List 底层不是纯数组按位置访问需要遍历大列表中频繁使用 lindex 会严重影响性能。lset按索引修改元素LSET key index value修改指定下标位置的值时间复杂度同样是 O (N)只有修改首尾元素时是 O (1)。 和 lindex 不同的是lset 下标越界会直接报错不会做兼容处理使用时要特别注意。llen获取列表长度LLEN key返回列表的元素总数时间复杂度 O (1)。原理和 Hash 的 hlen 一样底层有专门的变量记录元素个数直接读取即可不需要遍历。2.6 指定位置插入linsertLINSERT key BEFORE|AFTER pivot value在基准值 pivot 的前面或后面插入新元素从左往右找到第一个匹配的基准值就停止。# 在元素1前面插入100127.0.0.1:6379linsert key before1100(integer)4时间复杂度 O (N)因为需要遍历找到基准值的位置。如果基准值存在多个只会操作第一个匹配的位置。2.7 按值删除lremLREM key count element删除列表中值为 element 的元素count 参数控制删除方向和数量count 0从左往右删除 count 个匹配元素count 0从右往左删除 count 个匹配元素count 0删除列表中所有匹配元素。# 从左往右删除2个值为1的元素127.0.0.1:6379lrem key21(integer)2返回值是实际删除的元素个数时间复杂度 O (NM)N 是列表长度M 是删除的元素数。2.8 区间裁剪ltrimLTRIM key start stop只保留 [start, stop] 区间内的元素区间外的元素全部删除常用于维护固定长度的列表比如只保留最新的 100 条记录。# 只保留下标2到5的元素127.0.0.1:6379ltrim key25OK时间复杂度 O (N)N 是被删除的元素数量。2.9 阻塞弹出blpop /brpop这是 List 类型非常有特色的一组命令是 pop 的阻塞版本也是实现消息队列的核心。核心特性列表非空时和普通 pop 行为完全一致立即返回元素列表为空时客户端会阻塞等待直到有新元素插入或者超时timeout 参数设置最长等待时间单位为秒设为 0 表示永久等待。# 阻塞等待key中的元素最多等10秒127.0.0.1:6379brpop key10两个重要规则支持监听多个 key可以同时监听多个列表哪个列表先有元素就立即返回哪个列表的结果。适合多优先级队列的场景。# 同时监听key1、key2、key3哪个先有数据先返回哪个blpop key1 key2 key30多客户端公平竞争如果多个客户端同时对同一个 key 执行阻塞弹出新元素到来时最先执行阻塞命令的客户端会优先拿到元素按先后顺序轮询分配天然实现了消费者的负载均衡。关键注意点阻塞的是客户端不是 Redis 服务端。Redis 主线程依然可以处理其他命令不会因为某个客户端阻塞而卡住。这一点非常重要也是它能安全用于生产环境的前提。2.10 命令小结操作类型命令时间复杂度两端插入lpush / rpushO (k)k 为插入元素数条件插入lpushx / rpushxO(k)指定位置插入linsert before/afterO(N)范围查询lrangeO (sn)s 为偏移量n 为返回长度索引查询lindexO(N)获取长度llenO(1)两端弹出lpop / rpopO(1)按值删除lremO(NM)区间裁剪ltrimO(N)索引修改lsetO (N)首尾为 O (1)阻塞弹出blpop / brpopO(1)三. List 底层编码实现List 的底层编码经历了一次重要的演进从早期的双编码切换变成了现在的 quicklist 统一方案。3.1 早期方案ziplist linkedlist早期 Redis 版本和 Hash 类似根据数据量自动切换两种编码ziplist压缩列表当元素个数少、每个元素长度短时使用。连续内存紧凑存储空间利用率极高但插入删除需要移动内存数据量大了之后性能下降明显。linkedlist双向链表当数据量超过阈值后切换为双向链表。插入删除 O (1)但每个节点都要存前后指针内存开销大且内存碎片化严重CPU 缓存命中率低。两种编码各有优劣一个省空间、一个省时间但都走了极端。3.2 演进方案quicklist从 Redis 3.2 开始List 的默认底层编码变成了quicklist相当于 “双向链表 压缩列表” 的结合体宏观上是一个双向链表每个链表节点称为一个 quicklistNode每个节点内部又是一个 ziplist存储真正的元素数据。简单说就是把大链表拆成很多小段每一小段用紧凑的 ziplist 存储既保留了链表两端插入高效的优点又大幅降低了指针带来的内存开销同时兼顾了空间和时间。 这个设计思路和 C 里的std::deque非常像 —— 分段连续存储在数组和链表之间取折中是非常经典的工程权衡。3.3 配置与粒度控制quicklist 每个节点的 ziplist 大小通过list-max-ziplist-size配置控制负值代表按字节数限制比如 -2 代表每个 ziplist 最大 8KB正值代表按元素个数限制。默认值是 -28KB属于综合表现比较均衡的选择。实际业务中可以根据场景调整节点越小越接近普通链表插入越快、内存开销越大节点越大越接近纯 ziplist空间越省、插入越慢。还是那句老话记思想不记数字。理解可调、知道怎么调比背默认值重要得多。我们可以通过OBJECT encoding命令验证实际编码127.0.0.1:6379rpush key1234(integer)4127.0.0.1:6379OBJECT encoding keyquicklist源码视角quicklist 与阻塞机制的设计智慧站在 C/C 系统编程的角度看List 的两个设计非常有代表性值得细细品味。quicklist分段思想的经典应用纯数组随机访问快但插入慢纯链表插入快但访问慢、空间浪费。quicklist 的思路很朴素不要走极端把大问题拆成小问题。每个 ziplist 控制在几 KB即使做内存拷贝开销也可控节点之间用链表连接两端增删不需要移动数据。它没有追求理论上的最优而是追求工程上的 “够用且划算”。实际开发中很多问题都是这样极端的最优解往往代价高昂合适的折中方案反而综合收益最高。阻塞弹出的实现原理很多人会疑惑Redis 是单线程的blpop 阻塞了会不会卡住整个服务 答案是不会。阻塞的是客户端连接不是服务端主线程。 它的实现基于 Redis 的事件循环客户端执行 blpop 后如果列表为空就把这个客户端挂起注册一个事件主线程继续处理其他客户端的命令不受影响当有其他客户端往对应列表 push 元素时Redis 会按顺序唤醒最早阻塞的客户端把元素返回给它。整个过程没有轮询、不浪费 CPU也不会阻塞主线程是非常高效的事件驱动实现。Linux 下的阻塞队列、IO 多路复用本质都是这个思路没事就等着有事再唤醒。四. List 典型应用场景4.1 一对多关联关系存储比如班级和学生的关系我们可以用class:students:1这样的 key把班级下的所有学生 ID 存入 List直接通过班级 ID 查询学生列表。classStudents:1 - [1, 2, 3] classStudents:2 - [4, 5]这种方式查询效率很高适合读多写少的关联场景。缺点是只能按 key 查询做不了反向查询和条件过滤复杂统计还是要靠数据库。4.2 简单阻塞消息队列这是 List 最经典的应用之一用lpush brpop就能实现一个简易版的生产者消费者模型。生产者用 lpush 往列表尾部塞消息消费者用 brpop 阻塞等待消息有消息就处理没消息就等着不浪费 CPU。多个消费者同时消费同一个队列时消息会按阻塞顺序分配给不同消费者天然实现负载均衡。 优点是实现简单、延迟低缺点是功能有限不支持消息确认、不支持持久化保证、不支持广播适合简单的异步解耦场景。4.3 分频道消息队列通过不同的 key 模拟不同的频道不同业务的消息放进不同的列表消费者各自监听自己的频道。 比如短视频业务可以分成视频数据、弹幕、点赞、评论四个频道互不影响。 这样做的好处是解耦合某一个频道出问题不会影响其他频道也方便针对不同频道做独立的扩容和运维。4.4 社交产品 Timeline微博、朋友圈的信息流时间线是 List 非常典型的应用场景。 实现思路每篇微博用 Hash 存储详细内容key 为mblog:123每个用户的时间线用一个 List 存储里面只放微博 ID按发布时间倒序排列分页浏览时用lrange按范围取出一页微博 ID再批量查询对应的微博内容。两个常见优化点1n 问题优化如果查完 ID 列表后循环逐个查详情会产生大量网络请求。可以用 pipeline 管道批量提交命令或者直接把微博内容序列化后存字符串用 mget 批量获取大幅降低 IO 次数。大列表分页优化lrange 查列表两端很快但查中间位置需要遍历性能会下降。如果用户的时间线特别长可以拆分成多个 List比如按月份拆分避免单列表过大。4.5 选型口诀最后给一个简单的判断规则同侧存取lpushlpop /rpushrpop 栈先进后出异侧存取lpushrpop /rpushlpop 队列先进先出。核心考点总结最后梳理一下 List 类型的核心考点覆盖面试和工作高频问题类型特性元素有序位置有序、可重复、双端增删 O (1)支持正负下标。命令细节lrange 为闭区间、下标越界兼容处理lindex、lset 时间复杂度为 O (N)大列表慎用lset 下标越界直接报错。阻塞弹出blpop/brpop 的阻塞对象是客户端服务端不阻塞支持多 key 监听、多消费者公平竞争。底层编码早期 ziplist linkedlist 切换现在默认 quicklistquicklist 的分段设计思想空间与时间的折中。应用场景消息队列、时间线、关联关系存储以及各场景的优缺点与优化方案。设计思想分段折中、事件驱动阻塞、时空权衡的工程取舍。 我是草莓熊 Lotso若这篇技术干货帮你打通了学习中的卡点 【关注】跟我一起深耕技术领域从基础到进阶见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好需要时直接查、随时用 【评论】分享你的经验或疑问比如曾踩过的技术坑一起交流避坑 ️ 【投票】用你的选择助力社区内容方向告诉大家哪个技术点最该重点拆解 技术之路难免有困惑但同行的人会让前进更有方向愿我们都能在自己专注的领域里一步步靠近心中的技术目标结语✨把这些内容吃透超牛的放松下吧✨ʕ˘ᴥ˘ʔづきらど结尾 我是草莓熊 Lotso若这篇技术干货帮你打通了学习中的卡点 【关注】跟我一起深耕技术领域从基础到进阶见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好需要时直接查、随时用 【评论】分享你的经验或疑问比如曾踩过的技术坑一起交流避坑 ️ 【投票】用你的选择助力社区内容方向告诉大家哪个技术点最该重点拆解 技术之路难免有困惑但同行的人会让前进更有方向愿我们都能在自己专注的领域里一步步靠近心中的技术目标结语List 是 Redis 里最 “百变” 的数据结构既能当数组、当栈、当队列又能实现阻塞消息队列。底层从双编码演进到 quicklist处处体现着工程上的折中智慧。理解这些设计你才能在业务里选对、用好。下一篇我们会继续拆解 Set 类型看看去重集合的底层实现与典型业务场景✨把这些内容吃透超牛的放松下吧✨ʕ˘ᴥ˘ʔづきらど。
返回列表