一、一次 Redis 请求到底经历了什么?
客户端执行:
SET name Tom
Redis 处理这个请求,大致分为三个阶段:
① 网络读取
客户端数据从网卡进入 Redis
② 命令执行
解析 SET,修改 Redis 内存中的字典
③ 网络写回
把 OK 返回给客户端
其中:
网络读取、协议解析、网络写回
≠
真正修改 Redis 数据
Redis 6 引入的多线程主要优化前一部分;真正执行 GET、SET、HSET、INCR 等命令、访问和修改核心数据结构,仍主要由主线程串行完成。
二、Redis 所谓"单线程"指什么?
通常面试里说:
Redis 是单线程。
实际上省略了限定词,完整说法应该是:
一个 Redis 实例中,普通命令的核心执行过程主要由一个主线程串行完成。
例如三个客户端同时发送命令:
客户端 A:SET name Tom
客户端 B:INCR count
客户端 C:GET user:1
主线程不会同时修改数据,而是依次处理:
执行 SET name Tom
↓
执行 INCR count
↓
执行 GET user:1
所以不会出现:
SET 执行了一半
INCR 插进来执行
SET 再继续
这也是为什么单条 Redis 命令通常具有原子性。
Lua 脚本也是在这个命令执行线程中运行,所以脚本没执行完,当前节点的其他命令通常只能排队等待。
Redis 官方仍将命令处理描述为主要使用单线程事件循环,客户端请求按顺序执行。
三、Redis 单线程为什么还能处理很多连接?
因为:
单线程执行命令,不等于一次只能连接一个客户端。
Redis 使用 I/O 多路复用机制,例如 Linux 中的 epoll。
可以理解为:
客户端 A ─┐
客户端 B ─┼── epoll 统一监听 ── Redis 主线程
客户端 C ─┤
客户端 D ─┘
Redis 不需要为每个连接创建一个线程,也不需要让线程一直阻塞等待客户端数据。
操作系统会告诉 Redis:
A 的请求到了
C 的请求到了
D 的请求暂时没数据
Redis只处理已经准备好的连接。
这里要区分两个概念:
并发连接:可以同时维持很多客户端连接
并行执行:同一时刻多个 CPU 核同时执行多条命令
传统 Redis:
可以有大量并发连接
但一个实例的核心命令主要串行执行
四、Redis 4.0 引入了什么?
Redis 4.0 的重要变化之一,是加入了更完善的异步释放内存机制。
例如有一个非常大的 Hash:
DEL big_hash
DEL 不只是从字典中删掉一个 key,还要释放这个 key 对应的大量内存对象。
如果这个 Hash 有几百万个字段:
删除字典入口:很快
逐个释放几百万个对象:可能很慢
普通 DEL 的执行过程可能是:
主线程找到 big_hash
↓
主线程释放大量内存
↓
释放完成
↓
继续执行其他命令
释放期间,其他命令就会等待。
因此可以使用:
UNLINK big_hash
它大致分为:
主线程:
把 big_hash 从键空间中摘除
立即返回
后台线程:
慢慢释放 big_hash 占用的内存
示意图:
主线程 后台线程
摘除 big_hash
│
返回客户端 OK ─────────────→ 异步释放对象内存
继续处理 GET、SET
所以 UNLINK 的核心目的不是让"删除结果异步生效",而是:
key 会立刻从逻辑上消失,但昂贵的内存回收工作交给后台线程。
Redis 官方说明,类似 UNLINK 的多线程异步释放能力从 Redis 4 开始提供。
五、Redis 6.0 为什么引入 I/O 多线程?
Redis 的一次请求包含:
读 socket
↓
解析 Redis 协议
↓
执行命令
↓
构造响应
↓
写 socket
早期模式中,上述大量工作主要由主线程承担:
主线程:
读请求 → 解析 → 执行命令 → 写响应
随着硬件性能提升,可能出现一种情况:
真正执行 GET 很快
但读取大量网络数据、解析协议、发送大响应很耗 CPU
例如:
GET 一个较大的字符串
MGET 大量 key
HGETALL 较大的 Hash
CPU 时间可能大量花在:
从 socket 读取数据
拷贝内存
解析请求
把大量响应写回 socket
于是 Redis 6 引入了可配置的 I/O 多线程。
六、Redis 6 的多线程到底负责什么?
可以简化成下面的结构:
┌─ I/O线程1:读取、解析请求
客户端请求 ────────┼─ I/O线程2:读取、解析请求
└─ I/O线程3:读取、解析请求
↓
主线程串行执行命令
↓
┌─ I/O线程1:发送响应
响应结果 ──────────┼─ I/O线程2:发送响应
└─ I/O线程3:发送响应
核心仍然是:
I/O 线程:
帮助读取网络请求、解析协议、发送响应
主线程:
执行 GET、SET、HSET、INCR
修改核心数据结构
官方资料对 Redis 6 的描述也是:I/O 线程可以承担 socket 读写和命令解析,但数据命令仍由主线程处理。
七、面试标准回答
Redis 是单线程还是多线程,需要分角度回答。从核心命令执行角度看,一个 Redis 实例中的普通数据读写命令主要由主线程串行执行,因此通常称 Redis 为单线程模型。这样可以避免频繁加锁和线程上下文切换,也有利于保证单条命令的原子性。
但从整个 Redis 进程来看,它并不是只有一个线程。Redis 会使用后台线程处理异步释放内存、部分磁盘 I/O 等任务。Redis 4.0 引入了
UNLINK等 lazy free 能力,可以把大 key 的实际内存释放交给后台线程。Redis 6.0 又引入了 I/O 多线程,可以并行处理网络读取、协议解析和响应写回,以提高多核 CPU 的利用率;但是
GET、SET、INCR等核心命令仍主要由主线程串行执行。因此,Redis 6 的多线程是 I/O 多线程,而不是多个线程同时修改核心数据。Redis 将 Lua 脚本视为一个整体命令,并保证脚本原子执行。脚本执行期间,其他客户端的命令不能穿插执行,只能排队等待。因此,如果 Lua 脚本包含大量循环、操作大 key 或调用高复杂度命令,就会长时间占用 Redis 的命令执行线程,造成整个节点阻塞。使用 Lua 时应保证脚本短小、执行时间可控,并避免处理无上限的大量数据。