文章目录
本文从内存、线程模型、IO多路复用、数据结构、内存管理、网络协议、多线程IO等多个维度,由浅入深地剖析Redis高性能的本质。无论你是刚入门的后端开发者,还是经验丰富的架构师,都能从中获得新的理解。
前言
“Redis为什么快?”是面试中出场率极高的问题。很多候选人的回答停留在:
- 基于内存
- 单线程
- IO多路复用
- 高效的数据结构
这些答案没错,但过于表面。面试官想听到的是:为什么基于内存就快?单线程为什么反而快?IO多路复用具体怎么工作?数据结构高效在哪里? 本文带你层层递进,深入Redis高性能的每一个细节。
一、第一层:基于内存,但不仅仅是内存
1.1 内存与磁盘的速度差异
- 内存随机访问延迟:约 100纳秒
- SSD随机读延迟:约 100微秒
- 机械磁盘随机读延迟:约 10毫秒
内存比磁盘快 10万倍 以上。Redis将所有数据放在内存中,读写操作直接在内存完成,这是它快的根本原因。
1.2 但内存数据库不止Redis
Memcached同样基于内存,为什么Redis更受欢迎?因为Redis在内存之上设计了丰富且高效的数据结构,并解决了内存数据库的持久化、高可用、分布式等问题。所以“基于内存”只是起点,不是全部。
二、第二层:单线程模型——为什么单线程反而快?
2.1 Redis的单线程指的是什么?
Redis 6.0之前,命令执行和网络IO都在一个线程中完成。Redis 6.0引入多线程IO,但命令执行仍然是单线程。
2.2 单线程的优势
-
避免线程切换开销
线程切换需要保存/恢复上下文,消耗CPU时间。单线程无需切换,CPU利用率更高。 -
避免锁竞争和死锁
多线程访问共享数据需要加锁,锁竞争会降低性能,死锁更是灾难。单线程天然无锁,操作原子性有保障。 -
CPU缓存友好
单线程执行时,数据更可能命中CPU缓存,减少内存访问延迟。 -
代码简单,可维护性高
无并发问题,逻辑清晰,bug少。
2.3 单线程能利用多核吗?
单线程只能利用一个CPU核心。但Redis通过部署多个实例(分片)来利用多核。例如,一台32核服务器可以部署多个Redis实例,每个实例绑定不同CPU核心。
2.4 单线程的瓶颈
单线程的瓶颈在网络IO。当QPS很高时,读写socket、解析协议会占用大量CPU时间,导致命令执行被延迟。这就是Redis 6.0引入多线程IO的原因(后文详述)。
三、第三层:IO多路复用与事件驱动
3.1 传统阻塞IO的问题
每个客户端连接创建一个线程,线程阻塞在read上。大量连接导致大量线程,内存和切换开销巨大。
3.2 IO多路复用
IO多路复用允许一个线程监视多个文件描述符,当某个描述符就绪时,通知程序处理。常见实现:
select:O(n)轮询,有文件描述符数量限制(1024)poll:链表结构,无数量限制,仍O(n)epoll(Linux):红黑树+就绪链表,O(1)获取就绪事件kqueue(BSD/macOS)
Redis根据操作系统选择最佳实现,Linux下使用epoll。
3.3 Redis的Reactor模式
Redis使用单Reactor单线程模型(6.0前):
aeEventLoop:事件循环aeFileEvent:文件事件,包括可读、可写aeTimeEvent:时间事件,如定时任务
伪代码:
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
// 计算最近的时间事件
// 调用epoll_wait等待事件
int numevents = aeApiPoll(eventLoop, tvp);
for (int j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
if (fe->mask & AE_READABLE) fe->rfileProc(...);
if (fe->mask & AE_WRITABLE) fe->wfileProc(...);
}
// 处理时间事件
processTimeEvents(eventLoop);
return numevents;
}
3.4 为什么单线程+epoll能支持高并发?
- 非阻塞IO:socket设置为非阻塞,读写不会阻塞线程。
- 事件驱动:epoll_wait返回就绪的fd,只处理有数据的连接,避免空转。
- 单线程处理:没有线程切换和锁,效率极高。
- 内存操作快:命令执行在内存完成,极快。
因此,Redis可以轻松支持数万甚至十万级别的QPS。
四、第四层:高效的数据结构
Redis的高性能不仅因为内存,还因为为每种场景设计了最优的数据结构。
4.1 SDS(简单动态字符串)
- O(1)获取长度:
len字段。 - 二进制安全:不以
\0结尾,可存任意二进制。 - 预分配:扩容时多分配空间,减少内存分配次数。
- 惰性释放:缩短字符串时不立即回收内存,方便后续追加。
struct sdshdr {
int len; // 已使用长度
int free; // 剩余空间
char buf[]; // 数据
};
4.2 字典(哈希表)
- 渐进式rehash:扩容时,不一次性迁移所有键值,而是每次操作迁移一个桶。避免阻塞主线程。
- 两个哈希表:
ht[0]和ht[1],rehashidx记录迁移进度。
typedef struct dict {
dictht ht[2];
int rehashidx; // -1表示未rehash
} dict;
rehash过程:每次增删改查时,调用_dictRehashStep迁移一个桶,直到完成。
4.3 跳表(zset)
- 查询O(logN),插入删除O(logN)。
- 实现比平衡树简单,支持范围查询。
- 多层索引,平均层数logN。
typedef struct zskiplistNode {
double score;
sds ele;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
4.4 整数集合(intset)
- 当set中全是整数且数量少时,使用intset。
- 有序数组,节省内存。
- 升级机制:插入更大整数时,整体升级类型。
4.5 压缩列表(ziplist)与listpack
- ziplist:连续内存块,减少指针开销。但存在连锁更新问题。
- listpack:Redis 7.0引入,替代ziplist,解决连锁更新。
- quicklist:ziplist+双向链表,平衡内存和性能。Redis 7.0后使用listpack。
4.6 各数据结构时间复杂度
| 数据结构 | 用途 | 操作 | 时间复杂度 |
|---|---|---|---|
| SDS | 字符串 | 获取长度 | O(1) |
| 字典 | Hash | 增删改查 | O(1) |
| 跳表 | ZSet | 增删改查 | O(logN) |
| 整数集合 | Set | 增删查 | O(N) |
| 压缩列表 | List/Hash/ZSet | 增删查 | O(N) |
| quicklist | List | 两端插入 | O(1) |
五、第五层:内存管理与分配
5.1 jemalloc
Redis默认使用jemalloc作为内存分配器。它:
- 减少内存碎片
- 多线程arena,提高并发分配效率
- 快速分配/释放
5.2 共享对象
Redis预分配了0~9999的整数对象,多个键共享同一对象,节省内存。
5.3 引用计数
每个对象有refcount,为0时回收内存。
5.4 内存碎片整理
Redis 4.0引入activedefrag,自动整理内存碎片。
六、第六层:网络协议与客户端缓冲
6.1 RESP协议
- 简单:文本协议,解析快。
- 可读性好:便于调试。
- 支持多种类型:简单字符串
+、错误-、整数:、批量字符串$、数组*。
6.2 Pipeline
批量发送命令,减少RTT(往返时间),大幅提升吞吐量。
6.3 客户端输出缓冲区
Redis为每个客户端维护输出缓冲区,避免写阻塞。但大key可能导致缓冲区溢出,需注意。
七、Redis 6.0多线程IO:为什么引入?如何工作?
7.1 引入原因
单线程网络IO成为瓶颈。在高并发下,读写socket、解析协议占用大量CPU,导致命令执行延迟。
7.2 多线程IO模型
- 主线程:负责命令执行,保持原子性。
- IO线程:负责读取客户端请求、解析命令、写回响应。
- 默认关闭:
io-threads 4开启,建议设置为CPU核数的3/4。
7.3 工作流程
- 主线程将可读事件分发给IO线程。
- IO线程读取请求并解析,放入队列。
- 主线程从队列取命令并执行。
- 主线程将响应交给IO线程写回。
命令执行仍然是单线程,无需加锁。
7.4 性能提升
官方测试:4 IO线程下,QPS提升约1倍。
八、其他优化:持久化、集群、CPU亲和性
8.1 持久化
- RDB:fork子进程,写时复制,不阻塞主线程。
- AOF:追加写,每秒fsync,可配置。
8.2 集群
- 分片:数据分散到多个节点,横向扩展。
- 哨兵:高可用。
8.3 CPU亲和性
使用taskset将Redis绑定到特定CPU核心,减少上下文切换。
九、总结与面试回答模板
9.1 面试回答模板
“Redis快的原因可以从多个层面来看:
- 内存存储:所有数据在内存,读写延迟纳秒级。
- 单线程命令执行:避免线程切换和锁竞争,CPU缓存友好。6.0后引入多线程IO,但命令执行仍单线程。
- IO多路复用:基于epoll的事件驱动,单线程处理大量连接。
- 高效数据结构:SDS、字典(渐进式rehash)、跳表、listpack等,针对场景优化。
- 内存管理:jemalloc减少碎片,共享对象节省内存。
- 网络协议:RESP简单高效,支持Pipeline。
- 持久化与集群:fork子进程不阻塞,分片横向扩展。”
9.2 深入点
- 渐进式rehash如何避免阻塞?
- 跳表为什么比平衡树更适合zset?
- listpack如何解决连锁更新?
- 多线程IO如何分工?
- epoll的红黑树和就绪链表原理?
掌握这些,面试官一定会对你刮目相看。

337

被折叠的 条评论
为什么被折叠?



