很多工程师第一次接触 Redis 时,都会产生一个误解:
Redis = 内存 Key-Value 缓存
但当系统规模逐渐扩大之后,你会发现 Redis 的行为远远不止“缓存”这么简单。
例如:
- 排行榜系统突然变慢
- 分布式锁偶尔出现 双持锁
- 限流器偶尔 放过超额请求
- Redis 实例 周期性出现延迟尖刺
这些问题的根源,几乎都与 Redis 架构设计有关。
Redis 并不是简单地因为“在内存里”而快,而是依赖一套非常克制且精妙的架构设计:
- 单线程执行模型
- I/O 多路复用
- 高效的数据结构实现
- 特殊的持久化机制
- Lua 原子执行
理解这些设计之后,你会发现:
Redis 本质是一个 **高性能数据结构服务器(Data Structure Server)**。
本文将从 架构层面拆解 Redis 为什么可以如此高性能。
Redis 架构总览
Redis 的核心执行模型非常简单:
Client Connections
│
│
I/O Multiplexing
(epoll / kqueue / select)
│
│
Command Processing
(Single Thread)
│
┌──────────────┼───────────────┐
│ │ │
Data Structures Persistence Replication
(Memory Store) (RDB / AOF) (Replica)
关键特点:
| 模块 | 职责 |
|---|---|
| I/O 多路复用 | 同时处理成千上万连接 |
| 单线程执行 | 顺序执行命令 |
| 数据结构层 | 高效内存存储 |
| 持久化 | RDB / AOF |
| 集群 | 横向扩展 |
Redis 的性能优势来自 架构整体设计,而不是某一个点优化。
Redis 为什么选择单线程
Redis 最令人困惑的一点:
Redis 的命令执行是 单线程
在多核 CPU 普及的今天,这看起来像是反模式。
但实际上,Redis 正是因为单线程,才获得了极高性能。
消除了并发问题
在多线程系统中,几乎所有共享数据结构都需要:
- mutex
- rwlock
- CAS
- 原子变量
而 Redis 完全不需要。
例如:
// 两个客户端同时执行
INCR counter
执行顺序:
Thread model:
Client A -> INCR counter -> result=1
Client B -> INCR counter -> result=2
Redis 天然保证:
- 原子性
- 无竞争
- 无锁
避免上下文切换
多线程系统会产生:
- 线程调度
- CPU context switch
- lock contention
这些都会产生性能损耗。
Redis 的执行流程:
loop {
read request
execute command
write response
}
完全没有线程调度成本。
性能更加稳定
多线程系统常见问题:
- Lock contention
- CPU cache invalidation
- Priority inversion
Redis 的性能更加 **可预测(Predictable latency)**。
单线程为什么还能处理大量连接
如果只有一个线程,Redis 怎么能处理:
- 几万连接
- 每秒十万请求
答案是:
I/O 多路复用
I/O 多路复用模型
Redis 使用操作系统提供的机制:
| OS | 机制 |
|---|---|
| Linux | epoll |
| MacOS / BSD | kqueue |
| 旧系统 | select |
工作方式:
┌───────────────┐
Client1 ──────►│ │
Client2 ──────►│ epoll/kqueue │
Client3 ──────►│ │
Client4 ──────►│ │
└───────┬───────┘
│
Redis Event Loop
│
sequential command execution
流程:
- Redis 注册所有 socket
- OS 监控 socket 事件
- 数据到达时通知 Redis
- Redis 读取并执行命令
关键点:
Redis 不是为每个连接创建线程
而是:
一个线程处理所有连接
实际执行过程
例如:
客户端发送请求:
Client A: SET user:1 "John"
Client B: GET user:1
Client C: INCR counter
Redis 执行顺序:
1 SET user:1 "John"
2 GET user:1
3 INCR counter
假设每个操作耗时:
SET ≈ 500ns
GET ≈ 300ns
INCR ≈ 400ns
总耗时:
1.2 microseconds
因此 客户端几乎同时收到响应。
Redis 的真正核心:数据结构设计
Redis 的名字其实就说明了一切:
REmote DIctionary Server
它本质是一个 数据结构服务器。
String:并不只是字符串
Redis String:
- 二进制安全
- 最大 512MB
但内部实现有多种编码:
| 类型 | 编码 |
|---|---|
| 整数 | int |
| 短字符串 | embstr |
| 长字符串 | raw |
示例:
SET counter 42
INCR counter
Redis 会直接进行 整数运算。而不是字符串解析。
List:双端队列
Redis List 支持:
- 队列
- 栈
- 消息队列
示例:
队列
LPUSH queue task1
LPUSH queue task2
RPOP queue
执行顺序:
task1
task2
栈
LPUSH stack a
LPUSH stack b
LPOP stack
结果:
b
复杂度
| 操作 | 复杂度 |
|---|---|
| LPUSH | O(1) |
| RPOP | O(1) |
| LINDEX | O(N) |
陷阱
访问中间元素:
LINDEX list 50000
时间复杂度:
O(N)
在大列表中会明显变慢。
Sorted Set:Redis 的王牌
Sorted Set 是 Redis 最强大的结构之一。
典型场景:
- 排行榜
- 实时排名
- 延迟任务
- 优先级队列
示例:
ZADD leaderboard 9500 player1
ZADD leaderboard 8700 player2
查询 Top10:
ZREVRANGE leaderboard 0 9
查询排名:
ZREVRANK leaderboard player1
内部结构
Sorted Set 使用:
hash table + skip list
Skip List (按 score 排序)
│
│
Hash Table
(member -> score)
优势:
| 操作 | 复杂度 |
|---|---|
| 插入 | O(logN) |
| 删除 | O(logN) |
| 排名 | O(logN) |
| 查分数 | O(1) |
因此非常适合:
百万级排行榜系统
代价是什么
Sorted Set 内存占用较高。
因为每个成员需要存两份:
skip list node
hash entry
例如:
1000 万排行榜
≈ 500MB 内存
Hash:Redis 的内存优化利器
假设存储用户信息:
方案 1:多个 Key
SET user:1:name John
SET user:1:age 30
SET user:1:email a@b.com
Key 数量:
3 keys
方案 2:Hash
HSET user:1 name John
HSET user:1 age 30
HSET user:1 email a@b.com
Key 数量:
1 key
内部优化:ziplist
当满足条件:
字段 < 512
value < 64 byte
Redis 使用 紧凑编码:
ziplist / listpack
内存节省可达:
60%+
在大规模用户数据系统中非常关键。
总结
Redis 高性能来自于几项核心设计:
| 设计 | 作用 |
|---|---|
| 单线程执行 | 消除锁竞争 |
| I/O 多路复用 | 支持大量连接 |
| 高效数据结构 | 降低复杂度 |
| 紧凑内存编码 | 节省内存 |
但这些设计也带来新的问题:
- 慢命令会阻塞整个实例
- 持久化可能造成延迟
- 内存碎片可能膨胀
- 多命令操作可能产生竞态
因此,在下一篇中我们将继续深入:
- Redis 持久化机制(RDB / AOF)
- Lua 脚本为什么存在
- Redis 内存管理
- Pipeline 与事务
- Redis Cluster 架构