先说结论

Redis 快来自内存访问、高效数据结构、事件驱动 I/O、紧凑协议和较少上下文切换。经典核心命令主要串行执行,避免对共享数据结构频繁加锁;现代版本可以用 I/O 线程处理网络读写,但命令语义仍主要在核心线程执行。

“单线程”不是整个 Redis 进程只有一个线程,持久化、异步释放、复制和 I/O 都可能使用其他线程。

关键机制

事件循环用 I/O 多路复用管理大量连接,只有就绪连接才进入处理。数据结构会根据元素数量和内容选择紧凑编码,在内存、CPU 和操作复杂度之间折中。

一条命令的延迟组成

客户端排队 -> 网络往返 -> 事件循环读取/解析
        -> 命令执行 -> 内存访问/数据结构操作
        -> 响应编码 -> 网络发送 -> 客户端处理

Redis 在内存中操作避免了磁盘随机读的主要延迟,但命令仍要排队。单个慢命令、网络大包、CPU 满、fork 带来的页表开销、持久化 I/O 抖动都可能让后续轻量命令等待,因此看平均 QPS 不能证明 P99 正常。

事件循环与 I/O 多路复用

Redis 使用 epoll、kqueue 等机制等待大量 socket 的可读可写事件,一个或少量线程只处理就绪连接。相比“每连接一个线程”,它避免大量线程栈、上下文切换和锁竞争,特别适合命令短、状态集中在内存的数据服务。

核心命令执行通常按事件循环串行化,因此单条命令的时间复杂度非常重要。GETSET 是近似 O(1),但全量集合操作、阻塞 Lua、一次返回百万元素的 range 都会独占执行时间。

数据结构与编码

Redis 对 String、Hash、List、Set、ZSet 等提供语义级结构,内部会按元素数量和大小选择紧凑或通用编码。例如小 Hash 可以使用连续紧凑结构降低指针和分配成本,超过阈值后转为哈希表。阈值和实现随版本变化,业务侧应关注操作复杂度、Key 规模与内存,不应依赖某个内部编码名写逻辑。

选择合适类型也避免应用层重复序列化:排行榜用 ZSet 的 score 范围查询,计数用原子 INCR,字段更新用 Hash。把所有内容塞进巨大 JSON String,会失去局部更新、过期和复杂度控制能力。

I/O 线程不等于命令多线程

现代 Redis 可使用 I/O 线程并行处理网络读写、协议解析或响应发送,降低高带宽下网络处理负担;多数命令执行语义仍集中,避免共享数据结构并发锁。是否开启、线程数多少取决于网络包大小、CPU 和版本,应压测验证。

后台还有 AOF 重写、RDB 生成、异步删除、复制等线程或子进程活动。因此“Redis 单线程”应准确表述为“核心命令处理模型主要串行”,而不是“进程只有一个线程”。

如何定位慢

先看客户端 P99 与连接排队,再检查 Redis SLOWLOG、latency monitor、命令统计、CPU、网络、内存碎片、持久化和 fork。Slowlog 记录的是命令执行时间,不包含网络排队;客户端慢而 slowlog 正常时,可能是网络、连接池、事件循环被大响应或客户端自身问题。

典型治理包括拆大 Key、限制单次返回、用 SCAN 代替 KEYS、把复杂计算迁到异步任务、减少阻塞 Lua、压缩/分页大值,并为慢依赖设置超时和降级。

性能边界

大 Key、全量遍历、复杂 Lua 和高复杂度命令会长时间占用核心执行线程,阻塞其他请求。网络带宽、CPU 单核、内存延迟和持久化抖动都可能成为瓶颈。

容易踩坑的地方

内存数据库并不天然没有延迟;排队、网络、fork、页缺失和操作复杂度仍会造成长尾。开启 I/O 线程也不能加速 CPU 密集的命令执行。

常见问题

追问:为什么不用每个请求一个线程?

Redis 命令通常很短,事件循环避免锁和线程切换成本;需要更多 CPU 时通过分片把请求分散到多个实例或主节点。

追问:Redis 可以执行耗时计算吗?

不适合。即使 Lua 能在服务端减少网络往返,复杂/长时间脚本仍阻塞其他请求。计算密集任务应移到应用或专用计算系统。

追问:P99 高但慢日志为空怎么排查?

检查客户端连接池等待、网络重传、服务端事件循环排队、大响应包、fork/持久化抖动和 CPU 抢占;slowlog 只覆盖命令自身执行阶段。

追问:Redis 内存碎片高一定要重启吗?

不一定。先看实际可用内存、分配器、负载与版本能力;重启有可用性风险。应评估主动碎片整理、扩容或数据模型优化,并在演练后操作。