Redis 最反直觉的一点:它的命令执行是单线程的,却能扛十万级 QPS。 很多人由此得出两个错误结论——「单线程所以是瓶颈」和「多核机器上 Redis 用不满 CPU 是浪费」。真相是:单线程恰恰是 Redis 快且稳的原因之一(无锁、无上下文切换、天然原子),而它真正的软肋不是「不够快」,而是一条慢命令会独占那唯一的执行线程、拖垮全局。
本篇拆开 Redis 的线程模型,再把「怎么让它更快、别让它变慢」的工具箱过一遍。
Redis 深挖三篇:本篇是第三篇「线程模型与性能」,另外两篇是 数据结构与内存模型 与 持久化与高可用,都是 缓存系列开篇 的下钻。单线程 + IO 多路复用的底层模型与 连接与 I/O 模型 讲的 Reactor 是同一套思想。
TL;DR
- 「单线程」指命令执行:一个主线程串行跑所有命令,因此天然原子、无需加锁——这也是
INCR、SETNX、Lua 脚本能当并发原语用的根基。 - 快的三块基石:① 纯内存操作;② 单线程省去锁竞争与上下文切换;③ 基于 epoll 的 IO 多路复用 Reactor 事件循环,一个线程管海量连接。
- IO 多线程(6.0+):
io-threads只并行做网络读写与协议解析,命令仍单线程执行;高网络吞吐场景才需要开。 - bio 后台线程:异步做
close(fd)、fsync(AOF)、lazyfree(UNLINK 大 key),把耗时操作从主线程卸载。 - pipeline:把 N 条命令攒批一次发、一次收,N 次 RTT → 1 次;省的是网络往返、不是执行耗时,也不等于事务。
- 内存淘汰:
maxmemory+ 8 种maxmemory-policy;LRU/LFU 都是采样近似(maxmemory-samples),非全局排序。 - 过期删除:惰性(访问时才删)+ 定期(
activeExpireCycle抽样删)双管齐下。 - 慢命令是头号杀手:
KEYS、大HGETALL/SMEMBERS、大范围ZRANGE都是 O(N),单线程下会阻塞所有人——用SCAN/HSCAN替代。 - 删大 key 用
UNLINK(后台异步)而非DEL(主线程同步阻塞)。 - 热 key:
--hotkeys/ 监控定位,靠多副本打散 + L1 本地缓存兜底。
Table of contents
Open Table of contents
1. 线程模型:单线程执行 + IO 多线程 + bio
「单线程」指命令由一个主线程串行执行(因此天然原子);IO 多线程只并行网络收发与解析,bio 后台线程卸载耗时操作。慢命令会独占唯一的执行线程、拖垮全局。
一次请求的生命周期:
- epoll 事件循环(主线程):用 IO 多路复用监听成千上万个连接,谁有数据就绪就处理谁,O(1) 感知就绪。这就是 连接与 I/O 模型 里的 Reactor 模式。
- 读 + 解析:把请求字节读进来、按 RESP 协议解析成命令。
- 执行命令(单线程串行):在唯一的主线程里串行执行,纯内存操作。因为串行,所以任意单条命令天然原子,不需要锁。
- 写回响应:把结果编码写回连接。
一个贯穿全篇的推论:既然只有一个线程执行命令,任何一条慢命令都会独占这唯一资源、阻塞其后排队的所有命令。 Redis 的性能问题,绝大多数最终都能归到「某条命令在单线程上跑太久」。
2. IO 多线程与 bio 后台线程
为什么「单线程」还要引入多线程? 因为随着吞吐上升,瓶颈从「命令执行」转移到了「网络收发与协议解析」这些内核/用户态拷贝上。
- IO 多线程(Redis 6.0+,
io-threads):让多个线程并行做网络 read/write 与协议 parse,但命令执行仍在主线程单线程完成。所以它不破坏「单线程原子」的语义,只是把 IO 这段并行化。只有网络吞吐确实是瓶颈时才建议开(io-threads 4之类),命令 CPU 密集时开了也没用。 - bio(Background I/O)后台线程:把会阻塞主线程的慢操作卸载出去——
close(fd):关闭文件描述符;fsync(AOF):AOF 刷盘不卡主线程;lazyfree:UNLINK大 key、FLUSHDB ASYNC时,在后台异步释放内存(对比DEL在主线程同步释放,见 §6)。
3. pipeline:把 N 次 RTT 收敛为 1 次
单条命令的耗时,很多时候不在 Redis 执行(微秒级),而在网络往返(RTT)。逐条命令「发一条、等一条回」,N 条就是 N 个 RTT。
pipeline 把 N 条命令一次发出、一次收回,省的是网络 RTT 而非执行耗时;命令仍在主线程串行执行,与事务/Lua 的原子性无关。
要点:
- 省 RTT 不省执行:命令还是在主线程一条条串行跑,pipeline 只是攒批发送、攒批收结果。
- 不等于事务:pipeline 中间可以穿插其他客户端的命令,没有原子性;要原子用
MULTI/EXEC或 Lua。 - 批量别过大:单批太大撑爆响应缓冲、拉长该客户端延迟,还让这批命令长时间占用主线程。经验值每批几十到几百条。
4. 内存淘汰与过期删除
Redis 是有限内存,两套机制管内存:达到 maxmemory 时的淘汰,和给 key 设了 TTL 后的过期删除。
写入触发 maxmemory 检查后按策略采样淘汰;过期删除靠惰性(被访问才删)+ 定期(抽样主动删)互补,都漏掉的过期 key 最终由淘汰策略回收。
4.1 内存淘汰:8 种 policy
写命令让 used_memory 超过 maxmemory 时,按 maxmemory-policy 淘汰:
noeviction(默认):不淘汰,写命令直接报错(读仍可)。allkeys-lru/volatile-lru:近似 LRU,前者面向全体 key、后者只在设了过期时间的 key 里挑。allkeys-lfu/volatile-lfu:近似 LFU(按访问频率),用 morris 计数器 + 时间衰减估频次,适合热点稳定的场景。allkeys-random/volatile-random:随机淘汰。volatile-ttl:优先淘汰最快过期的。
关键:LRU/LFU 都是采样近似——Redis 不维护全局有序链表(太耗内存),而是每次随机采样 maxmemory-samples(默认 5)个 key,从中挑最该淘汰的。调大采样更精准但更耗 CPU。
4.2 过期删除:惰性 + 定期
- 惰性删除(被动):访问某个 key 时才检查它是否过期,过期就删。省 CPU,但没被访问到的过期 key 会一直占内存。
- 定期删除(主动,
activeExpireCycle):每隔 100ms 抽样各 db 里带 TTL 的 key,删掉其中过期的;若某轮过期比例高就继续多扫几轮,但限时执行避免自己变成慢操作。
两者互补:惰性兜住「被访问到的」,定期清理「没被访问、白占内存的」;都漏掉的,最终由 §4.1 的淘汰策略回收。
5. 慢查询与阻塞命令:单线程的致命伤
单线程模型下,一条慢命令会阻塞所有其他客户端。头号地雷:
KEYS *:O(N) 遍历整个键空间,百万 key 能卡几百毫秒——生产严禁,用SCAN游标遍历替代。- 大集合的一次性操作:
HGETALL/SMEMBERS/LRANGE 0 -1对 bigkey 是 O(N),用HSCAN/SSCAN或只取需要的字段(HMGET)。 - 大范围
ZRANGE/SORT/ZUNIONSTORE:元素多时代价陡增。 - 复杂 Lua 脚本:脚本执行期间独占主线程,别在脚本里干重活或死循环。
诊断:SLOWLOG(slowlog-log-slower-than 设阈值,SLOWLOG GET 看慢命令历史)、INFO commandstats、LATENCY 命令族(LATENCY LATEST/LATENCY DOCTOR)定位延迟毛刺来源(含 fork、AOF 刷盘、大 key 释放等)。
6. 大 key 删除:DEL vs UNLINK
删一个大集合,DEL 会在主线程同步释放它的所有元素(O(N)),大 key 能卡住整个 Redis。改用 UNLINK:它只在主线程里把 key 从键空间摘除(O(1) 感知),真正的内存释放交给 bio 后台线程 lazyfree 异步做。FLUSHDB/FLUSHALL 也有 ASYNC 变体。
可配
lazyfree-lazy-user-del yes让DEL行为等同UNLINK,以及lazyfree-lazy-expire/lazyfree-lazy-eviction让过期/淘汰时的释放也走后台。大 key 的成因见 数据结构与内存模型 §11。
7. 热 key:定位与打散
热 key 是单个 key 被极高频访问(爆款、热门 campaign),把承载它的单个节点单线程打满,殃及同节点其他 key(详见 缓存开篇 §2.7)。单线程模型让这个问题格外尖锐——一个热 key 就能吃满一个核。
- 定位:
redis-cli --hotkeys(需 LFU 策略)、代理/客户端侧埋点统计、MONITOR采样(有开销,短时用)、监控热点 slot。 - 打散:把
k复制成k:{0..N}多副本,写时全更、读时随机选一个,把压力摊到多个节点。 - L1 本地缓存兜底:热 key 在应用进程内缓存一份、TTL 极短,绝大多数读不出进程——这是最有效的一招(见 本地缓存深挖)。
8. 性能诊断指标
INFO:instantaneous_ops_per_sec(QPS)、used_memory/used_memory_rss、connected_clients、keyspace_hits/keyspace_misses(命中率)、evicted_keys、expired_keys。LATENCY:LATENCY DOCTOR给延迟诊断建议;持久化fork(RDB/AOF 重写,见 持久化与高可用)是延迟毛刺常见来源。redis-benchmark:压测基准。- 把命中率、慢日志、热 key、节点 CPU、
used_memory_rss纳入监控告警。
9. AdTech 落地与生产红线
竞价读预算/频控/元数据都在 tmax(80–150ms)关键路径上,Redis 延迟零容忍。而单线程意味着任何一条慢命令都是全节点级事故。
生产红线清单:
- 主线程严禁 O(N) 命令:
KEYS用SCAN、HGETALL用HSCAN/HMGET、避免大范围ZRANGE/SORT - 大 key 删除用
UNLINK,开lazyfree-lazy-* -
SLOWLOG+LATENCY纳入巡检,抓慢命令与延迟毛刺 - 后台/分析查询走只读从库,别和在线关键路径抢主库
- 合理设
maxmemory+maxmemory-policy,别让写在noeviction下集体报错 - 热 key 主动探测、多副本打散 + L1 本地缓存兜底
- Lua 脚本轻量、限时,别在脚本里干重活
- 关键路径客户端配超时 + 退避重试 + L1 兜底,别把可用性全押在 Redis 上
参考
- Redis 官方文档 · Redis latency troubleshooting:https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/latency/
- Redis 官方文档 · Using Redis as an LRU cache(淘汰策略与近似 LRU/LFU):https://redis.io/docs/latest/develop/reference/eviction/
- Redis 官方文档 · Redis latency monitoring:https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/latency-monitor/
- Redis 官方文档 · Pipelining:https://redis.io/docs/latest/develop/use/pipelining/
- antirez · Lazy Redis is better Redis(lazyfree/UNLINK 设计):http://antirez.com/news/93