Skip to content
Charles Shao
Go back

Redis 深挖(三)· 线程模型与性能

views

Redis 最反直觉的一点:它的命令执行是单线程的,却能扛十万级 QPS。 很多人由此得出两个错误结论——「单线程所以是瓶颈」和「多核机器上 Redis 用不满 CPU 是浪费」。真相是:单线程恰恰是 Redis 快且稳的原因之一(无锁、无上下文切换、天然原子),而它真正的软肋不是「不够快」,而是一条慢命令会独占那唯一的执行线程、拖垮全局

本篇拆开 Redis 的线程模型,再把「怎么让它更快、别让它变慢」的工具箱过一遍。

Redis 深挖三篇:本篇是第三篇「线程模型与性能」,另外两篇是 数据结构与内存模型持久化与高可用,都是 缓存系列开篇 的下钻。单线程 + IO 多路复用的底层模型与 连接与 I/O 模型 讲的 Reactor 是同一套思想。

TL;DR

Table of contents

Open Table of contents

1. 线程模型:单线程执行 + IO 多线程 + bio

Redis 6.0+ 线程模型:左侧海量客户端连接进入;主流程是 ① epoll 事件循环(主线程监听 N 个连接等就绪)② IO 线程并行 read+parse(只做网络读与协议解析)③ 主线程串行执行命令(单线程无锁无上下文切换纯内存)④ IO 线程并行 write 响应;右侧 bio 后台线程异步做 close(fd)、fsync(AOF)、lazyfree/UNLINK,主线程把耗时操作卸载给它

「单线程」指命令由一个主线程串行执行(因此天然原子);IO 多线程只并行网络收发与解析,bio 后台线程卸载耗时操作。慢命令会独占唯一的执行线程、拖垮全局。

一次请求的生命周期:

  1. epoll 事件循环(主线程):用 IO 多路复用监听成千上万个连接,谁有数据就绪就处理谁,O(1) 感知就绪。这就是 连接与 I/O 模型 里的 Reactor 模式。
  2. 读 + 解析:把请求字节读进来、按 RESP 协议解析成命令。
  3. 执行命令(单线程串行):在唯一的主线程里串行执行,纯内存操作。因为串行,所以任意单条命令天然原子,不需要锁。
  4. 写回响应:把结果编码写回连接。

一个贯穿全篇的推论:既然只有一个线程执行命令,任何一条慢命令都会独占这唯一资源、阻塞其后排队的所有命令。 Redis 的性能问题,绝大多数最终都能归到「某条命令在单线程上跑太久」。

2. IO 多线程与 bio 后台线程

为什么「单线程」还要引入多线程? 因为随着吞吐上升,瓶颈从「命令执行」转移到了「网络收发与协议解析」这些内核/用户态拷贝上。

3. pipeline:把 N 次 RTT 收敛为 1 次

单条命令的耗时,很多时候不在 Redis 执行(微秒级),而在网络往返(RTT)。逐条命令「发一条、等一条回」,N 条就是 N 个 RTT。

pipeline 对比:上半无 pipeline,客户端 SET k1 等 +OK、SET k2 等 +OK……N 条命令 N 个 RTT;下半 pipeline,客户端批量发送 SET k1..kN 不等回包,Redis 串行执行 N 条后一次返回 N 个 +OK,只需 1 个 RTT

pipeline 把 N 条命令一次发出、一次收回,省的是网络 RTT 而非执行耗时;命令仍在主线程串行执行,与事务/Lua 的原子性无关。

要点:

4. 内存淘汰与过期删除

Redis 是有限内存,两套机制管内存:达到 maxmemory 时的淘汰,和给 key 设了 TTL 后的过期删除

内存治理:左侧内存淘汰,写命令到达检查 used_memory>maxmemory,超了则按 maxmemory-policy 采样 maxmemory-samples 个 key 选淘汰候选、逐出释放后再写入;右侧过期删除双管齐下,惰性删除(访问 key 时检查 TTL 已过期才删,省 CPU 但冷 key 长期占内存)+ 定期删除(activeExpireCycle 每 100ms 抽样带 TTL key,过期比例高则继续扫,限时执行);底部列 8 种淘汰策略

写入触发 maxmemory 检查后按策略采样淘汰;过期删除靠惰性(被访问才删)+ 定期(抽样主动删)互补,都漏掉的过期 key 最终由淘汰策略回收。

4.1 内存淘汰:8 种 policy

写命令让 used_memory 超过 maxmemory 时,按 maxmemory-policy 淘汰:

关键:LRU/LFU 都是采样近似——Redis 不维护全局有序链表(太耗内存),而是每次随机采样 maxmemory-samples(默认 5)个 key,从中挑最该淘汰的。调大采样更精准但更耗 CPU。

4.2 过期删除:惰性 + 定期

两者互补:惰性兜住「被访问到的」,定期清理「没被访问、白占内存的」;都漏掉的,最终由 §4.1 的淘汰策略回收。

5. 慢查询与阻塞命令:单线程的致命伤

单线程模型下,一条慢命令会阻塞所有其他客户端。头号地雷:

诊断SLOWLOGslowlog-log-slower-than 设阈值,SLOWLOG GET 看慢命令历史)、INFO commandstatsLATENCY 命令族(LATENCY LATEST/LATENCY DOCTOR)定位延迟毛刺来源(含 fork、AOF 刷盘、大 key 释放等)。

删一个大集合,DEL 会在主线程同步释放它的所有元素(O(N)),大 key 能卡住整个 Redis。改用 UNLINK:它只在主线程里把 key 从键空间摘除(O(1) 感知),真正的内存释放交给 bio 后台线程 lazyfree 异步做。FLUSHDB/FLUSHALL 也有 ASYNC 变体。

可配 lazyfree-lazy-user-del yesDEL 行为等同 UNLINK,以及 lazyfree-lazy-expire/lazyfree-lazy-eviction 让过期/淘汰时的释放也走后台。大 key 的成因见 数据结构与内存模型 §11

7. 热 key:定位与打散

热 key 是单个 key 被极高频访问(爆款、热门 campaign),把承载它的单个节点单线程打满,殃及同节点其他 key(详见 缓存开篇 §2.7)。单线程模型让这个问题格外尖锐——一个热 key 就能吃满一个核。

8. 性能诊断指标

9. AdTech 落地与生产红线

竞价读预算/频控/元数据都在 tmax(80–150ms)关键路径上,Redis 延迟零容忍。而单线程意味着任何一条慢命令都是全节点级事故

生产红线清单

参考


views
Share this post on:

Previous Post
缓存一致性深挖:双删、Binlog 订阅与多级失效
Next Post
Redis 深挖(二)· 持久化与高可用