Redis 单线程模型与高性能原理
1. 核心问题:单线程为什么这么快
Section titled “1. 核心问题:单线程为什么这么快”Redis 命令执行是单线程的,却能达到约 10 万 QPS,背后依赖四个核心机制的协同:
| 机制 | 解决的问题 | 效果 |
|---|---|---|
| 纯内存操作 | 磁盘 I/O 太慢 | 读写速度是磁盘的 100 倍以上 |
| I/O 多路复用 | 为每个连接开线程太重 | 单线程同时监听万级连接 |
| 单线程无锁 | 多线程竞争开销大 | 无锁,无上下文切换 |
| 高效数据结构 | 通用结构性能不足 | SDS、跳表等专为高性能设计 |
2. 纯内存操作
Section titled “2. 纯内存操作”Redis 所有数据存储在内存中,读写操作无需访问磁盘:
内存 vs 磁盘读写速度对比:内存随机读写: ~100 ns ← Redis 数据存这里SSD 随机读写: ~100 μs (比内存慢约 1000 倍)HDD 随机读写: ~10 ms (比内存慢约 100000 倍)即使加上网络传输和协议解析,一次 Redis 命令的端到端延迟通常在 1 ms 以内。
3. I/O 多路复用
Section titled “3. I/O 多路复用”3.1. 传统阻塞 I/O 的问题
Section titled “3.1. 传统阻塞 I/O 的问题”每个客户端连接都需要一个线程:
传统方案(每连接一线程):客户端 1 ──▶ 线程 1(等待读取,大部分时间阻塞)客户端 2 ──▶ 线程 2(等待读取,大部分时间阻塞)...客户端 N ──▶ 线程 N
问题:1 万个连接需要 1 万个线程,内存和上下文切换开销极大3.2. I/O 多路复用(epoll)
Section titled “3.2. I/O 多路复用(epoll)”一个线程同时监听多个连接,只有当连接有事件(可读/可写)时才处理:
I/O 多路复用方案:
客户端 1 ─┐客户端 2 ─┤客户端 3 ─┼──▶ epoll(内核监听)──▶ 事件队列 ──▶ 单线程逐个处理... ─┤ [就绪事件]客户端 N ─┘
特点:- 无论多少连接,只需 1 个线程- 没有事件时线程不消耗 CPU- epoll 是 Linux 下最高效的多路复用实现(O(1) 事件通知)3.3. Redis 事件循环
Section titled “3.3. Redis 事件循环”Redis 主线程事件循环(单线程):
┌──────────────────────────────────────────┐ │ Redis 事件循环 │ │ │ │ epoll_wait() ──▶ 获取就绪事件 │ │ │ │ │ ▼ │ │ 文件事件处理器 │ │ ├── 连接应答(accept) │ │ ├── 命令读取(read) │ │ ├── 命令执行(单线程,原子) │ │ └── 结果回写(write) │ │ │ │ 时间事件处理器 │ │ ├── serverCron(清理过期 key 等) │ │ └── AOF 刷盘检查 │ └──────────────────────────────────────────┘4. 单线程的优势与代价
Section titled “4. 单线程的优势与代价”4.1. 优势
Section titled “4.1. 优势”无锁:不需要 synchronized、mutex,消除竞争开销无上下文切换:单线程不涉及线程调度,CPU 缓存友好命令原子性:单线程天然保证每条命令原子执行,简化并发编程4.2. 代价:慢命令阻塞所有请求
Section titled “4.2. 代价:慢命令阻塞所有请求”单线程意味着一条慢命令会阻塞后续所有请求:
正常情况:命令 A(0.1ms) → 命令 B(0.1ms) → 命令 C(0.1ms)
慢命令: 命令 A(0.1ms) → KEYS *(5000ms,全表扫描) → 命令 B 被阻塞 5 秒5. Redis 6.0 多线程
Section titled “5. Redis 6.0 多线程”Redis 6.0 引入多线程,但只用于网络 I/O,命令执行仍是单线程:
Redis 6.0 线程模型:
客户端 ──▶ I/O 线程 1(读取请求、解析协议)─┐客户端 ──▶ I/O 线程 2(读取请求、解析协议)─┤──▶ 主线程(单线程执行命令)──▶ I/O 线程(回写响应)客户端 ──▶ I/O 线程 3(读取请求、解析协议)─┘
瓶颈转移:网络带宽大时,I/O 是瓶颈 → 多线程网络 I/O 提升吞吐 命令执行仍单线程,保持原子性开启多线程 I/O(redis.conf):
io-threads 4 # I/O 线程数(建议设为 CPU 核数的一半)io-threads-do-reads yes # 读操作也使用多线程6. 高效数据结构
Section titled “6. 高效数据结构”Redis 内部数据结构均针对性能专门设计:
数据结构底层实现:
String ──▶ SDS(Simple Dynamic String) └─ 预分配内存,O(1) 获取长度,二进制安全
Hash ──▶ listpack(小)/ hashtable(大) └─ 小对象用 listpack 节省内存
List ──▶ listpack(小)/ quicklist(大) └─ quicklist = 多个 listpack 节点的双向链表
Set ──▶ listpack / intset / hashtable └─ 纯整数集合用 intset,极度紧凑
ZSet ──▶ listpack(小)/ skiplist + hashtable(大) └─ 跳表支持 O(log N) 有序范围查询 哈希表支持 O(1) score 查询SDS(Simple Dynamic String)vs C 字符串:
| 对比项 | C 字符串 | SDS |
|---|---|---|
| 获取长度 | O(N) 遍历 | O(1)(存储了 len 字段) |
| 二进制安全 | ❌(以 \0 为结尾) | ✅(用 len 判断结束) |
| 内存预分配 | ❌ 每次重新分配 | ✅ 减少频繁内存分配 |
| 溢出保护 | ❌ 无检查 | ✅ 修改前检查容量 |
7. 高性能原理总览
Section titled “7. 高性能原理总览”┌────────────────────────────────────────────────────────┐│ Redis 高性能来源 ││ ││ 客户端请求 ││ │ ││ ▼ ││ epoll I/O 多路复用 ──▶ 单线程事件循环 ││ │ │ ││ │ 纯内存数据操作 ││ │ 高效数据结构 ││ │ 无锁/无上下文切换 ││ │ │ ││ ▼ ▼ ││ 网络回包 约 10 万 QPS │└────────────────────────────────────────────────────────┘| 机制 | 解决的问题 | 收益 |
|---|---|---|
| 纯内存 | 磁盘 I/O 慢 | 延迟降低 1000 倍 |
| epoll 多路复用 | 多线程开销大 | 支持万级并发连接 |
| 单线程执行 | 锁竞争、上下文切换 | 无额外开销,命令原子 |
| 高效数据结构 | 通用结构不够快 | 各类型操作接近理论最优 |
| 多线程网络 I/O(6.0+) | 网络带宽是瓶颈 | 高带宽下吞吐进一步提升 |