当前位置: 首页 > news >正文

Redis为什么快

Redis 为什么快?7个原因拆解:从内存到IO,从数据结构到设计哲学

作者:没有逆称
关键词:Redis 高性能、IO 多路复用、epoll、SDS、跳表、单线程模型


目录

  • 一、面试被问住了
  • 二、原因一:纯内存操作
  • 三、原因二:单线程模型(核心争议点)
  • 四、原因三:IO 多路复用(epoll)
  • 五、原因四:高效的数据结构
  • 六、原因五:对象共享与内存优化
  • 七、原因六:渐进式 rehash
  • 八、原因七:合理的事件驱动架构
  • 九、完整思维导图与总结表
  • 十、常见面试题

一、面试被问住了

上一场面试聊到项目缓存选型,面试官冒了一句:

“Redis 号称 QPS 能到 10 万+,你说说它为什么快?”

我脱口而出:“因为它是内存数据库……”

面试官继续盯着我,我硬挤了几个词——"单线程、IO多路复用……"就到头了。

事后复盘发现,Redis 快的理由远不止两三点,面试官期待的是系统性回答,从内存到IO、从数据结构到设计哲学,一层层拆开讲。

下回再被问到,我可以按下面这个思路答了——


先给面试官一个总览:

Redis 高性能 = 内存操作 + 单线程 + IO多路复用 + 高效数据结构 + 内存优化 + 渐进式rehash + 事件驱动 ┌─────────────────────────────────────┐ │ 各因素影响权重 │ ├─────────────────────────────────────┤ │ 纯内存操作 ★★★★★ 最重要 │ │ 高效数据结构 ★★★★☆ │ │ IO多路复用 ★★★★☆ │ │ 单线程模型 ★★★☆☆ 有争议 │ │ 渐进式rehash ★★★☆☆ │ │ 对象共享与编码优化 ★★★☆☆ │ │ 事件驱动架构 ★★☆☆☆ │ └─────────────────────────────────────┘

下面逐条展开。


二、原因一:纯内存操作

这是最根本的原因,没有之一。

2.1 内存 vs 磁盘 速度对比

一个简单的数量级对比:

存储层级典型延迟相比内存的倍数
CPU 寄存器~1 ns
内存(RAM)~100 ns1x(基准)
SSD(随机读)~100 μs1000x
磁盘(机械寻道)~10 ms100,000x
网络请求(同机房)~500 μs5000x

Redis 所有的数据读写都在内存中完成,不需要磁盘 IO。

2.2 对比 MySQL

MySQL 查询路径: 查询 → 解析SQL → 执行计划 → 存储引擎 ↓ 检查 Buffer Pool(内存中是否有缓存页) ↓ 没有 → 磁盘 IO 加载数据页(慢!) ↓ 返回结果 Redis 查询路径: 命令 → 解析 → 直接在内存哈希表查 ↓ 返回结果 (快!)

🔑一句话:内存访问速度比磁盘快 3~5 个数量级,这是 Redis 快的先天优势。


三、原因二:单线程模型(核心争议点)

3.1 Redis 的核心处理确实是单线程

Redis 6.0 之前——所有网络 I/O 和命令执行全部单线程。
Redis 6.0 之后——网络 I/O 引入多线程命令执行依然是单线程

很多人疑惑:单线程不是更慢吗?怎么反而成了高性能的原因?

3.2 单线程为什么快?

原因 1:避免了上下文切换的开销

多线程环境下,CPU 需要在不同线程之间切换,每次切换都要保存和恢复寄存器、程序计数器等,带来几十到几百纳秒的额外开销。

多线程模型(一般应用): 线程A ———→ 切换 → 线程B ———→ 切换 → 线程A ↑ ↑ 保存/恢复 保存/恢复 上下文 上下文 单线程模型(Redis): 线程A —→ 一直跑 —→ 完事 没有切换,没有开销

原因 2:避免了锁竞争

多线程共享数据需要加锁,加锁本身有开销,锁冲突更会导致线程阻塞等待。

Redis 单线程执行命令,天然不需要锁:

你们同时读 Key A?排好队,一个一个来,不存在谁等谁的情况。

3.3 单线程的瓶颈在哪?怎么解决?

瓶颈点后果解决方案
单个 key 存储超大数据(big key)阻塞其他命令拆分大 key、用 scan 分批
CPU 密集型操作(如 hgetall 千万级哈希)长时间独占 CPU优化数据结构、使用 unlink 异步删除
单核性能上限单个实例吞吐瓶颈Redis Cluster 分片扩容

3.4 那为什么 6.0 又引入了多线程?

6.0 的多线程只用于网络 I/O 读写,不用于命令执行:

Redis 6.0 多线程模型: 主线程(单线程)——执行命令 ↕ I/O 线程池(多线程)——读写 socket 数据

改进的原因是:网络 I/O 读写在大流量下占用 CPU 时间可观,用多线程分摊后,主线程可以更多地处理命令。


四、原因三:IO 多路复用(epoll)

4.1 什么是 IO 多路复用?

先看一个场景:

你是 Redis 服务器,有 10000 个客户端连接着你。每个客户端随时可能发命令过来,你该怎么知道谁发了消息?

方案一:BIO(阻塞 IO)——每个连接一个线程

// 伪代码:每个客户端一个线程while(true){SocketclientSocket=serverSocket.accept();newThread(()->{while(true){byte[]data=clientSocket.read();// 阻塞!没有数据就等// 处理命令}}).start();}

→ 1 万连接 = 1 万个线程 = 内存爆炸 + 上下文切换灾难

方案二:NIO + epoll 多路复用(Redis 的做法)

// 伪代码:一个线程管理所有连接intepollFd=epoll_create(10000);while(true){intn=epoll_wait(epollFd,events,10000,-1);// 谁有数据就返回谁for(inti=0;i<n;i++){// 只处理有数据可读的 socket,效率极高processCommand(events[i].fd);}}

4.2 epoll 为什么比 select/poll 快?

机制时间复杂度最大连接数工作方式
selectO(n)1024轮询所有 fd
pollO(n)无限制轮询所有 fd
epollO(1)无限制回调通知,只返回有事件的 fd

epoll 的核心优势——它不轮询,只通知。

你问 1 万个学生:“谁有问题?”(select/poll:遍历一遍)
vs
有问题的人举手,你只看到举手的人(epoll:回调通知)

4.3 Redis 中的实现

┌─────────────────────────────────────────┐ │ Redis 事件循环 │ │ │ │ ┌─────────────┐ ┌───────────────┐ │ │ │ 文件事件 │ │ 时间事件 │ │ │ │(Socket可读)│ │(定时任务) │ │ │ └──────┬──────┘ └──────┬────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────────┐ │ │ │ aeApiPoll(epoll_wait) │ │ │ └──────────────┬───────────────┘ │ │ │ │ │ ▼ │ │ 处理并返回结果给客户端 │ └─────────────────────────────────────────┘

Redis 内部封装了ae 事件驱动库,在 Linux 上使用 epoll,在 macOS 上用 kqueue,用统一接口隐藏差异。


五、原因四:高效的数据结构

Redis 不只存字符串,它的五种基本类型(String / Hash / List / Set / ZSet)底层都用了一套精心设计的数据结构,针对不同场景做了不同的编码优化。

5.1 底层数据结构一览

┌──────────┐ │ SDS │ ← 代替 C 字符串的"智能字符串" ├──────────┤ │ 双向链表 │ ← List 的底层之一 ├──────────┤ │ 压缩列表 │ ← 少量元素时的"紧凑存储" ├──────────┤ │ 哈希表 │ ← Hash / Set 的底层 ├──────────┤ │ 跳表 │ ← ZSet 的核心结构 ├──────────┤ │ 整数集合 │ ← 纯整数时的 Set 优化 ├──────────┤ │ 快速列表 │ ← 压缩列表 + 双向链表的混合体 └──────────┘

5.2 SDS(Simple Dynamic String)

C 语言的字符串用char[],问题很多。Redis 自己实现了 SDS:

// Redis 3.2+ 的 SDS 结构(sdshdr8)structsdshdr8{uint8_tlen;// 已用长度uint8_talloc;// 总分配长度unsignedcharflags;// 类型标记charbuf[];// 数据};

SDS 比 C 字符串好在哪?

对比项C 字符串SDS
获取长度O(n),遍历直到\0O(1),直接读 len
缓冲区溢出容易,不检查边界自动扩容,预分配空间
二进制安全\0会截断数据✅ 以 len 为准,存二进制
修改时重新分配每次都要空间预分配 + 惰性释放,减少 realloc

5.3 跳表(Skip List)

ZSet 的有序数据结构使用的是跳表(skip list),不是平衡树。

跳表的原理:

Level 3: 1 ─────────────────────────→ 9 ─────────→ null Level 2: 1 ─────────→ 5 ─────────→ 9 ─────────→ null Level 1: 1 ─→ 3 ─→ 5 ─→ 7 ─→ 9 ─→ 11 ─→ 13 ─→ null ↑ 查找 7:从顶层开始 1 < 7 → 向右 9 > 7 → 降一层 5 < 7 → 向右 9 > 7 → 降一层 7 == 7 ✓ (O(logN))

为什么用跳表不用平衡树?

对比项红黑树/平衡树跳表
实现复杂度复杂(旋转、染色)简单(随意增删层数)
范围查询中序遍历较复杂简单,底层链表直接遍历
内存占用每个节点 2 个指针平均每个节点 1.33 个指针(更低)
插入/删除O(logN)O(logN)

5.4 压缩列表(ziplist)

当 Hash / List / ZSet 的元素较少时,Redis 用压缩列表代替哈希表/跳表,连指针都不存,整个结构就是一块连续内存:

压缩列表内存布局: [zlbytes] [zltail] [zllen] [entry1] [entry2] ... [entryN] [zlend] 4字节 4字节 2字节 变长 变长 1字节

每个 entry 用变长编码存储前一个 entry 的长度和自身数据,不存指针,纯挨着放

🔑面试重点:你能说出压缩列表比哈希表好在哪?—— 节省内存,适合小数据量。


六、原因五:对象共享与内存优化

6.1 整数对象共享池

Redis 启动时会预创建0~9999 这 1 万个整数对象,后续所有用到这些整数值的地方,直接复用:

127.0.0.1:6379>SET k1100OK127.0.0.1:6379>SET k2100OK# k1 和 k2 的值指向同一个共享整数对象,不重复分配内存!

6.2 不同编码自动转换

Redis 会根据元素的数量和大小,自动选择最省内存的编码方式

Hash/List/ZSet 编码自动升级路径: 少量元素 ────→ 压缩列表(ziplist) │ │ │ 元素增多或 │ │ 元素变大 │ ▼ ▼ 大量元素 ────→ 哈希表 / 跳表 / 快速列表 Set 编码自动升级: 纯整数 ──→ 整数集合(intset) │ │ 添加非整数 ▼ 任意值 ──→ 哈希表

示例:你往一个 ZSet 里只塞了 10 个分数,Redis 不会直接用跳表,而是用压缩列表省内存。


七、原因六:渐进式 rehash

7.1 问题:哈希表要扩容怎么办?

哈希表元素越来越多,负载因子提高,必须扩容

通常的做法:

旧表(4个桶) → 新表(8个桶) 1. 分配新表 2. 把旧表所有元素 rehash 到新表 ← 这一步如果元素很多,会卡住! 3. 释放旧表

如果 Redis 在 rehash 时停下所有请求,几百万 key 全部重新计算哈希位置——那快就不成立了。

7.2 Redis 的做法:渐进式

不一次性搬完,每次搬一点点:

┌─────────────────────────────────────────┐ │ 渐进式 rehash 示意图 │ │ │ │ rehashidx = 0 │ │ │ │ 旧表[0] ──→ 新表[0] (本次搬) │ │ 旧表[1] ──→ 新表[1] (下次搬) │ │ 旧表[2] ... │ │ 旧表[3] ... │ │ 旧表[4] ... │ │ 旧表[5] ... │ │ 旧表[6] ... │ │ 旧表[7] ... │ │ │ │ 每次增删改查操作,顺带搬一个桶 │ │ 搬完一个 rehashidx++ │ │ 搬完所有 → rehashidx = -1(完成) │ └─────────────────────────────────────────┘

关键点:

🔑一句话:渐进式 rehash 把"一次性阻塞"变成了"平滑过渡",保证了 Redis 在高负载下的响应速度。


八、原因七:合理的事件驱动架构

8.1 Redis 的完整执行流程

┌─────────┐ ┌───────────────┐ ┌──────────┐ │ 客户端 1 │ ──→ │ │ │ │ └─────────┘ │ IO 多路复用 │ │ 命令执行 │ ┌─────────┐ │ (epoll_wait) │ ──→ │ (单线程) │ │ 客户端 2 │ ──→ │ │ │ │ └─────────┘ └───────────────┘ └──────────┘ ┌─────────┐ │ │ 客户端 N │ │ └─────────┘ ▼ ┌──────────────┐ │ 三种结果类型 │ │ │ │ 简单字符串 → │ │ 数组 → │ │ 整数 → │ └──────────────┘

8.2 Redis 的处理流程有多轻量

对比 Spring Boot Web 请求处理:

Spring Boot 处理一次请求: 创建线程(或从线程池取)→ 解析HTTP → 调用Controller → Service → DAO → 数据库查询 → 序列化JSON → 写回 (整个链路层层抽象,框架开销大) Redis 处理一次命令: epoll_wait 返回 → 读取命令 → 查找 key → 执行操作 → 写回结果 (几乎没有框架抽象层,纯 C 手写,极其轻量)

九、完整总结

9.1 一图流

┌──────────────────────────────┐ │ Redis 为什么快 │ └──────────────────────────────┘ │ ┌───────────────┼───────────────────┐ │ │ │ ┌────┴────┐ ┌────┴────┐ ┌──────┴──────┐ │ 内存层 │ │ IO 层 │ │ 数据结构层 │ └────┬────┘ └────┬────┘ └──────┬──────┘ │ │ │ ┌─────┴─────┐ ┌────┴─────┐ ┌──────┴──────┐ │ 纯内存操作 │ │ IO多路 │ │ SDS / 跳表 │ │ │ │ 复用epoll│ │ 压缩列表 │ └───────────┘ └──────────┘ └─────────────┘ ┌───────┐ ┌──────────────┐ ┌─────────────┐ │ 模型层 │ │ 优化层 │ │ 架构层 │ └───┬───┘ └──────┬───────┘ └──────┬──────┘ │ │ │ ┌────┴─────┐ ┌────┴──────┐ ┌──────┴──────┐ │ 单线程 │ │ 整数共享 │ │ 渐进式 │ │ 无锁设计 │ │ 编码切换 │ │ rehash │ └──────────┘ └───────────┘ └─────────────┘

9.2 速记表

原因一句话总结面试优先级
纯内存操作比磁盘快 3~5 个数量级⭐⭐⭐⭐⭐
单线程模型无上下文切换、无锁竞争⭐⭐⭐⭐
IO 多路复用 epollO(1) 事件通知,不轮询⭐⭐⭐⭐⭐
SDS / 跳表 / 压缩列表数据结构为性能极致优化⭐⭐⭐⭐
整数共享 + 编码切换减少内存分配、自动选择最优编码⭐⭐⭐
渐进式 rehash扩容不阻塞,平滑过渡⭐⭐⭐⭐
轻量事件驱动C 语言实现,几乎没有框架抽象开销⭐⭐

十、常见面试题

Q1:Redis 单线程的优缺点?6.0 为什么引入多线程?

答:

优点:

缺点:

6.0 多线程:仅在网络 I/O 读写阶段使用多线程,命令执行依然是单线程。原因是大流量下网络 I/O 本身就是瓶颈,用多线程分摊后主线程 CPU 更宽裕。


Q2:Redis 的 IO 多路复用具体是怎么实现的?

答:Redis 封装了ae事件驱动库,底层在 Linux 上用epoll,macOS 上用kqueue。工作机制是注册 socket 到 epoll,然后epoll_wait阻塞等待事件,有事件到达时只处理有数据可读的 socket,时间复杂度 O(1)。


Q3:Redis 的哈希表是怎么扩容的?会不会阻塞?

答:Redis 采用渐进式 rehash。当负载因子超过阈值时,分配新哈希表,但不一次性搬完。每次处理客户端命令时顺带搬一个桶,直到全搬完。rehash 期间查 key 先查新表再查旧表,新增 key 只加新表,不影响读写。


Q4:SDS 比 C 字符串好在哪里?

答:

  1. O(1) 获取长度——C 字符串需要遍历到\0
  2. 二进制安全——以 len 为长度,存二进制数据没问题
  3. 自动扩容——不需要手动管理内存
  4. 空间预分配——减少内存 realloc 次数

Q5:ZSet 为什么用跳表不用红黑树?

答:

  1. 实现简单——跳表代码比红黑树简单得多,不容易出 bug
  2. 范围查询方便——底层是链表,直接遍历即可;红黑树中序遍历较复杂
  3. 内存更低——跳表平均每个节点约 1.33 个指针,红黑树需要 2 个(左右子节点)+ 1 个父节点 = 3 个
  4. O(logN) 性能不输红黑树

Q6:Redis 处理 10 万 QPS,瓶颈到底在哪里?

答:瓶颈通常不在 CPU,而是:

  1. 网络带宽——尤其大 key 传输时
  2. 内存带宽——huge key 操作频繁时
  3. big key 阻塞——单个 key 几十 MB,一次操作阻塞其他请求
  4. fork 耗时——RDB 持久化时的 fork 操作在内存大时会阻塞

Q7:如果 Redis 是单线程,那持久化(RDB/AOF)不会阻塞吗?

答:核心命令执行是单线程,但 Redis 用子进程(fork)处理持久化:


Q8:和 Memcached 比,为什么 Redis 有时更快?

答:


如果这篇文章对你有帮助,点个赞吧!


参考资料:

http://www.jsqmd.com/news/839691/

相关文章:

  • 技能树工具haru-skills:结构化学习路径规划与知识管理实践
  • 实战解析:基于eNSP与USG5500防火墙构建企业级安全策略
  • NVMe SSD的“午睡”与“秒醒”:深入解读PS3/PS4低功耗状态的实现与代价
  • 电压跟随器:从原理到实战,如何用它解决信号传输的三大难题?
  • 【软考高级架构】论文范文18——论AIOps在云原生系统智能运维中的架构设计
  • Houdini-URP-风格化树木:从模型到Shader的全流程实战
  • 北京金发钹祥金属材料贸易:口碑好的北京不锈钢刨槽厂家推荐 - LYL仔仔
  • 2026年福建省CPPM报考指南:证书颁发机构与官方授权报考机构全解析 - 众智商学院课程中心
  • 终极固件解密指南:Universal-IFR-Extractor快速提取EFI/UEFI内部表单
  • 【LearnOpenGL实战】从SOIL到现代图像库:跨越十年的配置陷阱与解决方案
  • PyInstaller Extractor技术实现与逆向分析实践
  • SmartDock:如何在Android设备上构建高效桌面环境
  • 蓝牙AoA定位技术:从原理到实战,实现厘米级室内精准定位
  • 基于瑞萨RA8的智能小车开发:从硬件选型到多任务调度实战
  • 终极代码阅读神器:MultiHighlight智能高亮插件完整指南
  • CANoe UDP通信避坑指南:从Win11报错到本地回环地址的完整配置流程
  • 开源求职工具箱:用Markdown+Git构建个人求职管理系统
  • ncmdump终极指南:快速解锁网易云音乐加密文件,实现音乐格式自由转换
  • 最新英语作文批改APP测评 适合学生党写作提分的实用指南
  • 微信读书笔记助手:3个步骤让你从阅读到知识管理的效率提升300%
  • 企业微信 SCRM 服务商代开发实战:参数配置全打通,源码可直接交付
  • Synology Photos人脸识别终极补丁:让DS918+等x86设备重获AI照片管理能力
  • ScreenClaw:轻量级屏幕抓取与自动化工具的设计原理与实践
  • 从机房搬服务器到写代码上云:一个传统运维的十年转型路,我如何成了SRE?
  • 5分钟掌握ROFL播放器:英雄联盟回放文件终极查看器完整指南
  • 不只是优化和频率:用GaussView 5.0玩转HOMO/LUMO、电子密度与反应位点预测
  • 为什么vscode-R能让你在Visual Studio Code中享受专业级R开发体验?
  • Warcraft Helper:让魔兽争霸3在Windows 10/11上完美运行的3个关键步骤
  • 用示波器抓CAN波形,手把手教你从CAN_H信号里‘读’出0x18DAF110和0x06 0x08
  • ARM链接器关键选项解析:构建属性与FPU配置实战