Redis 为什么速度远超MySQL?
Redis 为什么速度远超 MySQL?一文讲透 IO 模型与底层数据结构
当 Redis 的响应以微秒计,而 MySQL 还在磁盘上"翻箱倒柜"时,这个性能差距到底从何而来?
答案藏在两个维度里:数据怎么存的,以及请求怎么处理的。
一、存储介质:内存 vs 磁盘,这是降维打击
Redis 把所有数据塞进内存,MySQL 的数据躺在磁盘上。
这不是"快一点"的区别,而是量级的区别。内存访问速度是纳秒级,磁盘 I/O 是毫秒级——差了将近100 万倍。
MySQL 即便有 InnoDB Buffer Pool 缓存热点数据,一旦缓存未命中,就必须回磁盘。而 Redis 从第一次访问开始,就是内存直读,根本不存在"缓存未命中"这个概念。
Redis 的瓶颈从来不是 CPU,而是内存大小和网络带宽。
二、底层数据结构:O(1) vs O(logN),算法层面的碾压
| 特性 | Redis | MySQL (InnoDB) |
|---|---|---|
| 核心结构 | 哈希表(dict) | B+ 树 |
| 查找复杂度 | O(1) | O(logN) |
| 数据组织 | Key-Value 直接定位 | 多层树节点逐层遍历 |
Redis 的哈希表通过计算 key 的哈希值,直接定位到桶(bucket),一步到位。MySQL 的 B+ 树哪怕只有百万行数据,也要从根节点一路走到叶子节点,层层比较。
更关键的是,Redis 的底层其实有6 种数据结构在支撑:
| 底层结构 | 对应的数据类型 | 特点 |
|---|---|---|
| 简单动态字符串(SDS) | String | 二进制安全,O(1) 取长度 |
| 哈希表(dict) | Hash | O(1) 查找,链地址法解决冲突 |
| 压缩列表(ziplist)→ listpack | Hash/List/ZSet(小数据量时) | 内存紧凑,连续存储 |
| 双向链表 | List | 顺序读写 O(N) |
| 跳表(skiplist) | Sorted Set | 多级索引,查找 O(logN) |
| 整数数组 | List/ZSet(小整数时) | 顺序存储,节省空间 |
其中跳表是个精妙设计:在链表基础上增加多级索引,从底层往上"跳",把 O(N) 的遍历压到 O(logN)。这也是 Sorted Set 能高效实现排行榜的核心原因。
而 MySQL 的 B+ 树虽然支持范围查询,但每次查询都要经历多次磁盘寻址,代价不菲。
三、IO 模型:这才是 Redis 真正的杀手锏
如果说内存存储是"先天优势",那IO 多路复用就是 Redis 的"后天绝学"。
先看五种 IO 模型的本质区别
| 模型 | 特点 | 适用场景 |
|---|---|---|
| 阻塞 IO | 线程傻等,直到数据就绪 | 连接少、简单场景 |
| 非阻塞 IO | 线程轮询,CPU 空转 | 不推荐,浪费资源 |
| IO 多路复用 | 一个线程管多个连接,事件驱动 | 高并发、连接数多 ✅ |
| 信号驱动 IO | 内核发信号通知,TCP 不实用 | UDP 为主 |
| 异步 IO | 全程不阻塞,理想模型 | 依赖 OS 支持,Java 7+ 才有 |
Redis 用的是IO 多路复用,底层调用epoll(Linux)、kqueue(BSD)、select(兜底),默认epoll。
epoll 的核心优势:
- 没有连接数限制(select 有 1024 的硬伤)
- 内存共享机制,不需要像 select 那样每次把 fd 集合从用户态拷贝到内核态
- 内核直接维护事件就绪列表,效率远高于用户态轮询
Redis 6.0 之前:纯粹的单线程 + 多路复用
一个线程搞定一切:接收请求 → 执行命令 → 返回响应。
客户端连接 → epoll 监听 → 事件就绪 → 单线程处理 → 响应优势极其明显:无锁、无上下文切换、无竞态条件。命令执行是内存操作,微秒级完成,单线程完全够用,QPS 轻松破万。
但瓶颈也很清晰:当网络 IO 本身成为负担(大包读写、高延迟网络),主线程会卡在 IO 上,后面的命令全部排队。
Redis 6.0 之后:多线程 IO 登场
Redis 6.0 引入了多线程,但注意——核心命令执行依然是单线程。
新架构是这样的:
| 阶段 | 负责线程 | 说明 |
|---|---|---|
| 接收连接、读请求 | IO 线程(默认 4 个) | 多线程并行处理网络 IO |
| 执行命令(读写数据) | 主线程(单线程) | 无锁执行,保证原子性 |
| 写响应 | IO 线程 | 多线程并行写入 |
这是一个精妙的折中:把最耗时间的 IO 从主线程剥离,把最需要安全的命令执行留给单线程。既解决了 IO 瓶颈,又没丢掉无锁的简洁。
四、MySQL 的 IO 模型:为什么不用 epoll?
MySQL 在网络层用的是BIO(阻塞 IO)+ 连接池,配合poll/select做多路复用。
为什么不上 epoll?三个字:JDBC 不支持。
JDBC 诞生于 20 年前,那时只有 BIO。MySQL 的 Java 驱动(Connector/J)遵循 JDBC 标准,天然就是阻塞模式。每来一个连接,MySQL 就开一个线程(受max_connections限制)。
这和 Web Server 的场景完全不同:
- Web Server:请求无状态、短促,NIO + 多路复用是最优解
- MySQL:一个 session 里 SQL 必须串行执行(事务隔离、锁机制),连接本身就是"重资源"
所以 MySQL 的架构选择是合理的:BIO + 线程池 + 连接池,在 JDBC 生态下已经是最优实践。随便加大连接数,反而会因为上下文切换和锁竞争拖垮性能。
五、一张表总结核心差距
| 维度 | Redis | MySQL |
|---|---|---|
| 存储介质 | 内存 | 磁盘(+ Buffer Pool 缓存) |
| 数据结构 | 哈希表/跳表等,O(1) | B+ 树,O(logN) |
| 线程模型 | 单线程命令执行 + 多线程 IO(6.0+) | 多线程 + 连接池 |
| IO 模型 | epoll 多路复用(事件驱动) | BIO + poll/select |
| 典型延迟 | 微秒级 | 毫秒级 |
| 事务支持 | 无(仅简单事务) | ACID 完整支持 |
| 适用场景 | 缓存、计数、排行榜、分布式锁 | 复杂查询、事务、持久化 |
写在最后
Redis 快,不是因为某一个点,而是内存 + 哈希表 + 单线程无锁 + epoll 多路复用这四张牌同时打出的结果。每一张单拿出来都是优势,合在一起就是碾压。
但快不等于全能。Redis 不支持复杂查询、没有事务保障、数据依赖内存容量。MySQL 慢,是因为它要保数据安全、保事务一致、保查询灵活——这些都是有代价的。
选型的本质不是"谁更快",而是"谁更适合"。需要极致速度且数据模型简单,Redis 当仁不让。需要复杂关系和事务保障,MySQL 无可替代。
