Theory · Redis · Threading
命令执行永远单线程,性能手段都在单线程之外 —— IO 多路复用、bio 后台线程、6.0+ IO 多线程、8.0 异步 IO 重写
内存操作 + 高效结构 + epoll 事件驱动 + 单线程无锁无切换,四要素撑起单实例 10w+ QPS
慢命令阻塞全实例是唯一软肋;lazyfree/bio 把"释放与落盘"挪出主线程,io-threads 把"网络读写"并行化
pipeline 只省 RTT、MULTI 执行期原子、Lua 真原子可编程——面试必考的层次差异
Why So Fast
数据全在内存,读写是纳秒~微秒级;对比磁盘数据库的毫秒级寻道,差 4–6 个数量级。这是"快"的地基,其他都是放大器。
SDS、dict 渐进式 rehash、skiplist、listpack/quicklist——每种类型按数据规模切换最优编码(详见 data-structures deck),操作复杂度 O(1)~O(logN)。
单线程监听成千上万连接,哪个 fd 就绪处理哪个——事件驱动、非阻塞,连接数与线程数解耦,C10K 问题不存在。
命令串行执行:没有锁竞争、没有上下文切换、没有死锁;所有命令天然原子,实现简单且行为可预测。官方 FAQ 明确:瓶颈在内存与网络带宽,不在 CPU,多核对单实例无益。
ae Event Loop · src/ae.c
Command Lifecycle
The Cost
KEYS * 全库扫描;大集合全量读取(SMEMBERS 百万元素 / LRANGE 0 -1 / 大 HGETALL);O(N) 的 SORT;复杂 Lua 脚本;大 key 的同步删除(DEL 百 MB value = 主线程直接 free)。任一都会让所有后续请求排队,P99 直接起飞。
slowlog-log-slower-than 默认 10000(微秒 = 10ms;设 0 记录一切、-1 关闭);slowlog-max-len 默认 128 条,内存中环形 FIFO,重启丢失。SLOWLOG GET / LEN / RESET。配套 LATENCY 事件监控(latency-monitor-threshold)。
| 慢命令 | 替代方案 |
|---|---|
KEYS pattern | SCAN 游标分批(增量遍历,O(1) 单步,不阻塞;注意 rehash 期间可能重复返回 key) |
| 大集合全量读 | SSCAN / HSCAN / ZSCAN 分批;业务侧分页;把大 key 拆桶(回到 listpack 编码区间) |
| 同步 DEL 大 key | UNLINK / FLUSHALL ASYNC(下一页) |
| 复杂 Lua | 拆短脚本;只做必要的 CAS 逻辑;监控 busy-reply-threshold 触发情况 |
Background Threads · src/bio.c
DEL 是主线程同步 free——100MB 的 hash 释放直接卡住全部请求。UNLINK 只把 key 从键空间摘除(O(1)),真正释放交给 BIO_LAZY_FREE 线程。判定阈值:对象"够大才值得异步"(源码 LAZYFREE_THRESHOLD=64:集合类元素数 >64 才丢后台,小对象直接删反而省去线程协调)。
FLUSHALL ASYNC / FLUSHDB ASYNC:共享对象引用计数清零 + 丢后台清空。
① BIO_CLOSE_FILE:后台关闭文件 fd(RDB/AOF 重写完成后的旧文件);② BIO_AOF_FSYNC:AOF 的 everysec fsync 在后台执行(主线程只管 write,磁盘慢也不卡命令);③ BIO_LAZY_FREE:异步释放大对象内存(UNLINK/lazyfree 配套)。
| lazyfree 配置(全默认 no) | 作用 |
|---|---|
lazyfree-lazy-user-del | 让 DEL 也走 UNLINK 语义——生产建议 yes(业务零改造获得异步删除) |
lazyfree-lazy-expire | 过期 key 的释放异步化(防过期风暴卡主线程) |
lazyfree-lazy-eviction | 内存淘汰的释放异步化(配合 maxmemory 使用) |
lazyfree-lazy-server-del | 隐式删除异步化:RENAME 源 key、SETRANGE 覆盖大 value 等内部删除路径 |
lazyfree-lazy-user-flush | FLUSHALL/FLUSHDB 默认按 ASYNC 处理 |
Threaded I/O · Redis 6.0+
| 配置 | 默认值 | 说明 |
|---|---|---|
io-threads | 1(= 关闭) | IO 线程数(含主线程);建议 2–4,官方提醒不超过核数的 3/4;高连接大吞吐场景收益明显 |
io-threads-do-reads | no | 读方向默认不并行:官方 redis.conf 注释——多线程读"通常无收益",仅 TLS 解析等场景经压测后开启 |
① Redis 的瓶颈是内存和网络带宽,CPU 本就空闲;② 命令并行执行必须给所有数据结构加锁——锁竞争、死锁、粒度设计全是成本,还会破坏"每条命令原子"的免费语义;③ 无锁单线程让实现简单、行为可复现。这是 redis.conf 注释与作者多次阐述的官方口径。
海量连接 + 大批量请求(pipeline 打包大报文)、大 value 读写——网络收发与协议解析占了大头。典型错误认知:"QPS 不够就开 io-threads":小命令短连接场景下收益趋近于零,先看 slowlog 和网络带宽。
How Threaded I/O Works
Atomicity Ladder
| 手段 | 省什么 | 原子性 | 适用与短板 |
|---|---|---|---|
| pipeline | 网络 RTT:攒 N 条命令一次发送一次收 | 无:执行期可能插入其他客户端的命令(服务端逐条执行) | 纯性能优化(批量导入/读);客户端排队,服务端需缓冲回复(大 pipeline 占内存) |
| MULTI/EXEC | RTT(配合 pipeline 可一次提交)+ 执行期隔离 | 执行期原子:EXEC 后整批连续执行不被插入;但不回滚(见下页) | 一组写要么都发要么不发;无中间逻辑,不能"读了再决定写什么" |
| Lua 脚本 | RTT + 执行期隔离 + 可编程 | 真原子:脚本作为一条命令串行执行,期间不插入任何其他命令 | CAS/条件写/读后写的唯一正解;代价:脚本慢 = 阻塞全实例(必须短小) |
WATCH stock + MULTI/DECR/EXEC:冲突则整批返回 nil 重试(乐观锁,高竞争下重试风暴);Lua 直接 if stock > 0 then DECR 一次成功——这就是"事务不能包含读后写逻辑,Lua 可以"的具体含义。
pipeline + MULTI:一次 RTT 提交整个事务(事务入队回复也批量回)。Lua 必要时内部仍可用 pipeline 客户端预载。Jedis/Lettuce 等客户端的"事务"API 默认就是 MULTI 包 pipeline。
Transactions · MULTI / EXEC / WATCH
入队错误(命令不存在/参数个数错):入队时就报错,EXEC 直接拒绝整批(返回 EXECABORT,Redis 2.6.5+ 行为)。执行错误(如对 string 执行 LPUSH):EXEC 照常运行,该条失败并返回错误,其余命令继续执行、已执行的不回滚。
antirez:执行期错误是编程 bug,开发环境就该暴露,回滚救不了生产逻辑;且实现回滚要给每条命令维护 undo,复杂化代码、拖慢常态路径。Redis 的选择:"简单 + 快",把正确性责任交给应用层。
| WATCH 乐观锁 | 机制 |
|---|---|
| 监视 | WATCH key:在 db 的 watched_keys 结构上登记(key → 监视它的客户端链表) |
| 污染 | 任何客户端对该 key 的写命令触发 touchWatchedKey:把监视者标记 CLIENT_DIRTY_CAS |
| 裁决 | EXEC 时检查标记:被污染 → 整个事务不执行,返回 nil(客户端约定俗成重试) |
| 生命周期 | EXEC/DISCARD/UNWATCH/连接断开 都会清空所有 WATCH |
Programmability · EVAL / Functions
脚本作为一条命令串行执行,执行期间不插入任何其他命令——这是"真原子"的实现方式,也意味着脚本跑多久,全实例停多久。busy-reply-threshold(7.0 由 lua-time-limit 改名)默认 5000ms:超时后 Redis 开始对其他请求回复 BUSY 错误。
解法:SCRIPT KILL 只能杀还没写过数据的脚本(否则数据不完整);已写过的只能 SCRIPT SHUTDOWN NOSAVE 关实例。所以脚本必须短小、避免循环扫全库。
默认整段复制(script replication):主库把脚本原文发给从库各跑一遍——要求脚本确定:同样的输入必须产生同样的写。脚本内随机命令受严格限制(TIME/RANDOMKEY 等调用后禁止再发写命令)。EVALSHA 按 SHA1 调用避免每次传全文;7.0 起 EVAL_RO 只读脚本可跑在副本。
| 7.0 Functions | 说明 |
|---|---|
FUNCTION LOAD | 把函数库注册到服务端并持久化(随 RDB/AOF 保存),取代"客户端每次传脚本"的模式——脚本库版本化管理 |
| 按效果复制 | Functions 复制执行产生的写命令(effect replication)而非脚本原文,对从库带宽更友好 |
| 定位 | 官方说法:EVAL 的继任者(server-side 程序化能力);Lua 引擎本身 7.0+ 已可选支援 Functions/RESP3(引擎可插拔) |
Protocol · REdis Serialization Protocol
5 种类型前缀:+ 简单字符串、- 错误、: 整数、$ bulk string、* 数组。文本协议、人可读、解析极快——协议简单本身就是性能设计的一部分。
SET k v 的请求与响应: *3\r\n$3\r\nSET\r\n$1\r\nk\r\n$1\r\nv\r → +OK\r\n
新增类型:% map、~ set、# bool、, double、_ null、> push。最大意义:带外推送——客户端缓存(client-side caching)的失效通知走独立 push 通道,不再混在回复流里;ZRANGE 也能直接回 map 语义。
As of Redis 8.x · 2025
| 版本 | 线程模型相关变化 |
|---|---|
| 6.0(2020) | 引入 threaded I/O:io-threads 可并行网络读(默认关)与写(默认开);命令执行单线程不变 |
| 7.0/7.2/7.4 | 线程模型基本未动;7.0 Functions / Multi-Part AOF 改变的是持久化与编程面;7.4 引入 hash field TTL(HEXPIRE) |
| 8.0(2025-05 GA) | 全新异步 I/O threading 实现(官方 8.0-M03 公布、GA 保留):参数名 io-threads 与默认值 1 不变;官方博客称性能为历代最佳(30+ 项优化,命令最高提速 87%) |
| 8.2(2025-08 GA) | 持续性能线:官方称命令再快 ~35%、吞吐/内存效率最高 +49%;线程模型仍为"IO 可并行、执行单线程" |
命令执行至今仍是单线程串行——所有原子性语义(INCR、事务、Lua)不依赖任何锁。Redis 8 的新 IO 线程是异步化网络栈,不是命令并行。面试答"Redis 是单线程吗"要分两层:执行层单线程(永远),网络/落盘/释放层多线程(可开)。
License 变化(7.4 起 RSALv2/SSPLv1,8.0 起部分回归 AGPL)与线程模型无关,别混着答。截至 2026-08,开源版最新为 8.x 系(8.2 之后仍有新版本线发布),本 deck 结论对 7.x/8.x 均成立。
Interview QA · 1/2
指命令执行永远单线程(6.0 至 8.x 不变)。外围早就是多线程:bio 三线程(关文件/AOF fsync/lazy free)、io-threads 网络线程、RDB/AOF 重写子进程、8.0 异步 IO 线程。原子性来自"无第二执行线程",不是锁。
内存操作(地基)→ 高效数据结构(O(1)~O(logN))→ IO 多路复用(单线程管海量连接)→ 单线程无锁无切换(白送原子性)。补定量:单实例 10w+ QPS;官方口径瓶颈在内存与网络带宽,不在 CPU。
①RESP 进内核缓冲 → ②epoll 读就绪、readQueryFromClient 解析 → ③processCommand/call 单线程执行 → ④写命令进 aof_buf 与复制流 → ⑤beforeSleep 统一写回 → ⑥回到 epoll_wait。io-threads 只并行②⑤,③永远串行。
SLOWLOG:slowlog-log-slower-than 默认 10000μs(10ms)、max-len 128,SLOWLOG GET 看慢命令原文;LATENCY 事件监控粒度更粗;INFO commandstats 按命令统计耗时分布。根治:SCAN 替代 KEYS、拆大 key、UNLINK 删除、拆短 Lua。
UNLINK 只从键空间摘除(O(1)),释放丢 BIO_LAZY_FREE 线程。但源码有判定:集合类元素 ≤64 的小对象直接同步删更快——线程协调成本高于释放成本,所以 UNLINK 内部对小对象走同步路径。
BIO_CLOSE_FILE:RDB/AOF 重写后关旧文件 fd;BIO_AOF_FSYNC:everysec 的 fsync 在后台执行,磁盘慢不卡命令(配合 aof 的 2s 延迟机制);BIO_LAZY_FREE:UNLINK/过期/淘汰的大对象异步释放。共 3 个(BIO_NUM_OPS=3)。
原子。多线程只覆盖网络读解析与写回;命令执行仍由主线程串行完成。线程间用屏障(自旋等待)同步,IO 线程不碰共享数据结构——不需要锁,语义与纯单线程一致。
默认 1(关闭);io-threads-do-reads 默认 no(读方向官方注释说通常无收益)。适合海量连接 + pipeline 大报文 + 大 value 的网络密集场景,建议 2–4 个且不超过核数 3/4;小命令场景收益趋近零,先查 slowlog 再考虑开线程。
Interview QA · 2/2
不支持。入队错误(命令不存在/参数错)在 EXEC 时拦截整批,返回 EXECABORT(2.6.5+);执行错误(类型错)只让该条失败,其余继续、已执行不撤销。官方理由:执行错误是编程 bug,回滚救不了还拖慢常态路径。
WATCH 把客户端登记到 watched_keys;期间任何客户端写该 key 会把监视者标记为 DIRTY_CAS;EXEC 检查到污染就不执行整个事务、返回 nil,客户端重试。EXEC/DISCARD/连接断开自动清空全部 WATCH。
脚本作为一条命令执行,期间不插入其他命令——原子与阻塞同源。超过 busy-reply-threshold(默认 5000ms)后其他客户端收到 BUSY;SCRIPT KILL 只能杀未写过数据的脚本,已写入的只能 SHUTDOWN NOSAVE,所以脚本必须短小。
EVAL 默认整段复制:主库发脚本原文、从库各跑一遍,要求脚本确定(调用随机命令后禁止写)。7.0 Functions 改为按效果复制产生的写命令,且函数库随 RDB/AOF 持久化、可版本管理——这是官方定位的 EVAL 继任者。
pipeline 纯客户端攒批省 RTT,服务端逐条执行、中间可插其他命令——不原子;MULTI 是服务端排队、EXEC 窗口内连续执行——执行期原子。可以组合:pipeline 包裹整段 MULTI 命令一次 RTT 提交整个事务。
每轮事件循环收尾:flushAppendOnlyFile(everysec 策略在此落盘)、handleClientsWithPendingWrites 把回复写回(io-threads 可并行)、activeExpireCycle SLOW 模式过期清理、释放解除阻塞的客户端等——它是主线程的"批处理窗口"。
周期时间事件,默认每秒 10 次(hz=10,可 1–500):过期 FAST 清理、dict rehash 1ms 兜底、客户端超时检查、复制/集群定时任务、内存统计。调大 hz = 清理更勤、统计更实时,但 CPU 空转开销上升。
新增 % map、~ set、# bool、, double、_ null 与 > push。最大价值是 push 类型:客户端缓存(client-side caching)的失效通知走独立带外通道,不混入命令回复流。HELLO 3 协商,不协商永远 RESP2。
Related · Cross-links
data-structures · 对象系统与底层数据结构(快四要素之"高效结构"的全量展开)
persistence · RDB / AOF / 混合持久化(aof_buf / bio fsync / fork 与主线程的关系)
cache-patterns · 缓存模式与一致性
distributed-lock · 分布式锁(Lua 真原子是锁实现的地基)
GMP 调度器:同为"事件驱动 + 用户态调度",调度点思想对照(GMP 的 sysmon ↔ Redis 的 serverCron)
Kafka Internals:单线程/少线程 + 顺序 IO 的性能哲学对照
OS · I/O 模型与 epoll:Redis 事件循环的内核底座(epoll_wait 与就绪链表)
通用线索:Reactor 模式、把慢操作异步化(bio ↔ Go 的 netpoller)、以单线程换无锁语义
References
本 deck 全部结论可溯源至下列一手材料——数字与版本结论以原文为准。
| redis.io/docs/latest/develop/use/redis-io-threading/(及 threading 相关 FAQ) | io-threads / io-threads-do-reads 语义与建议;单线程官方口径 |
| github.com/redis/redis · redis.conf(8.x)io-threads 注释 | "By default threading is disabled"、读方向多线程通常无收益的官方表述 |
| redis.io/blog/redis-8-0-m03-is-out-... · redis.io/blog/redis-8-ga/ | Redis 8 全新异步 I/O threading 实现、io-threads 默认 1、GA 性能数字(2025-05) |
| redis.io/docs/latest/operate (persistence) / programmability / protocol-spec | appendfsync 与 aof_buf、EVAL 原子性与 busy-reply-threshold、RESP2/RESP3 规范 |
| github.com/redis/redis · src/ae.c / ae_epoll.c / ae_kqueue.c | aeMain / aeProcessEvents / beforeSleep 调用时序;按平台多路复用实现 |
| github.com/redis/redis · src/bio.h / bio.c / lazyfree.c | BIO_NUM_OPS=3 三线程分类;LAZYFREE_THRESHOLD=64 异步删除判定 |
| github.com/redis/redis · src/networking.c / server.c / transactions.c | readQueryFromClient、processCommand/call、WATCH 的 watched_keys 与 DIRTY_CAS |
| github.com/redis/redis · 7.0 00-RELEASENOTES(Functions 部分) | FUNCTION LOAD、effect replication、busy-reply-threshold 改名 |