vault backup: 2026-05-26 13:52:08
This commit is contained in:
@@ -12,7 +12,7 @@ Redis 是一个**基于内存的数据结构服务器**(data structure server
|
||||
本文聚焦于一个核心问题:**Redis 的内部架构长什么样?它是怎么做到单线程还这么快的?**
|
||||
|
||||
> [!question] 单线程 → 慢?这是直觉误区
|
||||
> 提到"单线程",大多数人的第一反应是"性能差"。但 Redis 单实例可达 **10 万+ QPS**,延迟在微秒级。为什么?因为它的瓶颈从来不在 CPU,而在**网络 IO 和内存**。单线程反而省去了锁竞争和上下文切换的开销——这是 Redis 架构设计中最反直觉、也最关键的一个洞察。
|
||||
> 提到"单线程",大多数人的第一反应是"性能差"。但 Redis 单实例可达 **10 万+ QPS**,延迟在微秒级。为什么?因为对于常规 KV 操作,瓶颈通常不在 CPU,而在**网络 IO 和内存**。单线程执行命令反而省去了锁竞争和上下文切换的开销——这是 Redis 架构设计中最反直觉、也最关键的一个洞察。
|
||||
|
||||
## 整体架构
|
||||
|
||||
@@ -59,9 +59,11 @@ flowchart TB
|
||||
| **通信协议简单** | RESP(Redis Serialization Protocol)结构极简,解析无状态、无复杂序列化,开销远低于 JSON/XML 类协议 |
|
||||
|
||||
> [!question] 那为什么 MySQL 不用单线程?
|
||||
> 因为瓶颈不同。MySQL 的查询可能涉及磁盘 IO(随机读写)、复杂 SQL 解析、事务锁管理——这些操作耗时从微秒到毫秒不等,单线程会让 CPU 大量空转等待 IO。而 Redis 操作都在内存中完成,单个命令执行时间在亚微秒级,CPU 根本不会闲着。
|
||||
> 因为瓶颈不同。MySQL 的查询可能涉及**磁盘 IO**(随机读写)、复杂 SQL 解析、事务锁管理——这些操作耗时从微秒到毫秒不等,单线程会被磁盘 IO 阻塞,无法处理其他请求。多线程能让一个线程等磁盘时,其他线程继续处理请求。
|
||||
>
|
||||
> 简单记:**瓶颈在 IO → 多线程有意义;瓶颈不在 IO → 单线程更简单高效。**
|
||||
> Redis 也有 IO——网络 IO(与客户端之间的 socket 读写),但 IO 多路复用(epoll)已经用**单线程 + 事件驱动**高效地解决了这个问题。真正执行命令的环节是纯内存操作,耗时在亚微秒级,CPU 几乎不会空闲,所以单线程足矣。
|
||||
>
|
||||
> 简单记:**瓶颈在阻塞型 IO(如磁盘 IO)→ 多线程有意义,避免单线程被阻塞时无法响应其他请求;内存操作足够快、IO 已被事件驱动解决 → 单线程省去锁和上下文切换的开销,反而更高效。**(当然,Redis 6.0 仍对网络 IO 读写做了多线程优化,说明高并发下网络 IO 本身也会成为瓶颈——详见后文。)
|
||||
|
||||
### 单线程的局限
|
||||
|
||||
@@ -189,7 +191,7 @@ flowchart TD
|
||||
> [!question] 为什么不用 HTTP?
|
||||
> HTTP 是为浏览器设计的文本协议,头部冗余大、解析复杂、无状态但需要反复握手。而 RESP 的设计目标是:**简单、无状态、可逐行流式解析**——正好匹配 Redis "命令 → 响应"的交互模型。
|
||||
|
||||
RESP2(Redis 5.0 及之前)有 5 种数据类型:
|
||||
RESP2 是 Redis 自诞生以来一直使用的默认协议(Redis 6.0+ 仍默认使用 RESP2,客户端需通过 `HELLO` 命令主动升级到 RESP3)。RESP2 定义了 5 种数据类型:
|
||||
|
||||
| 前缀 | 类型 | 示例 | 说明 |
|
||||
|------|------|------|------|
|
||||
@@ -249,7 +251,7 @@ sequenceDiagram
|
||||
>
|
||||
> 炒菜仍然是瓶颈,但洗菜装盘并行化后,整体吞吐量提升了。
|
||||
|
||||
### 开启方式
|
||||
### 开启方式(Redis 6.0+)
|
||||
|
||||
```conf
|
||||
# redis.conf
|
||||
@@ -257,6 +259,9 @@ io-threads 4 # IO 线程数(建议 CPU 核心数的一半,最
|
||||
io-threads-do-reads yes # 是否对读操作也使用多线程(默认 no)
|
||||
```
|
||||
|
||||
> [!warning] 版本要求
|
||||
> `io-threads` 和 `io-threads-do-reads` 是 Redis 6.0 新增的配置项,6.0 之前的版本不支持,配置后会报错。
|
||||
|
||||
| 配置 | 说明 |
|
||||
|------|------|
|
||||
| `io-threads` | 线程数,包含主线程自身。设 4 表示 1 个主线程 + 3 个 IO 线程 |
|
||||
@@ -299,7 +304,7 @@ BIO 线程池在 Redis 启动时就已创建(默认 3 个线程),各自负
|
||||
> - `DEL`:在**主线程**中同步释放内存。如果 Key 是一个包含 100 万元素的 Hash,主线程会被阻塞几毫秒甚至更久
|
||||
> - `UNLINK`:仅在主线程中断开 Key 的引用(O(1)),实际内存释放交给 BIO 的 `lazyfree` 线程异步完成——主线程几乎不受影响
|
||||
>
|
||||
> 生产环境中,删除大 Key **务必使用 `UNLINK`**。
|
||||
> 生产环境中,删除大 Key **务必使用 `UNLINK`**(需 Redis 4.0+,4.0 以下版本只能用 `DEL`,此时应考虑分批删除或在从库执行)。
|
||||
|
||||
## 连接管理
|
||||
|
||||
@@ -323,7 +328,7 @@ flowchart LR
|
||||
| `maxclients` | 10000 | 最大同时连接数,超过后新连接被拒绝 |
|
||||
| `tcp-backlog` | 511 | 内核 TCP 连接队列长度,需与系统 `somaxconn` 对齐 |
|
||||
| `timeout` | 0(不超时) | 客户端空闲多久后断开,0 表示不主动断开 |
|
||||
| `tcp-keepalive` | 300 | TCP 心跳探测间隔(秒),及时发现死连接 |
|
||||
| `tcp-keepalive` | 300(Redis 3.2+) | TCP 心跳探测间隔(秒),及时发现死连接;3.2 之前默认为 0(不检测),易导致连接泄漏 |
|
||||
|
||||
> [!warning] 连接数暴涨的隐患
|
||||
> 连接数过多不仅消耗内存(每个连接约占 ~1KB 结构体 + 输出缓冲区),更重要的是 **epoll 事件通知量增大**,每轮循环需要遍历更多就绪文件描述符,主线程在 IO 上的时间占比上升,直接影响命令处理吞吐量。
|
||||
@@ -415,7 +420,7 @@ flowchart LR
|
||||
> 最坏情况:**fork 瞬间实际可用内存需要为当前数据量的 2 倍**。如果你的 Redis 实例用了 10GB,系统剩余内存不足 10GB,fork 后可能触发 OOM Killer,导致 Redis 进程被系统杀死。
|
||||
>
|
||||
> 生产建议:
|
||||
> - `maxmemory` 设为物理内存的 **60%~70%**,预留 fork 空间
|
||||
> - 设置 `maxmemory`(如 `maxmemory 8gb`),并将其控制在物理内存的 **60%~70%**,为 fork COW 预留空间
|
||||
> - 监控 `latest_fork_usec` 指标,超过 1ms 说明数据量大、fork 开销高
|
||||
> - 写入密集时避免同时触发 RDB 和 AOF rewrite
|
||||
|
||||
|
||||
Reference in New Issue
Block a user