Theory · Redis · Threading

单线程模型与高性能

命令执行永远单线程,性能手段都在单线程之外 —— IO 多路复用、bio 后台线程、6.0+ IO 多线程、8.0 异步 IO 重写

为什么快

内存操作 + 高效结构 + epoll 事件驱动 + 单线程无锁无切换,四要素撑起单实例 10w+ QPS

单线程的边界

慢命令阻塞全实例是唯一软肋;lazyfree/bio 把"释放与落盘"挪出主线程,io-threads 把"网络读写"并行化

原子性的三档

pipeline 只省 RTT、MULTI 执行期原子、Lua 真原子可编程——面试必考的层次差异

这份 deck 回答 Redis 面试第一热题"为什么快、是不是单线程"。主线:先给四要素建立框架,再走进 ae 事件循环看一条命令的一生,然后直面单线程的代价——慢命令与内存释放,看 Redis 怎么用 bio 和 lazyfree 补锅,接着拆 6.0 引入、8.0 重写的 IO 多线程,最后把 pipeline、事务、Lua 三种"伪原子/真原子"手段排成层次。所有版本结论以官方文档和 release notes 为准,8.0 的线程模型变化也核实过。

Why So Fast

Redis 为什么快:四要素,缺一不可

① 纯内存操作

数据全在内存,读写是纳秒~微秒级;对比磁盘数据库的毫秒级寻道,差 4–6 个数量级。这是"快"的地基,其他都是放大器。

② 高效数据结构

SDS、dict 渐进式 rehash、skiplist、listpack/quicklist——每种类型按数据规模切换最优编码(详见 data-structures deck),操作复杂度 O(1)~O(logN)。

③ IO 多路复用(epoll/kqueue)

单线程监听成千上万连接,哪个 fd 就绪处理哪个——事件驱动、非阻塞,连接数与线程数解耦,C10K 问题不存在。

④ 单线程:无锁、无切换

命令串行执行:没有锁竞争、没有上下文切换、没有死锁;所有命令天然原子,实现简单且行为可预测。官方 FAQ 明确:瓶颈在内存与网络带宽,不在 CPU,多核对单实例无益。

量级背书:官方 benchmark:普通 Linux 小机器单实例即可 10 万+ QPS(简单 GET/SET,pipeline 下更高)。追问"单线程为什么敢这么设计"→ 答瓶颈论:CPU 永远闲着,加线程只会引入锁复杂度。
为什么快必须按四要素答,只说"内存操作"是不及格的。内存是地基,数据结构保证单次操作复杂度低,epoll 保证一个线程能伺候海量连接,单线程免掉锁和上下文切换还白送原子性。四者共同指向一个事实:瓶颈在内存和网络,不在 CPU,所以官方敢说加 CPU 核对单实例没用。最后背一下量级:普通机器单实例 10 万 QPS 以上,pipeline 更高,这是面试官想听的定量证据。

ae Event Loop · src/ae.c

Reactor 事件循环:主线程的一生

Redis ae 事件循环:文件事件与时间事件 主线程运行 aeMain:以最近到期时间事件为超时调用 epoll_wait,处理就绪文件事件,进入 beforeSleep 完成收尾,再处理到期时间事件 serverCron,循环往复。 处理到期时间事件 回到等待(循环) aeMain · 主线程 while(!stop) aeProcessEvents() 自研事件库:单线程 Reactor epoll_wait(阻塞等待) 超时 = 最近时间事件的到期间隔 按平台选实现:ae_epoll.c / ae_kqueue.c / ae_evport.c / ae_select.c file events(文件事件) 读就绪 → readQueryFromClient 写就绪 → 写回输出缓冲 beforeSleep(每轮收尾钩子) flushAppendOnlyFile(everysec) 写回回复(含 io-threads 并行写) activeExpireCycle SLOW(过期清理) time events(时间事件) serverCron:hz 默认 10(1–500) 过期 FAST 清理 · rehash 兜底 1ms 客户端超时 · 复制与集群定时任务 磁盘/内存统计(INFO 数据来源) 一轮循环:epoll 等待 → 处理文件事件 → beforeSleep → 到期时间事件 → 回到等待 整个循环跑在唯一的主线程上——这就是"Redis 单线程"的本体
Redis 网络层是自研的 ae 事件库,标准 Reactor 模式。主线程循环四步:算出最近时间事件的到期间隔当超时,调 epoll_wait;处理就绪的文件事件,读请求;进 beforeSleep 收尾,包括 AOF 落盘、写回回复、主动过期;最后处理到期时间事件,也就是 serverCron。注意 hz 默认 10,意思是 serverCron 每秒跑十次,可调到 500。这个循环就是"单线程"的全部本体,理解了它,后面讲阻塞和异步化就都有了坐标系。

Command Lifecycle

一条命令从到达 socket 到收到响应

一条命令的生命周期六阶段 客户端发送命令,epoll 通知读就绪,主线程读取并解析 RESP,processCommand 单线程原子执行,写命令进入 aof_buf 与复制流,beforeSleep 阶段把回复写回 socket,客户端收到响应。 ① 客户端发送 RESP2 编码 · 如 SET k v *3\r\n$3\r\nSET\r\n$1\r\nk 进入内核 socket 接收缓冲区 ② 读取 + 解析 epoll 读就绪 → readQueryFromClient RESP 解析为 argv/argc; io-threads 开启时此步可并行 ③ 执行(永远单线程) processCommand → call() 校验:auth / type / maxmemory 慢命令在此阻塞全实例 ④ 传播(写命令) 追加 aof_buf · 写复制流 repl_stream AOF 落盘节奏由 appendfsync 决定; 从库异步收(详见 persistence deck) ⑤ beforeSleep 写回 handleClientsWithPendingWrites 能写直接 write;写不下注册写事件; io-threads 开启时并行写回 ⑥ 客户端收到响应 —— 回到 epoll_wait 等下一轮 全程无锁:③ 执行阶段的原子性由"单线程"这一事实保证,而非任何锁机制
把一条命令的旅程走一遍:客户端按 RESP 协议编码发来,epoll 通知读就绪,主线程读取并解析成参数数组;processCommand 做权限、类型、内存等校验后执行——这一步永远单线程,是原子性的全部来源;写命令同时追加到 aof 缓冲和复制流;回复先进输出缓冲,beforeSleep 时统一写回。io-threads 能并行的只有②的读解析和⑤的写回,③永远串行。面试时强调:原子性不是靠锁,是靠"根本没有第二个执行线程"这个事实。

The Cost

代价:一条慢命令 = 全实例停车

哪些操作会"停车"

KEYS * 全库扫描;大集合全量读取(SMEMBERS 百万元素 / LRANGE 0 -1 / 大 HGETALL);O(N) 的 SORT;复杂 Lua 脚本;大 key 的同步删除(DEL 百 MB value = 主线程直接 free)。任一都会让所有后续请求排队,P99 直接起飞。

发现工具:SLOWLOG

slowlog-log-slower-than 默认 10000(微秒 = 10ms;设 0 记录一切、-1 关闭);slowlog-max-len 默认 128 条,内存中环形 FIFO,重启丢失。SLOWLOG GET / LEN / RESET。配套 LATENCY 事件监控(latency-monitor-threshold)。

慢命令替代方案
KEYS patternSCAN 游标分批(增量遍历,O(1) 单步,不阻塞;注意 rehash 期间可能重复返回 key)
大集合全量读SSCAN / HSCAN / ZSCAN 分批;业务侧分页;把大 key 拆桶(回到 listpack 编码区间)
同步 DEL 大 keyUNLINK / FLUSHALL ASYNC(下一页)
复杂 Lua拆短脚本;只做必要的 CAS 逻辑;监控 busy-reply-threshold 触发情况
答题口径:"单线程模型的全部风险集中在'长时占用主线程':扫描、全量读、大 value 释放、慢脚本。治理手段就四字诀——分批、异步、短脚本、监控(SLOWLOG/LATENCY)。"
单线程的账单:所有命令串行,一条 KEYS 扫百万元素,后面所有人排队。常见元凶四类:全库扫描、大集合全量读、同步删大 key、慢脚本。发现靠 SLOWLOG,两个默认值要背:阈值 10000 微秒即 10 毫秒,队列 128 条,内存环形结构重启即失。治理四字诀:分批用 SCAN 系、异步用 UNLINK、脚本拆短、监控常开。SCAN 有个细节可以加分:rehash 期间游标遍历可能返回重复 key,业务要幂等。

Background Threads · src/bio.c

把"释放与落盘"挪出主线程:bio 三兄弟

UNLINK:DEL 的异步版

DEL 是主线程同步 free——100MB 的 hash 释放直接卡住全部请求。UNLINK 只把 key 从键空间摘除(O(1)),真正释放交给 BIO_LAZY_FREE 线程。判定阈值:对象"够大才值得异步"(源码 LAZYFREE_THRESHOLD=64:集合类元素数 >64 才丢后台,小对象直接删反而省去线程协调)。

FLUSHALL ASYNC / FLUSHDB ASYNC:共享对象引用计数清零 + 丢后台清空。

bio:3 个后台线程(bio.h BIO_NUM_OPS=3)

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-flushFLUSHALL/FLUSHDB 默认按 ASYNC 处理
DEL 和 UNLINK 的区别是必考题:DEL 主线程同步释放,大 key 直接卡死;UNLINK 只摘键、释放丢给后台 lazyfree 线程。有个精细判定:源码里阈值是 64,集合类元素超过 64 个才值得异步,小对象直接删更快,因为线程协调本身有成本。后台线程一共三类,bio.h 里写死:关文件、AOF 落盘、lazy free。五个 lazyfree 配置默认全 no,其中 lazy-user-del 建议生产开成 yes,业务无感获得异步删除。这套设计的本质:主线程只做摘除引用,重活全给后台。

Threaded I/O · Redis 6.0+

IO 多线程:并行的是"网络读写",不是"命令执行"

配置默认值说明
io-threads1(= 关闭)IO 线程数(含主线程);建议 2–4,官方提醒不超过核数的 3/4;高连接大吞吐场景收益明显
io-threads-do-readsno读方向默认不并行:官方 redis.conf 注释——多线程读"通常无收益",仅 TLS 解析等场景经压测后开启

官方为什么不把命令执行也多线程化

① Redis 的瓶颈是内存和网络带宽,CPU 本就空闲;② 命令并行执行必须给所有数据结构加锁——锁竞争、死锁、粒度设计全是成本,还会破坏"每条命令原子"的免费语义;③ 无锁单线程让实现简单、行为可复现。这是 redis.conf 注释与作者多次阐述的官方口径。

什么场景值得开

海量连接 + 大批量请求(pipeline 打包大报文)、大 value 读写——网络收发与协议解析占了大头。典型错误认知:"QPS 不够就开 io-threads":小命令短连接场景下收益趋近于零,先看 slowlog 和网络带宽。

8.0 演进(2025-05 GA):Redis 8 重写了全新的异步 I/O threading 实现(8.0-M03 引入,GA 保留):参数名与默认值不变(io-threads=1 仍默认关闭),开启后写方向吞吐大幅提升——但"命令执行单线程、原子性语义"至今未变。
6.0 的 IO 多线程只干两件事:读方向的解析(默认还不开)和写方向的写回,命令执行永远是主线程串行。io-threads 默认 1 等于关闭,io-threads-do-reads 默认 no,官方注释原话说多线程读通常没收益,除非 TLS。为什么不把执行也并行:官方口径三条,瓶颈不在 CPU;并行要加锁得不偿失;单线程让每条命令天然原子。什么场景值得开:海量连接加大报文。8.0 把这套机制整个重写成异步 IO 线程,参数没变、默认没开,但单线程执行的语义至今没动,这个结论要带上版本号说。

How Threaded I/O Works

三段式协作:分发 → 并行 → 汇合执行

IO 多线程三阶段:并行读写、单线程执行 主线程把待处理客户端按 round-robin 分配给 IO 线程,各线程并行完成读解析或写回,主线程自旋等待全员完成后,串行执行所有命令,随后进入写回阶段的再一轮并行。 阶段 A · 读 主线程 round-robin 分发待读客户端 (io-threads-do-reads=no 时读仍由主线程做) IO 线程并行 read + parse(互不等待) 主线程(0 号) 处理自己那份客户端 IO thread 1 read + RESP 解析 IO thread 2 read + RESP 解析 IO thread 3 read + RESP 解析 阶段 B · 汇合 主线程自旋等待全部 线程完成(屏障) 主线程串行执行所有命令(单线程 · 原子 · 无锁) 每条命令依次 call():这是唯一"干活"的串行区 命令执行不感知任何线程——语义与单线程完全一致 阶段 C · 写 分配待写客户端 IO 线程并行 write 回复 → 全员完成后回到 epoll_wait 默认开启的就是这一段:写方向多线程收益最大(大回复、pipeline 场景)
IO 多线程的协作是三段式。阶段 A:主线程把待读客户端轮询分给各 IO 线程,并行完成读取和 RESP 解析,读方向默认还不开。阶段 B:主线程自旋等全员到齐当屏障,然后一口气串行执行所有命令——这是唯一干活区,单线程无锁。阶段 C:把待写客户端再分下去并行写回,默认生效的就是这段。记住关键点:IO 线程之间从不共享"正在处理"的客户端,命令执行区永远只有一个线程,所以既不用加锁,语义也和纯单线程一模一样。

Atomicity Ladder

pipeline / MULTI / Lua:原子性三档,别混为一谈

手段省什么原子性适用与短板
pipeline网络 RTT:攒 N 条命令一次发送一次收:执行期可能插入其他客户端的命令(服务端逐条执行)纯性能优化(批量导入/读);客户端排队,服务端需缓冲回复(大 pipeline 占内存)
MULTI/EXECRTT(配合 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。

一句话层次:"pipeline 管网络,MULTI 管打包,Lua 管逻辑;只有 Lua 和 EXEC 的执行窗口是真原子,pipeline 只是省 RTT。"
这页是原子性层次题的标准答案。pipeline 只解决网络往返,命令到达服务端还是逐条执行,中间能插别人的命令,所以不原子。MULTI/EXEC 在执行窗口内连续不被打断,算执行期原子,但它不能包"先读再决定写什么"的逻辑。Lua 脚本作为一条命令串行执行,真原子还能编程,是条件写场景的唯一正解,代价是慢脚本阻塞全实例。扣库存的例子要会展开:WATCH 重试在低竞争下没问题,高竞争会重试风暴,Lua 一次判断一次成功。

Transactions · MULTI / EXEC / WATCH

Redis 事务:入队拦截,执行不回滚

两类错误,两种命运

入队错误(命令不存在/参数个数错):入队时就报错,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
面试要点:"Redis 事务是'打包串行执行'而非'ACID 事务':有隔离性(执行期不被插入)、无回滚、无 undo;UNWATCH 的正确姿势是 EXEC/DISCARD 自动清理,不用手工管理。"
事务页的关键是两类错误的分界。入队时的语法错误会在 EXEC 时拦截整批,这是 2.6.5 之后的行为;执行时的类型错误只让那一条失败,其他照跑,不回滚。为什么不回滚要能讲出官方逻辑:运行时错误是 bug,回滚救不了,反而要维护 undo 拖慢常态。WATCH 的机制三步:登记、写时污染、EXEC 时裁决,失败返回 nil 由客户端重试。收尾强调 Redis 事务不是 ACID 事务,有隔离性没回滚,这句话能直接区分背题和理解题的人。

Programmability · EVAL / Functions

Lua 脚本:真原子的代价与 7.0 Functions

EVAL:原子但阻塞

脚本作为一条命令串行执行,执行期间不插入任何其他命令——这是"真原子"的实现方式,也意味着脚本跑多久,全实例停多久。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(引擎可插拔)
Lua 原子的实现方式就是"脚本当一条命令跑",所以原子的另一面是阻塞:默认超过 5 秒,其他客户端开始收到 BUSY。此时 SCRIPT KILL 只能杀没写过数据的脚本,写过的只能关实例——这就是"脚本必须短小"的硬理由。复制默认复制脚本原文,要求确定性,脚本里调用过随机命令后就不许再写。7.0 的 Functions 是继任者:函数库注册到服务端并持久化,复制改成按效果复制写命令,运维上可以版本化管理脚本,这是 7.0 之后面试可以主动提的演进。

Protocol · REdis Serialization Protocol

RESP2 / RESP3:一句话说清协议层

RESP2(默认)

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

RESP3(6.0+,HELLO 3 切换)

新增类型:% map、~ set、# bool、, double、_ null、> push。最大意义:带外推送——客户端缓存(client-side caching)的失效通知走独立 push 通道,不再混在回复流里;ZRANGE 也能直接回 map 语义。

答题一句话:"RESP2 五前缀文本协议保证解析快;RESP3 在 6.0 引入,核心是 map/set/push 等富类型与带外推送,为客户端缓存等新特性铺路;客户端用 HELLO 协商版本,不协商就是 RESP2。"
协议这页一句话带过即可,但要能说出层次:RESP2 是五个前缀的文本协议,简单到人手可写,解析快是设计目标;RESP3 在 6.0 出现,加了 map、set、布尔、浮点和 push 类型,最重要的是 push 带外推送,客户端缓存的失效通知靠它。切换用 HELLO 3,不协商永远 RESP2,所以老客户端完全无感。面试被问协议时,能落到"为什么文本协议反而快"和 push 类型的用途,就够了。

As of Redis 8.x · 2025

Redis 8 的多线程现状:换引擎,不换灵魂

版本线程模型相关变化
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 均成立。

这页给版本现状,防止拿 2020 年的结论答 2025 年的面试。6.0 引入 IO 线程,7.x 线程模型没动,8.0 用全新异步 IO 线程实现重写了网络栈,性能大涨但参数和默认值没变。最重要的一句话:命令执行至今单线程,原子性语义从未依赖锁。所以"Redis 是单线程吗"要分两层答:执行层永远单线程,网络、落盘、释放这些外围早就是多线程了。顺带提一句 license 演进和线程无关,别在面试里混着说。

Interview QA · 1/2

模型与阻塞 8 连问

1 · Redis 是单线程吗?"单线程"到底指什么?

执行层单线程外围多线程

命令执行永远单线程(6.0 至 8.x 不变)。外围早就是多线程:bio 三线程(关文件/AOF fsync/lazy free)、io-threads 网络线程、RDB/AOF 重写子进程、8.0 异步 IO 线程。原子性来自"无第二执行线程",不是锁。

2 · 为什么快?四个要素按什么顺序答?

内存数据结构epoll单线程

内存操作(地基)→ 高效数据结构(O(1)~O(logN))→ IO 多路复用(单线程管海量连接)→ 单线程无锁无切换(白送原子性)。补定量:单实例 10w+ QPS;官方口径瓶颈在内存与网络带宽,不在 CPU。

3 · 一条命令的完整生命周期?

epoll 就绪解析call()beforeSleep 写回

①RESP 进内核缓冲 → ②epoll 读就绪、readQueryFromClient 解析 → ③processCommand/call 单线程执行 → ④写命令进 aof_buf 与复制流 → ⑤beforeSleep 统一写回 → ⑥回到 epoll_wait。io-threads 只并行②⑤,③永远串行。

4 · 线上发现 Redis 变慢,怎么定位慢命令?

SLOWLOGLATENCYINFO commandstats

SLOWLOG:slowlog-log-slower-than 默认 10000μs(10ms)、max-len 128,SLOWLOG GET 看慢命令原文;LATENCY 事件监控粒度更粗;INFO commandstats 按命令统计耗时分布。根治:SCAN 替代 KEYS、拆大 key、UNLINK 删除、拆短 Lua。

5 · DEL 和 UNLINK 的区别?什么时候 UNLINK 反而"不值"?

异步释放LAZYFREE_THRESHOLD 64

UNLINK 只从键空间摘除(O(1)),释放丢 BIO_LAZY_FREE 线程。但源码有判定:集合类元素 ≤64 的小对象直接同步删更快——线程协调成本高于释放成本,所以 UNLINK 内部对小对象走同步路径。

6 · bio 有哪几个后台线程?各干什么?

3 个close/fsync/lazyfree

BIO_CLOSE_FILE:RDB/AOF 重写后关旧文件 fd;BIO_AOF_FSYNC:everysec 的 fsync 在后台执行,磁盘慢不卡命令(配合 aof 的 2s 延迟机制);BIO_LAZY_FREE:UNLINK/过期/淘汰的大对象异步释放。共 3 个(BIO_NUM_OPS=3)。

7 · io-threads 开了之后,命令还原子吗?

原子不变读写并行/执行串行

原子。多线程只覆盖网络读解析与写回;命令执行仍由主线程串行完成。线程间用屏障(自旋等待)同步,IO 线程不碰共享数据结构——不需要锁,语义与纯单线程一致。

8 · io-threads 什么时候值得开?默认多少?

默认 1=关2–4高连接大吞吐

默认 1(关闭);io-threads-do-reads 默认 no(读方向官方注释说通常无收益)。适合海量连接 + pipeline 大报文 + 大 value 的网络密集场景,建议 2–4 个且不超过核数 3/4;小命令场景收益趋近零,先查 slowlog 再考虑开线程。

第一组 QA 抓主干。第一题的分层答法最重要:执行层永远单线程,外围早就是多线程,这个框架能接住几乎所有追问。第五题 UNLINK 的 64 阈值是稀缺细节:小对象同步删反而快。第六题 bio 三线程要和 lazyfree 五个配置区分开。第七题强调原子性与线程无关,靠的是"没有第二个执行线程"。第八题落到场景判断,别背成"QPS 不够就开线程"。

Interview QA · 2/2

事务与脚本 8 连问

9 · Redis 事务支持回滚吗?两类错误分别怎么处理?

不回滚EXECABORT

不支持。入队错误(命令不存在/参数错)在 EXEC 时拦截整批,返回 EXECABORT(2.6.5+);执行错误(类型错)只让该条失败,其余继续、已执行不撤销。官方理由:执行错误是编程 bug,回滚救不了还拖慢常态路径。

10 · WATCH 怎么实现乐观锁?失败时返回什么?

watched_keysDIRTY_CASEXEC 返回 nil

WATCH 把客户端登记到 watched_keys;期间任何客户端写该 key 会把监视者标记为 DIRTY_CAS;EXEC 检查到污染就不执行整个事务、返回 nil,客户端重试。EXEC/DISCARD/连接断开自动清空全部 WATCH。

11 · Lua 脚本为什么是原子的?脚本卡住了怎么办?

单命令串行busy-reply-threshold 5sSCRIPT KILL

脚本作为一条命令执行,期间不插入其他命令——原子与阻塞同源。超过 busy-reply-threshold(默认 5000ms)后其他客户端收到 BUSY;SCRIPT KILL 只能杀未写过数据的脚本,已写入的只能 SHUTDOWN NOSAVE,所以脚本必须短小。

12 · 脚本的复制是复制脚本还是复制效果?

默认整段复制Functions 按效果

EVAL 默认整段复制:主库发脚本原文、从库各跑一遍,要求脚本确定(调用随机命令后禁止写)。7.0 Functions 改为按效果复制产生的写命令,且函数库随 RDB/AOF 持久化、可版本管理——这是官方定位的 EVAL 继任者。

13 · pipeline 与 MULTI 的本质区别?能一起用吗?

RTT vs 原子可组合

pipeline 纯客户端攒批省 RTT,服务端逐条执行、中间可插其他命令——不原子;MULTI 是服务端排队、EXEC 窗口内连续执行——执行期原子。可以组合:pipeline 包裹整段 MULTI 命令一次 RTT 提交整个事务。

14 · beforeSleep 具体做哪些事?

AOF flush写回回复SLOW 过期

每轮事件循环收尾:flushAppendOnlyFile(everysec 策略在此落盘)、handleClientsWithPendingWrites 把回复写回(io-threads 可并行)、activeExpireCycle SLOW 模式过期清理、释放解除阻塞的客户端等——它是主线程的"批处理窗口"。

15 · serverCron 是什么?hz 默认多少,调大意味着什么?

时间事件hz=101–500

周期时间事件,默认每秒 10 次(hz=10,可 1–500):过期 FAST 清理、dict rehash 1ms 兜底、客户端超时检查、复制/集群定时任务、内存统计。调大 hz = 清理更勤、统计更实时,但 CPU 空转开销上升。

16 · RESP3 相比 RESP2 的核心增量?

map/set/bool/doublepush 带外推送

新增 % map、~ set、# bool、, double、_ null 与 > push。最大价值是 push 类型:客户端缓存(client-side caching)的失效通知走独立带外通道,不混入命令回复流。HELLO 3 协商,不协商永远 RESP2。

第二组偏事务和脚本。第九题两类错误必须分开举例:入队错拦整批、执行错不拦。第十一题 SCRIPT KILL 的边界要讲清:只杀没写过数据的脚本,这是数据完整性的保护。第十二题的复制方式变化是 7.0 的新考点,EVAL 复制原文、Functions 复制效果。第十四、十五题把 beforeSleep 和 serverCron 的职责列全,能体现读过事件循环源码。第十六题落在 push 类型的用途上,与客户端缓存连起来收尾。

Related · Cross-links

相关知识点

Redis 领域 · 同批 deck

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)、以单线程换无锁语义

收尾串联:数据结构那本 deck 是"快四要素"第二要素的全量展开,持久化 deck 会回到 aof_buf 和 bio 的 fsync 线程,分布式锁完全建立在 Lua 真原子之上。跨领域的两条线:GMP 的 sysmon 监控线程对照 Redis 的 serverCron,Go 的 netpoller 对照 Redis 的 bio,都是"把慢操作挪出关键路径"的同构思想。

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-specappendfsync 与 aof_buf、EVAL 原子性与 busy-reply-threshold、RESP2/RESP3 规范
github.com/redis/redis · src/ae.c / ae_epoll.c / ae_kqueue.caeMain / aeProcessEvents / beforeSleep 调用时序;按平台多路复用实现
github.com/redis/redis · src/bio.h / bio.c / lazyfree.cBIO_NUM_OPS=3 三线程分类;LAZYFREE_THRESHOLD=64 异步删除判定
github.com/redis/redis · src/networking.c / server.c / transactions.creadQueryFromClient、processCommand/call、WATCH 的 watched_keys 与 DIRTY_CAS
github.com/redis/redis · 7.0 00-RELEASENOTES(Functions 部分)FUNCTION LOAD、effect replication、busy-reply-threshold 改名
参考表里所有参数默认值和版本结论都有出处,8.0 的异步 IO 线程结论以官方博客为准——报版本结论时先报博客再报源码路径,可信度最高。