认识Redis
开篇:为什么 Redis 这么快?
面试里有个经典问题:"Redis 为什么这么快?" 几乎每个后端开发都被问过。但很多人背完答案就忘了,因为没有真正理解背后的设计思路。
这篇文章,我们就从零开始,把 Redis 的核心设计理念搞清楚。不背答案,理解本质。
一、Redis 是什么?
一句话概括:Redis 是一个基于内存的键值存储系统。
但它远不只是缓存。Redis 支持丰富的数据结构(String、Hash、List、Set、Sorted Set 等),还能做分布式锁、消息队列、排行榜、计数器……可以说是后端开发的瑞士军刀。
Redis 的名字来源于 Remote Dictionary Server,远程字典服务。它把数据存在内存里,所以读写速度极快——内存的寻址速度是纳秒级的,比磁盘快了大约 10 万倍。
Redis 的主要特点:
- 基于内存:所有数据存在内存中,读写速度极快
- 丰富的数据结构:不只是简单的 KV,支持 String、Hash、List、Set、ZSet 等
- 单线程命令执行:避免锁竞争,保证操作的原子性
- 持久化支持:RDB 快照和 AOF 日志,保证数据不丢
- 高可用:支持主从复制、哨兵模式、Cluster 集群
除了做缓存,Redis 还常见于这些场景:分布式锁(SETNX)、排行榜(ZSet)、计数器(INCR)、限流(滑动窗口)、分布式 Session、布隆过滤器、GEO 地理位置查询等。后面的《核心用法》篇会逐一展开每个场景的实现细节。
通信协议:RESP
Redis 使用自己设计的文本协议进行客户端与服务端的通信,叫做 RESP(REdis Serialization Protocol)。这个协议基于 TCP,设计上追求简单和高效,易于解析。
举个例子,当你执行 SET mykey myvalue 时,客户端实际发送的内容是:
*3\r\n
$3\r\n
SET\r\n
$5\r\n
mykey\r\n
$7\r\n
myvalue\r\n*3 表示有 3 个参数,$3 表示下一个参数长度是 3 个字节。服务端收到后返回 +OK\r\n 表示操作成功。
这个协议看起来"啰嗦",但解析起来非常简单和高效——不需要复杂的语法分析,按固定格式读取就行。
二、单线程还是多线程?
这是一个非常容易搞混的问题。答案取决于你说的是哪个版本、哪个模块。
2.1 版本演进
Redis 3.x 时代,确实是纯单线程。所有操作——网络 IO、命令执行——都在一个线程里完成。
Redis 4.x 开始,加入了后台线程来处理一些耗时操作,比如异步删除大 Key(UNLINK 命令)。想象一下,如果你有一个包含百万元素的 Set,用 DEL 删除它,主线程会卡住好几秒。4.0 引入了惰性删除,把这个耗时操作交给后台线程,主线程立刻返回,不阻塞。
Redis 6.x 是一个重要的转折点。它引入了多线程来处理网络 IO(读取请求、返回响应),但命令的执行仍然是单线程的。
所以,当我们说"Redis 是单线程的",准确含义是:
Redis 的网络 IO 和键值对读写是由一个主线程完成的。获取请求(socket 读)、解析、执行、返回结果(socket 写),在 6.0 之前都是一个线程串行处理的。
而持久化(RDB/AOF)、异步删除、集群数据同步等功能,一直都有额外的后台线程在干活。
整个 Redis 进程是多线程的,但核心的命令执行是单线程的。 这句话才是最精确的表述。
2.2 大 Key 删除问题
这里多说一嘴 4.0 解决的大 Key 删除问题,因为面试也常问。
当你用 DEL 删除一个包含大量元素的 Key 时(比如一个有 100 万元素的 Hash),Redis 需要逐个释放这些元素占用的内存。在单线程模型下,这个过程会阻塞主线程,导致所有客户端请求被卡住。
Redis 4.0 的解决方案是引入 UNLINK 命令(惰性删除):主线程只是把这个 Key 从键空间中移除(O(1) 操作),然后把实际的内存回收工作交给后台线程异步完成。客户端感觉这个 Key 已经不存在了,但内存是后台慢慢释放的。
# 同步删除,大 Key 会阻塞主线程
DEL my_big_hash
# 异步删除(推荐),后台线程回收内存
UNLINK my_big_hash三、为什么单线程还能这么快?
很多人第一反应是:单线程不是很慢吗?为什么 Redis 单线程还能达到每秒 10 万+ 的 QPS?
原因有四个,我们逐个拆解。
3.1 基于内存操作
Redis 所有数据都存在内存中。内存的响应时间大约是 100 纳秒,而磁盘(即使是 SSD)通常在微秒到毫秒级别。
打个比方:内存操作就像从你的口袋里掏手机,磁盘操作就像跑去家里拿——前者是随手可得,后者需要跑一趟。Redis 选择把所有数据都放在"口袋"里,自然快得多。
再从底层看一下为什么磁盘慢。磁盘有两个核心指标:寻址时间(ms 级)和带宽。而内存的寻址是 ns 级别的,磁盘比内存在寻址上慢了 10 万倍。再加上磁盘有 4K 对齐的限制(操作系统每次最少读取 4K),小数据的随机读写效率更差。这就是为什么数据库需要 B+ 树索引来减少磁盘 IO,而 Redis 完全不需要。
3.2 高效的数据结构
Redis 的底层数据结构是专门设计过的:
- 哈希表:O(1) 查找,几乎所有 Key 的定位都走哈希表
- 跳表:让有序集合的范围查询达到 O(log N)
- 压缩列表/紧凑列表:小数据量时极致节省内存
- SDS(简单动态字符串):O(1) 获取长度、二进制安全、预分配空间减少内存重分配
这些精心设计的数据结构,让每个操作都尽可能高效。比如你用 Java 的 HashMap,底层是数组+链表+红黑树。Redis 的哈希表思路类似,但针对自身场景做了大量优化(渐进式 rehash、编码自动转换等),在《底层原理》篇会详细展开。
3.3 IO 多路复用
这是 Redis 高性能的关键武器。一个线程通过 IO 多路复用技术(Linux 上是 epoll),可以同时监听成千上万个客户端连接。哪个连接有数据了,就处理哪个,不需要为每个连接创建一个线程。
下一节会详细讲这个机制。
3.4 避免上下文切换
单线程意味着不需要加锁,不需要线程切换。多线程编程中最头疼的锁竞争、死锁、上下文切换开销,在 Redis 里统统不存在。代码也因此更简单、更容易维护。
一次线程上下文切换大约需要几微秒,看起来不多,但在高并发场景下,每秒几十万次操作,累积起来的开销是可观的。Redis 选择单线程,彻底消除了这部分开销。
总结一下:Redis 的性能瓶颈不在 CPU,而在内存和网络带宽。 既然 CPU 不是瓶颈,单线程反而是最优解——简单、高效、无锁。
四、IO 多路复用
IO 多路复用是理解 Redis 高性能的核心,值得多花点篇幅。
4.1 用餐厅服务员来类比
想象一个餐厅,有很多桌客人,但服务员人数有限:
- 阻塞 IO 就像每桌配一个服务员,客人不点菜服务员就干站着等。10 桌就要 10 个服务员,1000 桌就要 1000 个,人力成本不可接受。
- 非阻塞 IO 是一个服务员挨桌问"您要点菜吗?",没人点就去下一桌。但如果有 1000 桌,挨个问一圈效率也不高,大部分时间都在"白跑"。更要命的是,每次问一桌都要从用户态切到内核态去检查,来回切换的开销很大。
- IO 多路复用 是给每桌装一个呼叫按钮。服务员只需要盯着按钮面板,哪桌按了就去哪桌服务。一个服务员就能高效服务整个餐厅。
Redis 用的就是这种"按钮面板"模式——epoll。
这里的"多路"指的是多个 Socket 连接,"复用"指的是复用同一个线程。通过一个线程处理多个 IO 流,既避免了多线程的开销,又不会因为某个连接没数据就干等着。
4.2 技术实现:Reactor 模型
Redis 的网络模型基于经典的 Reactor 模式,由四个部分组成:
多个 Socket 连接
↓
IO 多路复用程序(epoll) ← 监听所有连接的读写事件
↓
文件事件分派器 ← 把就绪的事件按顺序分发
↓
事件处理器 ← 执行具体的连接应答、读取、写入操作每当一个 Socket 准备好执行连接应答、读取、写入等操作时,就会产生一个文件事件。因为多个 Socket 可能同时就绪,多个事件可能并发出现。IO 多路复用程序把它们排成队列,文件事件分派器依次处理——这也是 Redis 被叫做单线程模型的原因:队列的消费者只有一个线程。
Redis 的 IO 多路复用程序底层封装了操作系统的 IO 多路复用函数库(select、poll、epoll 等),每个函数库在 Redis 源码中都有对应的单独文件,Redis 会根据运行平台自动选择最优的实现。
4.3 select / poll / epoll 的演进
Linux 提供了三种多路复用实现,代表了三代技术演进:
select 是最早的实现。它用一个 bitmap 来记录哪些 FD(文件描述符)需要监听。每次调用时,需要把整个 bitmap 从用户态拷贝到内核态,内核遍历所有 FD 检查状态,然后把结果拷贝回来。有两个硬伤:FD 数量上限为 1024(bitmap 大小限制),而且每次调用后 bitmap 会被修改(rset 不可复用),下次调用需要重新设置。
poll 改进了 select 的两个硬伤。它用一个 pollfd 结构体数组代替 bitmap,没有数量限制;每个结构体有独立的 revents 字段记录就绪状态,用完置零就能复用。但本质上还是遍历所有 FD,性能依然是 O(n)。
epoll 是质的飞跃。它的工作方式完全不同:
epoll_create:创建一个 epoll 实例,内核分配一块共享内存epoll_ctl:注册要监听的 FD,内核用红黑树管理这些 FDepoll_wait:等待事件发生。当某个 FD 有数据到达时,内核通过中断回调把它放到就绪链表中
用户态只需要读取就绪链表中的 FD,不需要遍历所有连接。一个 FD 在整个生命周期中只需要注册一次,不像 select/poll 每次调用都要重新传入。
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 1024 | 无限制 | 无限制 |
| 触发方式 | 遍历所有 FD | 遍历所有 FD | 事件驱动,仅返回就绪 FD |
| 性能 | O(n) | O(n) | O(1) |
| 数据拷贝 | 每次都拷贝 | 每次都拷贝 | 共享内存(mmap) |
| FD 复用 | 不可复用 | 可复用 | 可复用 |
| 内部结构 | bitmap | 结构体数组 | 红黑树 + 就绪链表 |
epoll 是目前最先进的 IO 多路复用方案,Redis、Nginx、Java NIO(在 Linux 上)都在用它。
五、Redis 6.0 为什么引入多线程?
既然单线程 + 多路复用已经很快了,为什么还要引入多线程?
5.1 问题在哪?
根据测算,Redis 将所有数据放在内存中,对于小数据包可以处理 8 万到 10 万 QPS。对 80% 的公司来说,这完全够用了。但随着业务复杂度提升,动辄上亿交易量的场景越来越多,需要更大的吞吐量。
传统方案是部署更多 Redis 实例组成集群,但资源消耗很大。经过分析发现,限制性能的主要瓶颈在网络 IO 上。
虽然 epoll 能高效地告诉你哪些连接有数据,但从内核态把数据拷贝到用户态这个过程本身是阻塞的——read() 和 write() 系统调用在数据传输时会阻塞当前线程。数据量越大、连接越多,这个拷贝耗时就越明显。
而且,在单线程模型下,一次完整的数据操作(读取 -> 解析 -> 执行 -> 写回)都在一个线程中完成,大量 CPU 时间片消耗在网络 IO 上,多核优势完全没有发挥。
5.2 6.0 的解决方案
Redis 6.0 的做法很巧妙:把最耗时的网络读写拆分给一组 IO 线程,命令执行仍然留给主线程。
客户端请求到达
↓
IO 线程组(多个)并行读取请求数据、解析协议
↓
主线程串行执行命令(单线程,无锁)
↓
IO 线程组(多个)并行将响应写回客户端这样做的好处是:
- 充分利用多核 CPU 处理网络 IO,吞吐量最高提升约 2 倍
- 命令执行仍然是单线程,不需要加锁,没有并发安全问题
- 对现有的数据结构和命令实现完全无侵入
需要注意的是,Redis 6.0 的多线程默认是关闭的,需要通过配置 io-threads 参数来开启。对于大多数场景,单线程已经足够;只有在网络 IO 成为明显瓶颈时,才需要开启多线程。
六、Redis vs Memcached
Redis 经常被拿来和 Memcached 比较。它们都是内存缓存,但设计理念差异很大。
| 对比维度 | Redis | Memcached |
|---|---|---|
| 数据结构 | String、Hash、List、Set、ZSet 等丰富类型 | 仅支持简单的 Key-Value |
| 持久化 | 支持 RDB 和 AOF | 不支持 |
| 线程模型 | 单线程执行命令(6.0 后 IO 多线程) | 多线程 |
| 集群方案 | 原生支持 Cluster(哈希槽分片) | 需要客户端自行分片 |
| 高级功能 | 事务、Lua 脚本、发布订阅、Stream | 仅基本 GET/SET |
| 内存管理 | 支持多种淘汰策略、内存碎片整理 | 预分配 Slab,管理较简单 |
| 协议 | 自定义 RESP 协议 | 文本协议 |
选型建议:如果只需要简单的 KV 缓存,且对吞吐量要求极高,Memcached 的多线程模型在简单场景下可能表现更好。但如果需要丰富的数据结构、持久化、分布式锁等功能,Redis 是更好的选择。实际工作中,Redis 的适用场景远比 Memcached 广泛,这也是 Redis 逐渐成为事实标准的原因。
七、常见面试题
Q1:Redis 为什么这么快?
四个核心原因:基于内存操作(内存响应约 100ns,比磁盘快 10 万倍)、高效的数据结构(O(1) 哈希表、O(log N) 跳表等)、IO 多路复用(epoll)让单线程也能处理高并发、单线程避免了锁竞争和上下文切换。Redis 6.0 还引入了多线程 IO 进一步提升网络吞吐。
Q2:Redis 是单线程还是多线程?
核心命令执行是单线程的,但整个 Redis 进程是多线程的。4.0 引入异步删除线程解决大 Key 阻塞问题,6.0 引入多线程网络 IO 提升吞吐量。"单线程"指的是处理客户端命令的主线程只有一个。
Q3:为什么 Redis 不用多线程执行命令?
因为 Redis 的瓶颈不在 CPU,而在内存和网络。基于内存的操作本身不需要太多计算,多线程会引入锁竞争、上下文切换等额外开销,反而可能降低性能。单线程 + IO 多路复用已经足够高效。如果确实需要利用多核的缓存系统,可以参考 Tair 或 Dragonfly,它们采用多线程模型对标 Redis。
Q4:Redis 6.0 的多线程和 Memcached 的多线程有什么区别?
Memcached 的多线程是全面的——IO 和命令执行都是多线程,需要加锁来保证线程安全。Redis 6.0 只在网络 IO 层面使用多线程,命令执行仍然是单线程,这样既提升了 IO 吞吐,又避免了并发安全问题,是一种更精巧的设计。
小结
这篇文章从 Redis 的基本定位出发,理清了单线程/多线程的演进脉络(3.x 纯单线程 → 4.x 异步删除 → 6.x 多线程 IO),深入理解了 IO 多路复用的原理(select → poll → epoll),分析了 6.0 引入多线程的动因,最后对比了 Redis 和 Memcached 的差异。
核心要记住的一句话:Redis 快,不是因为单线程本身快,而是因为它把所有操作都放在内存里,用高效的数据结构和 IO 多路复用技术,让单线程也能充分发挥性能。
下一篇我们进入 Redis 的核心用法——五大数据类型、分布式锁、布隆过滤器。
附录:Redis 8.0 新特性速览
2025 年 5 月,Redis 8.0 正式发布,带来了一些重要变化,简要梳理几个关键点:
开源协议回归
Redis 8.0 在 RSALv2/SSPLv1 基础上新增了 AGPLv3 授权选项,重新获得开源社区认可。免费版从 "Community Edition" 更名为 "Redis Open Source"。
新增数据结构
这是 8.0 最让人兴奋的部分,一口气增加了多个数据结构:
- Vector Set(Beta):专为 AI 场景设计的向量集合,支持高维向量嵌入的存储与相似性搜索。推荐系统、语义搜索等场景直接受益。
- 原生 JSON 支持:内置 JSON 数据类型,支持 JSONPath 的原子操作,不再需要手动序列化/反序列化。
- 时间序列:简化物联网传感器、股票价格、系统遥测等时序数据处理。
- 概率数据结构:内置 Bloom Filter、Cuckoo Filter、Count-min Sketch、Top-K、t-digest 五种类型。
其中布隆过滤器被原生集成这一点值得重点关注。在 8.0 之前,布隆过滤器需要安装 Rebloom 插件或者基于 Bitmap 手动实现。现在直接使用 BF.ADD、BF.EXISTS 等命令即可,大大降低了使用门槛。
性能提升
- IO 多线程优化:启用
io-threads后吞吐量最高提升 112% - 90 个常用命令延迟降低 5.4%-87.4%,其中 BITMAP 操作提升 87%
- 主从复制优化:同步时间减少 18%
面向 AI 的扩展
Redis 8.0 明显在向 AI 基础设施方向发力。Vector Set 在基准测试中实现每秒 66K-160K 向量插入,配合 Redis Copilot(自然语言 AI 助手),开发者可以用自然语言生成查询语句。
这些新特性表明 Redis 正在从"缓存中间件"向"多模数据平台"演进。不过对于大多数后端面试来说,掌握前面七节的内容就足够了。
附录:五种 IO 模型
网络编程中有五种经典的 IO 模型,Redis 的 IO 多路复用只是其中之一。了解全貌有助于理解 Redis 的设计选择。
| IO 模型 | 特点 | 类比 |
|---|---|---|
| 阻塞 IO | 线程发起 IO 请求后一直等待,直到数据就绪 | 在银行柜台排队,排到了才能办业务 |
| 非阻塞 IO | 线程发起 IO 请求后立即返回,需要反复轮询检查 | 不停问柜员"轮到我了吗?" |
| IO 多路复用 | 一个线程同时监听多个 IO 事件,有事件就绪时才处理 | 叫号系统,叫到谁谁上去 |
| 信号驱动 IO | 注册信号处理函数,数据就绪时内核发信号通知 | 柜员办好了打电话叫你 |
| 异步 IO(AIO) | 发起请求后完全不管,内核完成后直接通知结果 | 快递送货上门,你不用去取 |
Redis 选择了 IO 多路复用模型,因为它在高并发、大量连接的场景下表现最好。异步 IO 虽然理论上更优,但 Linux 上的实现(io_uring)还相对较新,Redis 暂时没有采用。
信号驱动 IO 的问题是信号处理机制本身比较复杂,且在大量连接时可能出现信号丢失。而阻塞和非阻塞 IO 的缺陷前面已经用餐厅服务员的类比解释过了。
综合来看,IO 多路复用是当前服务端网络编程的最佳实践,Redis、Nginx、Netty、Node.js 等高性能框架无一例外都采用了这种模型。
附录:Redis 集群为什么用 16384 个哈希槽?
这个知识点虽然不属于"认识 Redis"的核心内容,但面试出现频率很高,放在这里作为补充。
Redis Cluster 采用哈希槽(Hash Slot)而不是一致性哈希来做数据分片。每个 Key 通过 CRC16 算法计算后对 16384 取模,决定落在哪个槽上。集群中的每个节点负责一部分槽。
CRC16 算法本身可以产生 2^16 = 65536 个值,为什么不用 65536 个槽而是 16384(2^14)呢?三个原因:
1. 心跳包大小。 Redis 节点之间每秒都要发送 Ping/Pong 心跳包,包头里带有一个 bitmap 标记该节点负责哪些槽。16384 个槽的 bitmap 大小是 2KB(16384 / 8),而 65536 个槽则是 8KB。考虑到集群中每个节点每秒要发送多个心跳包,8KB 会造成不必要的带宽浪费。
2. 集群规模有限。 Redis 官方建议集群主节点数量不超过 1000 个。16384 个槽分给 1000 个节点,每个节点平均约 16 个槽,完全够用。
3. 压缩效率。 槽位越少,在节点较少时压缩比越高,数据传输更高效。