强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Redis 完全指南 / 03 - Redis 架构原理

Redis 架构原理

3.1 整体架构

┌─────────────────────────────────────────────────────┐
│                    Redis Server                      │
│                                                      │
│  ┌──────────┐   ┌──────────────┐   ┌──────────────┐ │
│  │ 网络层   │──→│ 事件循环      │──→│ 命令执行器    │ │
│  │ (I/O)    │   │ (Event Loop) │   │ (Command)    │ │
│  └──────────┘   └──────┬───────┘   └──────────────┘ │
│                        │                              │
│               ┌────────┴────────┐                    │
│               │                 │                    │
│        ┌──────▼──────┐  ┌──────▼──────┐             │
│        │ 时间事件     │  │ 文件事件     │             │
│        │ (Timer)     │  │ (I/O Event) │             │
│        └─────────────┘  └─────────────┘             │
│                                                      │
│  ┌──────────────────────────────────────────────┐   │
│  │              内存数据存储                      │   │
│  │   String / List / Hash / Set / ZSet / ...     │   │
│  └──────────────────────────────────────────────┘   │
│                                                      │
│  ┌──────────┐   ┌──────────┐   ┌────────────────┐   │
│  │ 持久化   │   │ 复制     │   │ 客户端管理      │   │
│  │ RDB/AOF  │   │ Replicas │   │ Client Mgmt    │   │
│  └──────────┘   └──────────┘   └────────────────┘   │
└─────────────────────────────────────────────────────┘

Redis 的核心设计理念可以用三个词概括:单线程 + 事件驱动 + 内存优先

3.2 单线程模型

为什么是单线程?

Redis 的核心命令执行模块是单线程的。这看起来违反直觉——单线程怎么能这么快?原因如下:

因素说明
纯内存操作数据在内存中,读写速度纳秒级,远快于磁盘
避免锁开销单线程无需加锁,省去了锁竞争和死锁的风险
避免上下文切换多线程的 CPU 上下文切换是性能杀手
I/O 多路复用使用 epoll/kqueue 同时监听大量连接
高效数据结构SDS、ziplist、quicklist 等内部结构经过深度优化

单线程的瓶颈与 Redis 6.0 的改进

单线程在以下场景会遇到瓶颈:

网络 I/O ← 瓶颈所在
    │
    ▼
┌──────────┐    ┌──────────┐    ┌──────────┐
│ 读取请求 │───→│ 执行命令 │───→│ 写入响应 │
│ (单线程) │    │ (单线程) │    │ (单线程) │
└──────────┘    └──────────┘    └──────────┘

Redis 6.0 引入多线程 I/O(Threaded I/O),将网络读写拆分到多个线程:

# redis.conf
# I/O 线程数(0 表示禁用,使用传统单线程)
# 建议设置为 CPU 核心数的一半,不超过 8
io-threads 4

# I/O 线程是否执行写操作
io-threads-do-reads yes
Redis 6.0+ 多线程 I/O 模型:

    Thread-1 ──→ 读请求 ─┐
    Thread-2 ──→ 读请求 ─┤
    Thread-3 ──→ 读请求 ─┼──→ 单线程执行命令 ──→ 多线程写响应
    Thread-4 ──→ 读请求 ─┘         │
                                   │
                            (命令执行仍是单线程)

⚠️ 注意:Redis 6.0 的多线程只用于 网络 I/O,命令执行仍然是单线程。这保证了线程安全,无需加锁。

3.3 事件驱动模型

Redis 使用 Reactor 模式处理事件,核心是一个无限循环(Event Loop):

// 伪代码:Redis 事件循环
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 1. 计算最近的定时事件时间
        int timeout = aeCalcTimer(eventLoop);
        
        // 2. 调用 epoll_wait / kqueue / select 等待 I/O 事件
        aeApiPoll(eventLoop, timeout);
        
        // 3. 处理文件事件(I/O 事件)
        processFileEvents();
        
        // 4. 处理时间事件(定时任务)
        processTimeEvents();
    }
}

文件事件(File Events)

由网络 I/O 触发的事件:

新连接到来 ──→ 连接应答处理器(connAcceptHandler)
客户端写入 ──→ 命令请求处理器(readQueryFromClient)
客户端读取 ──→ 命令回复处理器(sendReplyToClient)

时间事件(Time Events)

周期性触发的定时任务:

事件频率作用
serverCron10 次/秒(默认)统计信息、过期键删除、触发 RDB/AOF、主从同步
clusterCron1 次/秒集群心跳检测、故障判断
replCron1 次/秒主从复制状态管理

I/O 多路复用

Redis 会根据操作系统选择最高效的 I/O 多路复用实现:

平台实现说明
LinuxepollO(1) 事件通知,最高效
macOS/BSDkqueue类似 epoll
SolarisevportSolaris 原生事件端口
其他select通用实现,O(n) 性能较差
# 查看 Redis 使用的 I/O 多路复用机制
redis-cli INFO server | grep "multiplexing_api"
# multiplexing_api:epoll

3.4 内存管理

jemalloc 内存分配器

Redis 默认使用 jemalloc 作为内存分配器(而非 glibc 的 malloc):

# 查看使用的内存分配器
redis-cli INFO memory | grep mem_allocator
# mem_allocator:jemalloc-5.3.0
分配器特点
jemalloc默认,内存碎片率低,多线程友好
tcmallocGoogle 开发,并发性能好
libc标准 C 库,碎片率较高

Redis 对象编码(Object Encoding)

Redis 中每种数据类型底层可能使用不同的编码方式,以在性能和内存之间取得平衡:

# 查看 Key 的底层编码
redis-cli OBJECT ENCODING mykey
数据类型小数据编码大数据编码转换阈值
Stringint / embstrraw44 字节
Listlistpack(旧版 ziplist)quicklist元素数 > 128 或值 > 64 字节
Hashlistpackhashtable字段数 > 128 或值 > 64 字节
Setintset(全整数时)hashtable元素数 > 128 或含非整数
ZSetlistpackskiplist + hashtable元素数 > 128 或值 > 64 字节

Redis 7.0+ 使用 listpack 替代了旧版的 ziplist,解决了 ziplist 的级联更新(cascade update)问题。

SDS(Simple Dynamic String)

Redis 不使用 C 原生字符串,而是自研了 SDS

// SDS 结构体(sdshdr8 示例)
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;        // 已使用长度
    uint8_t alloc;      // 分配的总长度
    unsigned char flags; // 类型标志
    char buf[];          // 实际数据(以 '\0' 结尾,兼容 C 字符串函数)
};

SDS 相比 C 字符串的优势:

特性C 字符串SDS
获取长度O(n) 遍历O(1) 直接读 len
缓冲区溢出可能不可能(自动扩展)
二进制安全❌(遇 \0 截断)✅(按 len 读取)
内存预分配减少 realloc 次数
惰性释放缩短时不立即回收

跳表(Skip List)

Sorted Set(ZSet)在元素较多时使用 跳表 + 哈希表 的双结构:

Level 3: HEAD ─────────────────────→ 9 ─────→ NIL
Level 2: HEAD ──────→ 4 ───────────→ 9 ─────→ NIL
Level 1: HEAD ──→ 2 ──→ 4 ──→ 7 ──→ 9 ──→ NIL

跳表的查询时间复杂度为 O(log N),与平衡树相当,但实现更简单,范围查询更高效。

3.5 RESP 协议

RESP(REdis Serialization Protocol)是 Redis 客户端与服务端之间的通信协议。

RESP 数据类型

类型前缀示例说明
Simple String++OK\r\n简单状态回复
Error--ERR unknown command\r\n错误回复
Integer::1000\r\n整数回复
Bulk String$$5\r\nhello\r\n二进制安全的字符串
Array**2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n数组

手动发送 RESP 协议

# 使用 telnet 直接与 Redis 通信
telnet localhost 6379

# 发送 SET hello world(RESP 编码)
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n

RESP3(Redis 6.0+)

Redis 6.0 引入了 RESP3 协议,增加了更多数据类型:

# 切换到 RESP3
redis-cli HELLO 3

# RESP3 新增类型
# - Double:   ,3.14\r\n
# - Boolean:  #t\r\n / #f\r\n
# - Blob Error: !4\r\nERR\r\n
# - Map:      %2\r\n...
# - Set:      ~2\r\n...
# - Push:     >2\r\n...

3.6 命令处理流程

一个完整的命令从客户端到服务端的处理流程:

客户端
  │
  │ 1. 发送命令(RESP 编码)
  ▼
网络层(epoll 监听可读事件)
  │
  │ 2. 读取数据到输入缓冲区
  ▼
命令解析器
  │
  │ 3. 解析 RESP 协议,提取命令和参数
  ▼
命令执行器
  │
  │ 4. 查找命令处理函数
  │ 5. 执行前的准备工作(权限检查、参数校验等)
  │ 6. 调用命令处理函数
  │ 7. 执行后续工作(传播到 AOF/从节点、慢查询记录等)
  ▼
输出缓冲区
  │
  │ 8. 将响应写入输出缓冲区
  ▼
网络层(epoll 监听可写事件)
  │
  │ 9. 发送响应给客户端
  ▼
客户端

代码验证

# 启动 Redis 服务端(前台模式,可看到日志)
redis-server --loglevel verbose

# 另一个终端执行命令
redis-cli SET test "hello"
redis-cli GET test

# 在服务端日志中可以看到完整处理流程

3.7 单线程 vs 多线程 vs 多实例

方案优点缺点
单线程(Redis 默认)无线程安全问题,代码简单单核 CPU 上限
多线程 I/O(Redis 6.0+)网络 I/O 性能提升 1-2 倍命令执行仍是单线程
多实例部署充分利用多核,实例间隔离运维复杂,数据分散
# 验证:多线程 I/O 效果对比
# 关闭 I/O 线程
redis-cli CONFIG SET io-threads 1
redis-benchmark -t set,get -n 100000 -c 256 -q

# 开启 4 个 I/O 线程
redis-cli CONFIG SET io-threads 4
redis-benchmark -t set,get -n 100000 -c 256 -q

💡 技巧:如果单实例 CPU 达到瓶颈,推荐使用 Redis Cluster 多分片方案,而非在单实例上启用多线程。


📌 业务场景

场景一:高并发计数

利用单线程的原子性特性,INCR 命令天然线程安全。在电商秒杀中使用 DECR 做库存扣减,无需加锁:

# 初始化库存
SET stock:sku001 1000

# 扣减库存(原子操作)
DECR stock:sku001
# 返回值 >= 0 表示扣减成功,< 0 表示库存不足

场景二:Pipeline 批量写入

单线程模型下,网络往返(RTT)是性能瓶颈。使用 Pipeline 批量发送命令:

import redis
r = redis.Redis()
pipe = r.pipeline()
for i in range(10000):
    pipe.set(f"key:{i}", f"value:{i}")
pipe.execute()  # 一次性发送所有命令

场景三:大 Key 避免阻塞

单线程意味着一个慢命令会阻塞所有请求。避免使用 KEYS *、大 Key 的 LRANGE 等:

# ❌ 危险:可能阻塞数秒
KEYS user:*

# ✅ 安全:增量遍历
SCAN 0 MATCH user:* COUNT 100

🔗 扩展阅读