总体架构
This content is not available in your language yet.
Valkey 的架构核心是“数据状态单线程化,外围工作并行化”。读码时要把 IO worker、BIO 和 fork child 分成三种不同的并发模型,不能笼统称作后台线程。
先给答案:Valkey 并行化网络搬运,但不拆散数据状态的单一所有权
Section titled “先给答案:Valkey 并行化网络搬运,但不拆散数据状态的单一所有权”Valkey 的 IO worker 可以并行 accept、poll、socket read/write,命令解析后的核心数据修改仍回到主线程。这样把网络瓶颈从命令执行路径剥离,同时避免为 dict、quicklist 和 zset 引入跨线程锁协议。
IO worker、BIO 与 fork child 是三种不同边界:前者协作推进客户端 IO,BIO 延后文件或释放任务,fork child 隔离持久化计算。CPU 重命令仍会阻塞主线程,worker 队列积压和 fork COW 则是另外两类故障,不能用“多线程”一词混在一起判断。
+----------------------+ | 主线程 | | ae event loop | | command + data state | +----+------------+----+ | | IO inbox/outbox BIO queues | | +----------v--+ +---v--------+ | IO workers | | BIO threads | | read/write | | fs/free | +-------------+ +------------+ | fork child RDB/AOF/Modulesrc/server.c:7558 的主入口完成配置解析、服务初始化和事件循环进入;initServer 位于 src/server.c:2959,负责监听 socket、事件注册以及线程相关初始化。IO 线程在 src/io_threads.c:484 附近创建并启动,BIO 在 src/bio.c:155 初始化。
main └─ initServer ├─ create listening/client events ├─ initIOThreads ├─ bioInit └─ aeMain └─ beforeSleep / aeProcessEvents / afterSleepsocket readable -> IO worker read / poll -> shared inbox / client state -> 主线程解析命令 -> 数据结构 mutation -> response 入 outbox -> IO worker writesrc/server.c:7890 进入 aeMain 后,事件循环按 sleep 前、事件处理、sleep 后的边界推进;IO worker 的睡眠前后钩子分别在 src/io_threads.c:116 与 src/io_threads.c:152。这意味着线程调度不是独立于事件循环的另一套业务循环,而是围绕主循环协作。
为什么不把命令执行也并行化
Section titled “为什么不把命令执行也并行化”如果多个 worker 直接修改 dict、quicklist、zset 等共享对象,就必须为每条命令引入锁、事务边界或分片所有权。那会把 Redis 原本容易推理的顺序语义,换成锁竞争、锁顺序和可见性问题。Valkey 选择保留主线程的数据所有权,把收益集中在 socket read/write 和 poll 上。
- IO worker 加速的是网络路径;CPU 很重的 Lua、模块命令和大范围 key 扫描仍会阻塞主线程。
fork子进程不是线程,复制的是进程地址空间,写时复制带来额外内存压力。- BIO 的“异步”只表示调用线程不等待,任务仍可能因队列积压延迟完成。
这是“单一所有权 + 外围流水线”的通用模式:把必须顺序化的状态留在一个 owner,把可分段的输入输出搬到 worker。它适合事件循环服务器、日志代理和协议网关,但不适合天然需要跨分片原子 mutation 的工作负载。
面试锚点:Valkey 的多线程到底并行了什么?为什么命令执行仍在主线程?BIO 和 fork child 有何区别?