很多人背熟了 Redis 的八股文,却从没想过:为什么它要设计这么多底层结构?
答案只有四个字:因地制宜。
数据少的时候,它拼命"省内存";数据多的时候,它拼命"保速度"。
理解了这个逻辑,你就能把 Redis 所有的核心设计串成一条线。
第一站:基础数据类型,Redis 的"五官"
Redis 为什么快?因为它不把数据当字节看,而是当数据结构看。这五种基础类型,就像你的五官,各司其职:
| 类型 | 生活类比 | 一句话场景 |
|---|---|---|
| String(字符串) | 便签纸 | 存验证码、做计数器 |
| List(列表) | 排队通道 | 消息队列、最新动态 |
| Hash(哈希) | 微型档案柜 | 存用户信息、购物车 |
| Set(集合) | 去重背包 | 抽奖去重、共同好友 |
| ZSET(有序集合) | 竞技排行榜 | 积分排名、延迟队列 |
主线伏笔: 这五种类型里,ZSET 是最复杂的,因为它既要去重(像 Set),又要排序(像 List)。为了同时满足这两点,Redis 在底层搞出了"两套班子",也是我们今天的重头戏。
第二站:ZSET 的进化------"人少坐板凳,人多建高架"
如果你来实现 ZSET,你面临一个矛盾:用数组排序快(但插入慢),用链表插入快(但查找慢)。
Redis 的解决方案极其聪明:根据人数多少,动态切换战术。

1. 人少的时候(元素 < 128 且文本较短):启用"大通铺"模式(listpack)
大家紧挨着坐在一条长板凳上(连续内存),按分数从小到大排好。虽然新增一个人需要挪动后面的人(线性查找),但反正人少,挪动的代价极低。关键是这张"长板凳"极度节省内存,没有任何指针开销。
物理结构示意:
text
+--------+--------+--------+--------+--------+
| 总分數 | 元素数 | "小明" | "小红" | END |
| | | +95 | +92 | |
+--------+--------+--------+--------+--------+
每个小块只记录自己的长度,互不影响
追问:为什么现在的 Redis 用 listpack,而不用以前的 ziplist?
因为 ziplist 像"多米诺骨牌",中间插一个人,后面的骨牌全得动(级联更新)。listpack 改成了 "各自独立" ,改自己不影响邻居,彻底消除了这个隐患。
2. 人多的时候(超过阈值):立刻升级为"立交桥 + GPS"模式(跳表 + 字典)
一旦变成大几千人的排行榜,再用"大通铺"就会卡死。Redis 立刻拆成两套系统协作:

- GPS 导航(字典 dict) :专门用来查"张三多少分?"------ O(1) 瞬间定位。
- 立体高架桥(跳表 skiplist) :专门用来查"分数前 10 名是谁?"------ 多层索引极速遍历。
第三站:跳表的秘密------抽奖抽出来的"高速电梯"
跳表是 ZSET 在大数据量下的灵魂。它到底是什么?
你可以把跳表想象成一条普通的地铁线,但随机给一部分站点加装了"跨站直达电梯" :
text
rust
L3 (特快) Head ----------------------------------------> [100] ----------------------------------------> NULL
L2 (快线) Head ----------------> [50] -----------------> [100] ----------------------------------------> NULL
L1 (普快) Head -------> [20] --> [50] --------> [80] --> [100] ----------------------------------------> NULL
L0 (底站) Head -> [5] -> [20] -> [35] -> [50] -> [65] -> [80] -> [90] -> [100] ---------------------> NULL
- 底层:所有站点(元素)串成一条线。
- 上层:随机抽 50% 的站点,建一条快线。
- 更上层:再在快线里抽 50%,建一条特快线。
查找"80"的过程 :L3 直达 100(过头了)→ 下到 L2 到 50 → 右到 100(过头)→ 下到 L1 到 80,命中。只跳了 3 步!
这种"随机抽奖"的方式,让查找效率直接飙升到 O(logN) (百万级数据只需几次跳跃)。
为什么不用红黑树(平衡二叉树)?
因为红黑树虽然查找快,但做范围查询(比如取前 10 名)很麻烦 ,需要中序遍历。而跳表的底层是链表,取出一段数据就像切豆腐一样顺滑,而且代码实现比红黑树简单十倍,不容易出 Bug。
第四站:哈希表扩容------不砸墙的"蚂蚁搬家"
Redis 的全局大仓库(字典)如果塞满了,需要扩容换个大房子。
粗暴的思维:把旧房子东西全搬到新房子,再开门营业。
结果 :Redis 卡死几秒,线上报警。
Redis 的思维 :渐进式 Rehash(蚂蚁搬家) 。

- 先把新房子(扩容后的空间)准备好。
- 不着急搬。每次你来找数据(执行 get/set 命令)时,Redis 顺手搬一个箱子过去。
- 半夜没人访问时,定时任务再偷偷主动搬几个箱子。
搬家期间的规则(面试加分项):
- 查数据:先翻旧房子,翻不到再去翻新房子。
- 写数据:只写新房子(保证旧房子只减少不增加,最终变成空房被回收)。
这样搬一整个仓库,用户完全感知不到卡顿,把耗时的"大手术"切碎成毫秒级的"小动作" 。
第五站:线程模型------彻底讲透"几个线程"和"谁干活"
网上关于 Redis 线程的讨论经常吵成一锅粥:有人说单线程,有人说多线程。都对,也都不全对。 要回答清楚,得把 Redis 的线程分成三支队伍来看。
先破一个最常见的误解
很多人说"Redis 6.0 变成了多线程",这句话对了一半,错了一半。
✅ 对的部分:Redis 6.0 确实引入了多线程,用来处理网络 IO。
❌ 错的部分 :Redis 执行命令的核心逻辑,永远是单线程,从来没变过。
一句话定论:多线程只干"搬砖"的活(收数据、发数据),核心的"脑力活"(操作内存数据)依然是老板一个人干。
三支线程队伍,各司其职

第一队:主线程(1个) ------Redis 的"大脑",唯一有资格操作内存数据的线程。所有命令(GET/SET/ZADD 等)都在这里顺序执行,永远不加锁,所以极快。
第二队:后台 BIO 线程(3个,固定) ------专门处理"虽然耗时但不能阻塞主线程"的脏活累活:
- BIO 线程1:关闭文件和回收 AOF/RDB 产生的文件描述符。
- BIO 线程2 :AOF 持久化的
fsync刷盘操作(磁盘 IO 很慢,必须交给后台)。 - BIO 线程3 :Redis 4.0 引入,负责
UNLINK异步删除大 Key(如果是 DEL 删一个几十 MB 的 Hash,主线程会卡住,UNLINK 把删除动作扔给后台慢慢干)。
第三队:IO 线程(数量由配置决定) ------Redis 6.0 引入,专门负责网络数据的收发,绝不触碰 Redis 内存数据:
- 只负责
read()把请求从 Socket 读进来,和write()把结果写回 Socket。 - 数量由
io-threads配置项决定。设置io-threads N,实际会创建N-1个 IO 线程(主线程自己也会参与 IO 工作)。 - 默认值 :
io-threads 4,所以默认创建 3 个 IO 线程。
Redis 默认到底有几个线程?(精确答案)
以最常见的 Redis 6.0+ 默认配置(io-threads 4)为例:
| 线程类型 | 数量 | 说明 |
|---|---|---|
| 主线程 | 1 个 | 固定不变,执行所有命令 |
| 后台 BIO 线程 | 3 个 | 固定不变(Redis 4.0+) |
| IO 线程 | io-threads - 1 = 3 个 |
默认配置下 |
| 线程总数 | 7 个 | 1 + 3 + 3 = 7 |
注意 :如果把
io-threads设为 1,则 IO 线程数为 0,总线程数回到 4 个(1 主 + 3 BIO),等价于 Redis 6.0 之前的纯单线程模式。
IO 线程数怎么设置才合理?
io-threads 并非越大越好,设置不当反而因上下文切换拖慢性能:
-
官方铁律 :线程数必须小于 CPU 核心数。
-
推荐值:
- 4 核 CPU:设为
2或3 - 8 核 CPU:设为
6 - 不建议超过 8,再多反而因线程切换开销得不偿失。
- 4 核 CPU:设为
-
默认只负责写回 :默认 IO 线程只负责
write()写回响应。如果想让 IO 线程也负责read()读取请求,需额外开启io-threads-do-reads yes(通常不建议开,读取阶段涉及解析协议,交给多线程容易出问题)。
一个请求的完整生命周期(厘清分工)

注意:上图为 Redis 6.0+ 默认配置下的线程分工。三个阶段分工明确:
- 阶段一(读) :主线程单线程执行
read(),IO 线程不参与。 - 阶段二(执行命令) :只有主线程,串行操作内存,不加锁。
- 阶段三(写) :IO 线程并行 执行
write()写回响应(这是默认开启的多线程行为)。
如果设置 io-threads-do-reads yes,阶段一也会变为 IO 线程并行读取,但官方不推荐且极少在生产环境开启。
三个阶段的角色分工:
- 阶段一(读) :IO 线程并行把请求读进来,主线程等着。
- 阶段二(执行) :只有主线程在干活,串行执行所有命令,不加锁。
- 阶段三(写) :IO 线程并行把结果写回去,主线程等着。
面试时怎么回答"Redis 线程模型"?
递进式回答模板(背下来直接说):
"Redis 的线程模型要分三层看:
第一层(核心) :执行 Redis 命令的主线程永远是单线程的,所有读写内存的操作都在这一个线程里串行执行,所以 Redis 的所有命令都是天然原子性的,不需要加锁,这也是 Redis 为什么能做到微秒级延迟的根本原因。
第二层(后台) :Redis 有 3 个固定的后台 BIO 线程,专门干耗时但不紧急的脏活------关闭文件、AOF 刷盘、异步删除大 Key。
第三层(网络 IO) :Redis 6.0 引入了可配置的 IO 线程(默认 3 个),只负责 Socket 的读写,不碰内存数据。这样就把网络收发的耗时代价摊到了多个 CPU 核心上,解决了高并发下网络吞吐的瓶颈。
按默认配置,一个 Redis 实例总共有 7 个线程(1 主 + 3 BIO + 3 IO)。"
终章:整条主线的逻辑闭环
回顾一下这条主线,你会发现 Redis 的所有设计都遵循同一个法则:
| 场景 | 战术 | 目标 |
|---|---|---|
| 数据少时 | 紧凑存储(listpack) | 省钱(省内存) |
| 数据多时 | 跳表 + 字典 | 保快(O(logN) 查询) |
| 扩容时 | 蚂蚁搬家(渐进式 Rehash) | 求稳(不阻塞服务) |
| 处理请求时 | 核心单线程 + IO 多线程 | 分清主次(不加锁 + 榨干带宽) |
面试官问"Redis 为什么快"?
不要再只背"基于内存"了。正确的答案是:因为它是一套"活的"系统,面对不同的数据量和场景,它会动态选择最合适的底层数据结构,永远在内存占用和访问速度之间寻找当前最优解。线程模型上,核心命令执行永不妥协------永远是单线程不加锁;网络 IO 则因地制宜,用多线程榨干多核 CPU 的带宽。