【Redis 初阶】数据结构与单线程模型深度解析:对外接口与底层编码的双重智慧

📅 2026/8/22 13:27:06
【Redis 初阶】数据结构与单线程模型深度解析:对外接口与底层编码的双重智慧
草莓熊Lotso个人主页❄️个人专栏:《C知识分享》 《Linux 入门到实践零基础也能懂》✨生活是默默的坚持毅力是永久的享受 博主简介文章目录前言一. Redis 数据结构对外接口与底层编码的分离设计1.1 先理清两个核心概念1.2 五大基础类型的底层编码实现1.3 查看底层编码object encoding 命令1.4 学习理念记思想不记数字二. 深入 Redis 单线程模型2.1 单线程模型的工作过程2.2 经典面试题单线程为什么这么快三. 源码视角设计背后的工程考量3.1 quicklist空间与时间的经典折中3.2 ae 事件循环单线程的驱动核心结尾前言上一篇我们梳理了 Redis 的通用命令与过期机制打下了基础操作的底子。而 Redis 之所以能碾压同类内存数据库核心竞争力有两个一是丰富且高效的数据结构二是经典的单线程事件驱动模型。很多人学 Redis 停留在 “会用 set/get、知道有五种数据类型” 的层面但对底层编码优化、单线程为什么快这些核心原理一知半解一到面试深挖就卡壳。本文我们从「对外数据类型」与「底层编码实现」的分离设计讲起逐个拆解五大基础类型的底层实现思路再深入 Redis 最经典的单线程模型从工作过程到性能本质结合系统编程视角一次性讲透。一. Redis 数据结构对外接口与底层编码的分离设计1.1 先理清两个核心概念我们常说 Redis 有 string、list、hash、set、zset 五种数据结构这其实是对外暴露的数据类型也就是 Redis 向使用者承诺的 API 与时间复杂度。比如 hash 类型承诺你查询、插入、删除都是 O (1)list 承诺两端操作 O (1)。但在源码底层同一种数据类型可能对应多种编码实现方式。Redis 会根据数据量大小、数据类型自动选择最合适的底层编码在时间和空间之间做权衡 —— 既保证对外的时间复杂度承诺不变又尽可能节省内存。打个通俗的比方餐厅承诺你 10 分钟上一份炒面这是对外的服务标准。至于后厨是老师傅现炒、还是预制菜加热只要最终口味和时效达标都可以。Redis 的编码优化就是这个逻辑对外接口语义不变内部实现按需优化。1.2 五大基础类型的底层编码实现我们逐个来看每种数据类型对应的底层编码以及各自的适用场景。string 字符串string 是最基础的类型底层有三种编码方式int 编码当 value 是整数时Redis 不会存成字符串而是直接用整型存储既省空间又方便计算。比如计数器场景天然适配 int 编码。embstr 编码针对短字符串的优化编码将 RedisObject 和字符串数据在内存上连续分配只需一次内存分配缓存友好性更好空间效率更高。raw 编码最通用的字符串编码底层就是一个字节数组对应 C/C 的 char 数组、Java 的 byte 数组长字符串会使用这种方式。补充一个细节C/C 里的 char 占 1 字节等价于 Java 的 byte而 Java 的 char 占 2 字节用来存 Unicode 字符两者不要搞混。Redis 是 C 语言写的字符串本质就是字节流不关心内容编码。hash 哈希hash 类型对应两种底层编码ziplist压缩列表当 hash 中元素数量少、字段和值都比较短时使用压缩列表存储。它是一块连续的内存通过紧凑排列节省空间虽然查询是遍历但元素少的时候性能差异可以忽略整体视为 O (1)。hashtable哈希表当元素数量超过阈值后自动转成标准哈希表实现保证查询、插入的 O (1) 复杂度。核心设计思想小数据量时优先省内存大数据量时优先保性能。Redis 里有大量的 key如果每个小 hash 都用完整哈希表内存碎片和开销会非常大统一用压缩列表紧凑存储整体内存收益非常可观。list 列表list 类型的编码经历了一次演进早期版本元素少用 ziplist元素多用 linkedlist双向链表。Redis 3.2 之后引入quicklist作为默认实现相当于 “链表 压缩列表” 的结合体 —— 整个 list 是一个双向链表链表的每个节点又是一个 ziplist。这种设计同时兼顾了两端操作的效率和空间利用率和 C 里的std::deque设计思路非常像分段连续存储既避免链表的内存碎片化又保证两端插入删除的高效性。set 集合set 类型有两种编码intset整数集合当集合里全是整数、且元素数量少时使用整数集合紧凑存储非常省内存。hashtable哈希表当元素不满足整数条件、或数量超过阈值时转成哈希表实现保证去重和查询的 O (1) 复杂度。zset 有序集合zset 是 Redis 非常有特色的数据结构同样有两种编码ziplist压缩列表元素数量少时用压缩列表存储按 score 顺序排列遍历即可实现有序性。skiplist跳表元素数量多时使用跳表作为核心实现。跳表在普通链表的基础上给每个节点增加了多层指针通过 “跳层” 的方式将查询时间复杂度降到 O (logN)实现简单、并发友好是 Redis 非常经典的底层设计。1.3 查看底层编码object encoding 命令我们可以通过OBJECT encoding key命令查看某个 key 实际使用的底层编码127.0.0.1:6379setnum111OK127.0.0.1:6379typenum string127.0.0.1:6379OBJECT encoding numint127.0.0.1:6379hset user name zhangsan age20(integer)2127.0.0.1:6379OBJECT encoding userziplist可以看到同样是 string 类型底层可能是 int 编码同样是 hash 类型小数据量下默认是 ziplist。整个切换过程 Redis 自动完成使用者完全无感知。1.4 学习理念记思想不记数字很多同学学这里特别喜欢背阈值比如字符串小于 39 字节用 embstrhash 元素超过 512 个转 hashtable。这里我特别强调一句只记思想不记数字。原因很简单这些阈值都是可配置的不同版本 Redis 默认值也不一样死记硬背毫无意义面试真正考察的是你 “空间换时间、时间换空间” 的权衡思路而不是具体数字实际工作中最优阈值永远要靠压测得出而不是背来的。就像 HashMap 的红黑树转换阈值、线程池的核心线程数这些参数本质都是 “可调的工程选择”不是 “必须遵守的定理”。理解背后的设计思想比记住数字重要一万倍。二. 深入 Redis 单线程模型单线程模型是 Redis 最经典的设计也是面试必考题。很多人对它有误解我们从工作过程到性能本质一步步拆解。2.1 单线程模型的工作过程首先澄清一个误区Redis 单线程指的是「核心命令的执行是单线程」不是说整个 Redis 进程只有一个线程。高版本 Redis 里网络 IO、持久化等工作是由额外的后台线程做的真正执行数据操作、处理命令逻辑的只有一个主线程。它的工作模式很简单 所有客户端发来的命令请求都会先进入一个队列排队主线程按顺序逐个取出命令、执行、返回结果。宏观上看是多个客户端并发访问微观上所有命令都是串行执行的。这带来一个最直接的好处天然线程安全。 比如两个客户端同时对同一个 key 执行incr自增操作在多线程模型下会出现并发安全问题自增两次实际只加 1但在 Redis 单线程模型下两个命令一定会排队先后执行最终结果一定是自增两次完全不需要加锁也不会有竞态问题。这个逻辑就像学校食堂只有一个打饭窗口放学时大家一窝蜂跑过来宏观上是并发的但到了窗口前都得排队阿姨一个一个打饭微观上是串行的。不会出现两个人同时抢一份菜的情况。当然单线程能成立是有前提的Redis 的核心操作都是内存操作短平快不消耗 CPU。如果是 CPU 密集型的业务单线程肯定扛不住必须上多线程。而 Redis 的瓶颈从来不在 CPU而在内存和网络 IO。单线程的弊端也非常明显一旦有一个慢命令占用主线程太久所有其他命令都会被阻塞。这也是为什么keys *、大 key 删除、复杂聚合计算在生产环境是高危操作 —— 它们会卡住整个 Redis 服务。2.2 经典面试题单线程为什么这么快这道题面试出场率接近 100%很多人只能答出 “内存存储、单线程无锁”答得完整且有深度的人很少。我们从四个层面完整拆解1. 内存存储是根本前提Redis 所有数据都在内存里操作的是内存数据结构而 MySQL 这类关系型数据库操作的是磁盘。内存访问是纳秒级磁盘寻道是毫秒级差了好几个数量级。这是最本质的差距也是一切高性能的基础。2. 功能简洁执行逻辑轻量数据库要做的事情太多了SQL 解析、查询优化、事务处理、约束校验、索引维护…… 每一步都有额外开销。而 Redis 的核心逻辑非常纯粹根据 key 找到 value执行简单的数据结构操作没有复杂的语义和约束干的活少自然就快。3. 单线程避免了线程竞争开销多线程不是银弹它带来收益的同时也会引入上下文切换、锁竞争、缓存失效等额外开销。 Redis 的操作都是短平快的内存操作本身不耗 CPU就算开多线程也很难利用好多核反而会因为锁和切换降低效率。单线程模型彻底规避了这些问题实现简单且高效。补充CPU 密集型任务适合多线程可以充分利用多核IO 密集型 轻计算任务单线程 IO 多路复用往往是更优的选择。Redis 就是典型的后者。4. IO 多路复用单线程搞定海量连接这是 Redis 能支撑高并发的核心技术。很多人会疑惑单线程怎么同时处理成百上千个客户端连接答案就是epoll 实现的 IO 多路复用机制。我们用一个通俗的例子理解 假设你要去买三份小吃蛋炒饭、肉夹馍、饺子。方案一你挨个排队买等完第一家再等第二家最后等第三家。这就是同步阻塞 IO串行处理效率最低。方案二找三个人各买各的同时等。这就是每连接一线程模型效率高但线程多了系统开销大连接上万之后根本扛不住。方案三你一个人去买挨个下单后就在旁边等着哪家做好了老板就喊你一声你去取哪家的。这就是IO 多路复用一个线程管理多个 socket哪个连接有数据就绪了就去处理哪个。Redis 面对的场景正好符合第三种方案的前提大部分连接大部分时间是空闲的同一时刻只有少数连接是活跃的。IO 多路复用充分利用了等待时间用一个线程就扛住了海量并发连接。Linux 下的 IO 多路复用有三套经典 APIselect、poll、epoll。Redis 使用的是性能最高的 epoll它采用事件通知机制不需要遍历所有连接连接数再多也不会导致性能线性下降。C/C 开发可以直接使用 Linux 原生的 epoll APIJava 则是通过 NIO 封装了底层的 epoll 机制。做后端开发IO 多路复用是必须吃透的底层知识。三. 源码视角设计背后的工程考量站在源码实现的角度看Redis 的设计处处体现着 “实用主义” 的工程智慧这里挑两个点做解读。3.1 quicklist空间与时间的经典折中在 quicklist 出现之前list 用 ziplist 和 linkedlist 切换要么空间好但插入性能差要么插入好但空间碎片多。quicklist 的设计非常巧妙宏观上是双向链表保证两端 O (1) 插入删除微观上每个链表节点是一个 ziplist用连续内存减少碎片、提升缓存命中率。你可以调整每个 ziplist 的长度来适配不同场景ziplist 设得短性能接近链表设得长空间更紧凑。这就是典型的 “把选择权交给使用者” 的设计提供机制不提供标准答案。3.2 ae 事件循环单线程的驱动核心Redis 的单线程本质是跑在一个事件循环aeEventLoop里。这个事件循环同时处理两类事件文件事件客户端的网络 IO 请求底层封装 epoll时间事件定时任务比如定期删除过期 key、持久化触发等。整个事件循环就是一个 while 循环每次计算最近的时间事件还有多久然后阻塞等待文件事件超时后就去处理时间事件。整个过程没有锁、没有线程切换逻辑清晰且高效是非常经典的 Reactor 模式单线程实现。核心考点总结设计思想对外数据类型与底层编码分离接口语义不变内部按需优化在空间和时间之间做权衡。编码对应stringint、embstr、rawhash/set/zset小数据量用 ziplist/intset大数据量转 hashtable/skiplistlist3.2 之后默认 quicklist兼顾链表与压缩列表的优势学习原则记思想不记数字理解 “小数据省空间、大数据保性能” 的核心逻辑。单线程本质命令执行单线程串行天然线程安全网络 IO 等辅助工作由后台线程完成。单线程快的四大原因内存存储、功能简洁、无线程竞争开销、epoll IO 多路复用。IO 多路复用select/poll/epoll 的区别epoll 事件驱动的优势适用场景。单线程弊端慢命令会阻塞全局生产环境禁止执行耗时未知的重操作。结尾 我是草莓熊 Lotso若这篇技术干货帮你打通了学习中的卡点 【关注】跟我一起深耕技术领域从基础到进阶见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好需要时直接查、随时用 【评论】分享你的经验或疑问比如曾踩过的技术坑一起交流避坑 ️ 【投票】用你的选择助力社区内容方向告诉大家哪个技术点最该重点拆解 技术之路难免有困惑但同行的人会让前进更有方向愿我们都能在自己专注的领域里一步步靠近心中的技术目标结语本文我们从对外数据结构到底层编码实现再到单线程模型的工作原理与性能本质把 Redis 最核心的两个基础知识点讲透了。理解这些底层设计你再去学具体命令、排查线上问题就不会只停留在表面。接下来我们会逐个深入每一种数据结构拆解常用命令、典型业务场景与最佳实践带你从 “会用” 走向 “用好”。✨把这些内容吃透超牛的放松下吧✨ʕ˘ᴥ˘ʔづきらど