【Redis】 Redis 是单线程还是多线程

一、一次 Redis 请求到底经历了什么?

客户端执行:

复制代码
SET name Tom

Redis 处理这个请求,大致分为三个阶段:

复制代码
① 网络读取
客户端数据从网卡进入 Redis

② 命令执行
解析 SET,修改 Redis 内存中的字典

③ 网络写回
把 OK 返回给客户端

其中:

复制代码
网络读取、协议解析、网络写回
        ≠
真正修改 Redis 数据

Redis 6 引入的多线程主要优化前一部分;真正执行 GETSETHSETINCR 等命令、访问和修改核心数据结构,仍主要由主线程串行完成。


二、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 的利用率;但是 GETSETINCR 等核心命令仍主要由主线程串行执行。因此,Redis 6 的多线程是 I/O 多线程,而不是多个线程同时修改核心数据。

Redis 将 Lua 脚本视为一个整体命令,并保证脚本原子执行。脚本执行期间,其他客户端的命令不能穿插执行,只能排队等待。因此,如果 Lua 脚本包含大量循环、操作大 key 或调用高复杂度命令,就会长时间占用 Redis 的命令执行线程,造成整个节点阻塞。使用 Lua 时应保证脚本短小、执行时间可控,并避免处理无上限的大量数据。

相关推荐
这个DBA有点耶1 小时前
SQL调优进阶:从“优化一条SQL”到“优化一个系统”的思维升级
java·大数据·数据库·sql·程序人生·dba·改行学it
IvorySQL1 小时前
PG 日报|SQL/PGQ 图查询基于联接重写机制实现
数据库·人工智能·sql·postgresql·区块链·ivorysql
数聚天成DeepSData4 小时前
遥感农业数据集下载全攻略
数据库·人工智能·深度学习·机器学习·自然语言处理·数据挖掘
一勺菠萝丶5 小时前
生产环境平滑升级实战-Nginx维护页数据库迁移与安全回滚
数据库·nginx·安全
心静自然凉8005 小时前
MySQL主从同步配置(一主一从)
数据库
haidy ahmed5 小时前
企微复杂审批流反序列化:动态表单解析引擎与EAV存储模型
数据库·人工智能·自动化·企业微信
Lihua奏5 小时前
MVCC:为什么多人同时读写数据,数据库还能不乱
数据库
YYYing.5 小时前
【C++大型项目之高性能服务器框架 (七) 】Socket与ByteArray模块
服务器·c++·后端·框架·高性能·c/c++
952365 小时前
Redis - 应用
数据库·redis·分布式·缓存