跳转到内容

Redis 单线程模型与高性能原理


1. 核心问题:单线程为什么这么快

Section titled “1. 核心问题:单线程为什么这么快”

Redis 命令执行是单线程的,却能达到约 10 万 QPS,背后依赖四个核心机制的协同:

机制解决的问题效果
纯内存操作磁盘 I/O 太慢读写速度是磁盘的 100 倍以上
I/O 多路复用为每个连接开线程太重单线程同时监听万级连接
单线程无锁多线程竞争开销大无锁,无上下文切换
高效数据结构通用结构性能不足SDS、跳表等专为高性能设计

Redis 所有数据存储在内存中,读写操作无需访问磁盘:

内存 vs 磁盘读写速度对比:
内存随机读写: ~100 ns ← Redis 数据存这里
SSD 随机读写: ~100 μs (比内存慢约 1000 倍)
HDD 随机读写: ~10 ms (比内存慢约 100000 倍)

即使加上网络传输和协议解析,一次 Redis 命令的端到端延迟通常在 1 ms 以内


每个客户端连接都需要一个线程:

传统方案(每连接一线程):
客户端 1 ──▶ 线程 1(等待读取,大部分时间阻塞)
客户端 2 ──▶ 线程 2(等待读取,大部分时间阻塞)
...
客户端 N ──▶ 线程 N
问题:1 万个连接需要 1 万个线程,内存和上下文切换开销极大

一个线程同时监听多个连接,只有当连接有事件(可读/可写)时才处理:

I/O 多路复用方案:
客户端 1 ─┐
客户端 2 ─┤
客户端 3 ─┼──▶ epoll(内核监听)──▶ 事件队列 ──▶ 单线程逐个处理
... ─┤ [就绪事件]
客户端 N ─┘
特点:
- 无论多少连接,只需 1 个线程
- 没有事件时线程不消耗 CPU
- epoll 是 Linux 下最高效的多路复用实现(O(1) 事件通知)
Redis 主线程事件循环(单线程):
┌──────────────────────────────────────────┐
│ Redis 事件循环 │
│ │
│ epoll_wait() ──▶ 获取就绪事件 │
│ │ │
│ ▼ │
│ 文件事件处理器 │
│ ├── 连接应答(accept) │
│ ├── 命令读取(read) │
│ ├── 命令执行(单线程,原子) │
│ └── 结果回写(write) │
│ │
│ 时间事件处理器 │
│ ├── serverCron(清理过期 key 等) │
│ └── AOF 刷盘检查 │
└──────────────────────────────────────────┘

无锁:不需要 synchronized、mutex,消除竞争开销
无上下文切换:单线程不涉及线程调度,CPU 缓存友好
命令原子性:单线程天然保证每条命令原子执行,简化并发编程

单线程意味着一条慢命令会阻塞后续所有请求:

正常情况:命令 A(0.1ms) → 命令 B(0.1ms) → 命令 C(0.1ms)
慢命令: 命令 A(0.1ms) → KEYS *(5000ms,全表扫描) → 命令 B 被阻塞 5 秒

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 # 读操作也使用多线程

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 判断结束)
内存预分配❌ 每次重新分配✅ 减少频繁内存分配
溢出保护❌ 无检查✅ 修改前检查容量

┌────────────────────────────────────────────────────────┐
│ Redis 高性能来源 │
│ │
│ 客户端请求 │
│ │ │
│ ▼ │
│ epoll I/O 多路复用 ──▶ 单线程事件循环 │
│ │ │ │
│ │ 纯内存数据操作 │
│ │ 高效数据结构 │
│ │ 无锁/无上下文切换 │
│ │ │ │
│ ▼ ▼ │
│ 网络回包 约 10 万 QPS │
└────────────────────────────────────────────────────────┘
机制解决的问题收益
纯内存磁盘 I/O 慢延迟降低 1000 倍
epoll 多路复用多线程开销大支持万级并发连接
单线程执行锁竞争、上下文切换无额外开销,命令原子
高效数据结构通用结构不够快各类型操作接近理论最优
多线程网络 I/O(6.0+)网络带宽是瓶颈高带宽下吞吐进一步提升