Redis 8 深度解析:从命令执行、内存管理到持久化、复制与 Cluster
本文面向已经使用过 Redis、希望进一步理解其内部执行机制与生产设计边界的开发者和架构师。
版本基线:截至 2026 年 7 月 27 日,Redis Open Source 当前稳定系列为 8.8,本文以 Redis 8.8.1 源码与最新版官方文档为主要依据;涉及 Redis 8.0 引入的 I/O 线程重构、Redis 7.0 延续至 Redis 8 的 Multi-Part AOF 等机制时,会明确说明版本边界。
文中的容量公式属于工程估算模型,不是 Redis 内部的固定计算公式。任何生产容量结论都应通过真实数据、真实编码、真实客户端和真实写入速率进行压测验证。
文中架构图均使用 Mermaid 代码块。发布到 Hexo 前,请确认当前主题或 Markdown 渲染链路已经启用 Mermaid。
一、先建立一张 Redis 8 全景图
Redis 常被概括为“单线程内存数据库”,这个说法只说对了一半。
更准确的描述是:
Redis 8 以内存数据结构为核心,以事件驱动模型处理连接,以 I/O 线程并行完成部分网络读取、协议解析与响应写回,以主执行线程串行执行绝大多数核心命令,再通过后台线程或子进程完成异步释放、AOF 刷盘、RDB 生成和 AOF 重写等工作。
这套设计解决了几个相互冲突的问题:
- 核心键空间操作尽量避免锁竞争;
- 单条普通命令天然具备清晰的串行执行边界;
- 网络 I/O 不再完全受限于单个线程;
- 磁盘持久化、内存释放等慢操作尽可能移出主执行路径;
- 通过复制和 Cluster,将单实例能力扩展为高可用与水平分片能力。
flowchart TB
Client[客户端<br/>RESP2 / RESP3] --> Net[Socket 与事件循环<br/>epoll / kqueue / select]
Net --> IO[I/O 线程<br/>读取、协议解析、响应写回]
IO --> Main[主执行线程]
Main --> Gate[命令入口检查<br/>ACL / OOM / Cluster / 状态校验]
Gate --> Proc[命令处理函数<br/>操作内存数据结构]
Proc --> Reply[客户端响应缓冲区]
Reply --> IO
Proc --> AOF[AOF Buffer / fsync]
Proc --> Repl[复制流 / Replication Backlog]
Proc --> Notify[通知、慢日志、统计、缓存失效]
Main --> BG[后台任务]
BG --> Lazy[Lazy Free]
BG --> RDB[RDB 子进程]
BG --> Rewrite[AOF Rewrite 子进程]
理解 Redis,不能只看某一个命令的时间复杂度,还必须把下面四条链路放在一起:
- 前台链路:请求读取 → 协议解析 → 命令校验 → 命令执行 → 返回响应;
- 内存链路:对象编码 → 分配器 → 碎片 → RSS → fork Copy-on-Write;
- 数据安全链路:AOF/RDB → 主从复制 → 故障转移;
- 分布式链路:Hash Slot → 客户端路由 → 节点故障判定 → Replica 晋升。
二、Redis 8 整体执行逻辑
本章主要依据:Redis 8.0 Release Notes、networking.c 与 server.c。
2.1 Redis 启动阶段做了什么
Redis 启动并不是简单地“读取配置后监听 6379”。典型过程如下:
flowchart TD
Start[进程启动] --> Config[加载配置文件与启动参数]
Config --> Init[初始化 server 状态、数据库、命令表、ACL]
Init --> Modules[加载内置组件或外部模块]
Modules --> Persist{是否存在可恢复数据}
Persist -->|AOF 可用| LoadAOF[加载 AOF]
Persist -->|仅 RDB| LoadRDB[加载 RDB]
Persist -->|无持久化| Empty[空数据集启动]
LoadAOF --> Listen[创建 TCP / Unix Socket]
LoadRDB --> Listen
Empty --> Listen
Listen --> Events[注册文件事件与时间事件]
Events --> Loop[进入主事件循环]
启动阶段主要完成:
- 初始化
redisServer全局状态; - 创建逻辑数据库及键空间;
- 注册命令表,将命令名映射到具体处理函数;
- 初始化 ACL、慢日志、延迟监控、客户端缓存等子系统;
- 根据配置加载 Redis 8 集成的数据能力;
- 根据持久化配置恢复数据;
- 创建监听 Socket;
- 注册连接、读写、时间事件;
- 启动 I/O 线程及后台线程;
- 进入事件循环。
如果同时启用 AOF 与 RDB,Redis 重启时优先使用 AOF 恢复,因为 AOF 通常保存了更完整的变更历史。这里的“优先”不代表 AOF 一定比 RDB 更可靠,最终可靠性仍取决于 appendfsync、磁盘、文件完整性和备份策略。
2.2 从 TCP 字节到 Redis 命令
客户端通常通过 RESP2 或 RESP3 发送请求。例如:
1 | |
编码成 RESP 后类似:
1 | |
Redis 收到字节后,不会立刻调用 setCommand(),而是先经历:
- Socket 可读事件触发;
- 数据进入客户端查询缓冲区;
- RESP 解析器判断是否已经得到一条完整命令;
- 构造
argc/argv; - 识别命令、参数和涉及的 Key;
- 将命令交给主执行线程;
- 进入统一命令入口进行校验;
- 调用具体命令处理函数。
在 Redis 8.8.1 源码中,networking.c 的解析代码会把请求转换成待执行命令结构;server.c 中的 processCommand() 再完成真正的命令入口控制。
2.3 Redis 8 的 I/O 线程到底做什么
Redis 6 已经引入 I/O 多线程,Redis 8 又对 I/O 线程模型做了重构和性能优化。理解这一点时,必须避免两个极端误解:
- 错误理解一:Redis 8 还是彻底的单线程;
- 错误理解二:Redis 8 的所有命令都能在多个线程并行修改数据。
对于 Redis 核心命令,更接近真实情况的是:
sequenceDiagram
participant C as Client
participant IO as I/O Thread
participant M as Main Execution Thread
participant D as In-memory Dataset
C->>IO: 发送 RESP 请求
IO->>IO: 读取 Socket、解析完整命令
IO->>M: 加入主线程待执行队列
M->>M: 命令查找与统一校验
M->>D: 串行执行核心数据操作
D-->>M: 返回执行结果
M->>IO: 响应进入输出队列
IO-->>C: 写回 Socket
也就是说:
I/O 可以并行,绝大多数核心键空间命令仍在主执行线程中串行执行。
这样设计的主要收益是:
- 网络读取、RESP 解析、响应写回可以利用多核;
- 核心数据结构不需要普遍引入细粒度锁;
- 命令顺序和原子边界仍然清晰;
- 减少共享数据竞争带来的复杂度。
源码中,当命令在非主 I/O 线程完成解析后,会设置待执行标记并进入主线程队列,而不是直接在该 I/O 线程里调用核心命令处理函数。
需要注意,Redis 8 集成的 Search、Vector、TimeSeries 等能力可能使用自己的 Worker 或协作执行机制。因此,“核心命令主要串行执行”不能机械地推广为“Redis 进程内任何计算都只能使用一个线程”。
2.4 processCommand():真正的命令总闸门
命令解析完成后,会进入统一入口。这个入口不是简单的 switch-case,而是一组安全性和运行状态检查。
典型检查包括:
1. 命令存在性和参数数量
Redis 根据命令表查找 SET、GET、HSET 等命令,并检查参数数量。不存在的命令或错误参数会在真正访问数据之前被拒绝。
2. ACL 和认证
检查当前连接是否已经认证,以及用户是否有权:
- 执行当前命令;
- 访问当前 Key;
- 访问当前命令类别;
- 使用受保护的管理命令。
3. Cluster Slot
Cluster 模式下,Redis 会提取命令涉及的 Key 并计算 Slot。如果多 Key 命令跨 Slot,通常返回 CROSSSLOT;如果请求发到错误节点,则返回 MOVED 或 ASK。
4. 内存与淘汰
写命令执行前可能触发内存检查和淘汰。如果无法释放足够内存,并且命令属于会增加内存的类型,Redis 返回 OOM 错误。
5. 实例状态
包括:
- 当前是否正在加载数据;
- 当前节点是否为只读 Replica;
- AOF/RDB 是否出现阻止写入的错误;
- 是否满足
min-replicas-to-write等写安全条件; - 是否处于暂停状态;
- 是否有超时脚本或繁忙模块;
- Pub/Sub 客户端是否允许执行该命令;
- 是否需要延迟或阻塞当前客户端。
通过这些检查后,Redis 才会进入命令处理函数。
2.5 call() 与具体命令处理函数
概念上,命令执行链路可简化为:
1 | |
其中:
processCommand()负责统一入口与状态检查;call()负责执行前后统计、传播控制和慢日志等工作;command->proc(client)是具体命令函数,如setCommand()、getCommand();addReply()一类函数把结果放入客户端输出缓冲区。
一次写命令完成后,还可能触发:
- AOF 传播;
- 复制流传播;
- Keyspace Notification;
- Client Tracking 失效消息;
- 慢日志和命令统计;
- 被阻塞客户端唤醒;
- 过期或淘汰统计更新。
因此,“命令执行完”并不等于“只修改了一个 Hash 表”。Redis 还要维护整套可观测性、持久化和复制语义。
2.6 一个 SET 命令的完整链路
假设执行:
1 | |
flowchart TD
A[客户端发送 RESP] --> B[I/O 线程读取并解析]
B --> C[构造 argv<br/>SET order:1001 PAID EX 60]
C --> D[主线程 processCommand]
D --> E{检查}
E --> E1[命令与参数]
E --> E2[ACL]
E --> E3[Cluster Slot]
E --> E4[maxmemory / OOM]
E --> E5[实例角色与持久化状态]
E --> F[调用 SET 处理函数]
F --> G[写入键空间]
G --> H[写入绝对过期时间]
H --> I[生成 OK 响应]
I --> J[AOF / 复制传播]
J --> K[I/O 线程写回客户端]
需要区分几个时间点:
- 内存数据已经更新;
- 响应已经进入输出缓冲区;
- 响应已经到达客户端;
- AOF 已经
write(); - AOF 已经
fsync(); - Replica 已经接收;
- Replica 已经处理;
- RDB 已经包含这条数据。
这些不是同一个时刻。Redis 的性能来自对这些时刻进行解耦,而数据安全设计恰恰要明确业务需要保证到哪一个时刻。
2.7 为什么慢命令仍然危险
即使启用了多个 I/O 线程,下面这些操作仍可能长期占用主执行线程:
- 对超大 Key 执行
HGETALL、SMEMBERS; - 返回数百万成员的范围查询;
- 大集合的交并差运算;
- 复杂 Lua 脚本或 Function;
KEYS *;- 大 Key 同步删除;
- 某些高复杂度模块命令;
- 在单次 Pipeline 中堆积过量请求与响应。
I/O 线程好比增加了收银、传菜和打包人员,但厨房里某道核心菜仍可能占住主灶台。网络吞吐提高了,并不会自动消除算法复杂度和大对象处理成本。
三、过期键、内存淘汰策略与事务
本章主要依据:EXPIRE、Key Eviction、Transactions、expire.c 与 evict.c。
3.1 过期和淘汰不是一回事
二者经常被混为一谈:
| 机制 | 触发原因 | 是否需要 maxmemory |
目标 |
|---|---|---|---|
| 过期 Expiration | Key 的 TTL 到期 | 否 | 删除已经失效的数据 |
| 淘汰 Eviction | 实例达到内存上限 | 是 | 为新写入腾出内存 |
一个 Key 可能:
- 因 TTL 到期被删除;
- 在 TTL 到期前因内存压力被淘汰;
- 永不过期,但被
allkeys-*策略淘汰; - 永不过期且在
volatile-*策略下永远不成为候选者。
3.2 Redis 如何保存 TTL
Redis 不是给每个 Key 启动一个定时器。那样在数千万 Key 下会产生无法接受的调度成本。
逻辑上,Redis 为有过期时间的 Key 维护额外的过期元数据。过期时间使用绝对时间戳,因此:
- Redis 停机期间,时间仍然流逝;
- 重启后,已过期的 Key 会被视为过期;
- 系统时钟异常跳变可能影响 TTL 行为;
- 分布式锁等依赖 TTL 的场景应正确配置时间同步,并理解墙上时钟变化的风险。
Redis 8 还支持 Hash Field Expiration,即可以给 Hash 的字段设置过期时间。它改变了过去“一个 Hash 只能整体过期”的建模限制,但字段级 TTL 同样会带来额外元数据和扫描成本,使用前仍需测量实际内存。
3.3 被动过期:访问时发现
当客户端访问某个 Key 时,Redis 会检查它是否已经过期:
flowchart LR
A[访问 Key] --> B{是否存在 TTL}
B -->|否| C[正常读取]
B -->|是| D{当前时间是否超过过期时间}
D -->|否| C
D -->|是| E[删除 Key]
E --> F[表现为 Key 不存在]
优点是简单、精准,不需要扫描全部 Key。
问题是:如果一个过期 Key 再也没有被访问,它就可能继续占用内存。因此 Redis 还需要主动过期。
3.4 主动过期:自适应抽样清理
Redis 周期性从带 TTL 的键空间中抽样,删除已经过期的 Key,并根据抽样中过期比例决定是否继续。
核心思想不是“每秒扫描所有 Key”,而是:
- 从过期字典中抽样;
- 删除样本中的过期 Key;
- 如果样本中过期比例仍高,继续一轮;
- 受 CPU 和时间预算约束,避免过期清理长期霸占主线程;
- 在普通周期和事件循环休眠前存在不同强度的清理机会。
active-expire-effort 可以在 CPU、延迟和过期内存滞留之间调整。值越大,Redis 越积极地寻找和删除过期 Key,但也会消耗更多 CPU,并可能增加延迟。
主动过期是一种概率和预算控制算法,所以:
TTL 到期表示 Key 从语义上已经失效,不保证对应内存会在那个毫秒立即归还给操作系统。
3.5 过期删除如何保持主从一致
Primary 负责决定 Key 何时真正删除,并将删除语义传播到 Replica 和 AOF。Replica 不应完全依赖自己的本地时钟独立决定最终数据历史,否则主从节点可能因为时钟或调度差异产生不同结果。
因此,复制链路中不仅包含客户端写命令,也包含:
- Key 过期产生的删除;
- 内存淘汰产生的删除;
- 其他会改变数据集的内部动作。
3.6 maxmemory 到底限制了什么
达到 maxmemory 时,Redis 会根据策略尝试释放 Key。但并不是 Redis 进程的所有内存都严格计入淘汰判断。
例如下列内存可能形成额外峰值:
- 复制积压缓冲区;
- Replica 输出缓冲区;
- AOF Buffer;
- AOF Rewrite 相关缓冲;
- 普通客户端输出缓冲;
- Cluster Bus 与迁移缓冲;
- allocator 碎片;
- fork 后 Copy-on-Write 页面;
- Redis 进程代码、栈和其他非数据集内存。
INFO memory 中的 mem_not_counted_for_evict 用于描述不计入 Key 淘汰判断的一部分内存,主要包括临时复制与 AOF 缓冲。这也是为什么把 maxmemory 直接配置成宿主机物理内存大小非常危险。
3.7 Redis 8 当前淘汰策略
| 策略 | 候选范围 | 选择依据 | 适合场景 |
|---|---|---|---|
noeviction |
不淘汰 | 内存不足时拒绝部分写命令 | 数据不可随意丢失、由业务控制容量 |
allkeys-lru |
所有 Key | 近似淘汰最久未访问 | 通用缓存,热点明显 |
allkeys-lfu |
所有 Key | 近似淘汰访问频率最低 | 长期热点比短期最近访问更重要 |
allkeys-lrm |
所有 Key | 近似淘汰最久未修改 | 读多写少,希望保留持续更新的数据 |
allkeys-random |
所有 Key | 随机 | 访问分布接近均匀或实现极简 |
volatile-lru |
仅有 TTL 的 Key | 近似 LRU | 只允许缓存数据被淘汰 |
volatile-lfu |
仅有 TTL 的 Key | 近似 LFU | 有 TTL 的频率型缓存 |
volatile-lrm |
仅有 TTL 的 Key | 近似 LRM | 按修改活跃度保护带 TTL 数据 |
volatile-random |
仅有 TTL 的 Key | 随机 | 简单 TTL 候选池 |
volatile-ttl |
仅有 TTL 的 Key | 优先淘汰剩余 TTL 最短 | 业务已经用 TTL 表达价值优先级 |
注意:如果使用 volatile-*,但没有任何 Key 带 TTL,那么效果会退化得类似 noeviction,写入可能直接失败。
3.8 Redis 的 LRU/LFU/LRM 为什么是“近似”
Redis 不维护一个包含全部 Key、每次访问都精确移动节点的全局 LRU 双向链表,因为那会带来:
- 每次访问都修改共享元数据;
- 大量指针和链表内存;
- 更高 CPU 与缓存行开销;
- 更复杂的并发协调。
Redis 采用采样和候选池:
flowchart LR
A[达到 maxmemory] --> B[从候选 Key 中抽样]
B --> C[计算 idle / freq / modified time]
C --> D[更新 eviction pool]
D --> E[选出当前最差候选]
E --> F[删除并传播]
F --> G{内存是否降到目标内}
G -->|否| B
G -->|是| H[继续执行命令]
maxmemory-samples 越高,结果越接近精确算法,但 CPU 成本也越高。
LRU、LFU、LRM 的差异
- LRU 关心最近一次访问时间;
- LFU 关心访问频率,并带有频率衰减概念;
- LRM 关心最近一次修改时间,而不是读取时间。
如果一个 Key 被频繁读取但很久没有更新:
- LRU/LFU 可能认为它很有价值;
- LRM 可能认为它应该优先被淘汰。
因此策略必须和业务价值模型一致,而不是“听说 LRU 最常用就一律 LRU”。
3.9 同步删除与 Lazy Free
删除一个小 String 很快,但删除一个包含数百万成员的集合可能非常慢。
DEL通常在命令路径中同步释放对象;UNLINK先从键空间摘除,再由后台线程异步释放;lazyfree-lazy-eviction、lazyfree-lazy-expire等选项可以让某些淘汰或过期释放异步化。
异步释放降低主线程阻塞,但会形成“逻辑上已删除、物理内存稍后归还”的时间差。监控时要同时观察待释放对象与 RSS,而不能只看 Key 数量。
3.10 Redis 事务的真正语义
Redis 事务围绕:
MULTIEXECDISCARDWATCH
工作。
sequenceDiagram
participant C as Client
participant R as Redis
C->>R: MULTI
R-->>C: OK
C->>R: SET account:a 90
R-->>C: QUEUED
C->>R: SET account:b 110
R-->>C: QUEUED
C->>R: EXEC
R->>R: 顺序执行整个队列,中间不插入其他客户端命令
R-->>C: 返回每条命令结果数组
Redis 事务提供的关键保证是:
EXEC后,事务中的命令按顺序执行;- 执行期间不会插入其他普通客户端命令;
MULTI后的命令先排队,不立即执行;DISCARD丢弃队列;- AOF 对事务使用完整事务记录边界。
但它不是关系数据库事务的简单平替。
Redis 没有自动回滚
错误分为两类:
- 入队前错误:命令名、参数数量等错误,事务可在
EXEC时被整体拒绝; - 执行期错误:例如对 String 执行 List 操作,这一条返回错误,但其他已经排队的命令继续执行。
Redis 不为执行期错误回滚已经成功的命令。
WATCH 是乐观锁
WATCH key 后,如果在 EXEC 前该 Key 被其他客户端修改,事务会放弃执行,并返回空结果。客户端需要重新读取、重新计算和重试。
WATCH 适合低冲突的 Compare-And-Set;高冲突场景可能发生大量重试。
3.11 Pipeline、Transaction、Lua/Function 的区别
| 能力 | 主要目的 | 是否减少 RTT | 是否保证组合原子性 | 失败处理 |
|---|---|---|---|---|
| Pipeline | 批量发送,提升吞吐 | 是 | 否 | 每条命令独立返回 |
| MULTI/EXEC | 串行执行一组命令 | 可结合 Pipeline | 是,执行期间不被其他命令插入 | 无运行时自动回滚 |
| Lua / Function | 服务端执行复合逻辑 | 是 | 通常作为一个执行单元 | 脚本错误需业务处理 |
Lua 或 Function 可以避免“读取—计算—写入”之间被其他客户端插入,但脚本执行时间过长会占用主执行线程。正确做法是:逻辑短小、数据边界明确、时间复杂度可控。
Cluster 模式下,事务和 Lua 涉及的 Key 通常必须位于同一 Slot。
四、Redis 内存计算:从 Key 大小到宿主机容量
本章主要依据:MEMORY USAGE、MEMORY STATS、INFO 与 Memory Optimization。容量公式为工程预算模型,必须通过压测校准。
4.1 “Redis 占用内存”至少有四种口径
生产中最常见的误判是只盯着一个 used_memory。
flowchart TB
Data[逻辑数据集<br/>Key + Value + TTL] --> Alloc[allocator_allocated<br/>分配器已分配]
Alloc --> Active[allocator_active<br/>活跃页,含外部碎片]
Active --> Resident[allocator_resident<br/>分配器驻留页]
Resident --> RSS[used_memory_rss<br/>进程实际驻留内存]
Buffer[客户端 / AOF / 复制 / Cluster 缓冲] --> RSS
COW[fork Copy-on-Write] --> RSS
Code[代码、栈、共享库等] --> RSS
关键指标含义:
| 指标 | 含义 |
|---|---|
used_memory |
Redis 通过分配器分配的内存总量 |
used_memory_dataset |
数据集本身占用,尽量排除管理开销 |
used_memory_overhead |
键空间、客户端、复制等管理开销 |
allocator_allocated |
allocator 已经分配给 Redis 的字节 |
allocator_active |
allocator 活跃页,可能包含碎片 |
allocator_resident |
allocator 当前驻留物理内存 |
used_memory_rss |
OS 看到的进程 RSS |
mem_not_counted_for_evict |
不纳入 Key 淘汰计算的部分缓冲内存 |
mem_clients_normal |
普通客户端缓冲区 |
mem_replication_backlog |
复制积压缓冲区 |
mem_aof_buffer |
AOF 相关临时缓冲 |
mem_cluster_links |
Cluster 节点连接内存 |
4.2 一个 Key 的内存不等于字符串长度
假设:
1 | |
业务字符串总长度很小,但实际内存还包括:
- Key 对象;
- Value 对象;
- SDS Header;
- 字典 Entry 或 Key-Value 对象;
- Hash Table Bucket;
- 指针与对齐;
- allocator size class 浪费;
- TTL 元数据;
- 可能的引用和访问元数据;
- Rehash 期间的临时双表开销。
所以不能用:
1 | |
直接估算 Redis 容量。
推荐使用:
1 | |
MEMORY USAGE 对嵌套类型默认抽样,SAMPLES 0 表示扫描全部成员进行估算,但对超大集合可能非常昂贵,不应在高峰期随意执行。
4.3 数据结构编码会让内存发生阶跃变化
Redis 会根据元素数量和元素长度,为小聚合对象选择紧凑编码。当超过阈值后,再转换为普通结构。
Redis 8.8.1 默认配置中常见阈值包括:
1 | |
这意味着同样的业务对象:
- 在阈值以内可能使用 Listpack/Intset,内存很紧凑;
- 超过阈值后转为 Hash Table、Skip List、Quicklist 等结构;
- 单个元素变长,也可能触发转换;
- 转换后内存可能不是线性小幅增长,而是出现阶跃。
因此容量压测必须覆盖:
- 正常对象大小;
- P95/P99 大对象;
- 刚好跨编码阈值的对象;
- Rehash;
- TTL;
- 写高峰;
- RDB/AOF Rewrite。
4.4 一个可执行的容量估算方法
第一步:采样真实数据
不要拿随便生成的 key1=value1 代替真实业务数据。至少按业务类型分层采样:
- 普通缓存;
- Session;
- Hash 聚合对象;
- ZSet 排行榜;
- Stream;
- 向量、JSON、TimeSeries;
- 大 Key 与异常长尾。
第二步:计算每类对象的实际平均内存
设第 i 类数据:
- 数量为
N_i; - 平均
MEMORY USAGE为M_i; - 峰值增长系数为
G_i。
则数据集初步估算:
1 | |
这是数据集估算,不是最终 maxmemory,更不是宿主机 RSS。
第三步:加上固定与动态开销
1 | |
第四步:反推 maxmemory
工程上可采用:
1 | |
这不是官方固定比例,而是一种预算思维。
4.5 容量示例
假设在预生产导入真实数据后,某类缓存对象的平均 MEMORY USAGE 为 420 Byte,峰值预计 8000 万个:
1 | |
这只是对象样本的内存,还没有考虑:
- 数据库字典增长和 Rehash;
- TTL 字典;
- 复制与 AOF 缓冲;
- 客户端输出缓冲;
- allocator 碎片;
- fork COW;
- 流量增长。
假设宿主机为 64 GiB,团队基于压测得到如下预算:
| 预算项 | 示例值 |
|---|---|
| OS、监控与进程基础预留 | 6 GiB |
| 高写入期间预期 COW 峰值 | 10 GiB |
| 复制、AOF、客户端等缓冲 | 4 GiB |
| 碎片与安全余量 | 4 GiB |
可配置给 maxmemory |
约 40 GiB |
这里的 40 GiB 只是示例。写入速率越高、对象越大、持久化越频繁、Replica 越慢、客户端返回越大,需要预留的空间通常越多。
4.6 Copy-on-Write 为什么会形成峰值
执行 BGSAVE 或 BGREWRITEAOF 时,Redis 使用 fork() 创建子进程。父子进程起初共享物理页;父进程继续写入时,被修改的内存页会复制。
flowchart LR
Before[现有内存页] --> Fork[fork]
Fork --> Parent[父进程继续服务]
Fork --> Child[子进程生成 RDB / AOF Base]
Parent --> Write{父进程修改某页}
Write -->|未修改| Shared[父子共享物理页]
Write -->|发生修改| Copy[复制该内存页]
Copy --> Peak[RSS 上升]
COW 额外内存取决于:
- 子进程持续时间;
- 写入速率;
- 修改页面分布;
- 页面大小与 Transparent Huge Pages;
- 数据结构更新方式;
- 内存是否高度碎片化。
不能简单认为 fork 一定复制整个数据集,也不能假设 fork 几乎不占额外内存。正确答案只能来自写入压测和 INFO persistence、INFO memory 中的实际观测。
4.7 内存碎片怎么看
常见关系:
1 | |
解释时必须结合绝对值:
- 小实例因为基数很小,比例可能看起来很高;
- 比例接近 1 不代表一定健康,还要看 RSS 是否持续增长;
- 碎片高可能来自频繁创建删除、大小分布复杂、allocator 无法及时归还页面;
- RSS 高也可能来自 COW、客户端缓冲或 OS 行为,而不只是 allocator 碎片。
MEMORY PURGE 可以请求 allocator 尝试归还空闲页,但不保证所有内存立即回到 OS,也不应把它当作容量设计的补丁。
4.8 大 Key 与大量小 Key,谁更危险
大 Key 的问题
- 单次命令处理时间长;
- 网络响应大;
- 删除释放慢;
- 复制流突增;
- AOF 体积与重写压力大;
- Cluster 迁移慢;
- 形成热点 Slot。
大量小 Key 的问题
- 每个 Key 的固定对象和字典开销被放大;
- TTL 元数据增加;
- 过期扫描成本增加;
- Rehash 和 Bucket 占用显著;
- RDB/AOF 中 Key 元数据比例升高。
所以 Redis 的内存优化不是简单地“不要大 Key”,而是:
在业务可接受的原子边界、访问模式和维护成本之间,选择合适的聚合粒度。
五、AOF 与 RDB 原理
本章主要依据:Redis Persistence 与 Redis 8.8.1 示例配置文件。
5.1 RDB:某一时刻的数据快照
RDB 保存的是数据集在某个时刻的紧凑二进制表示。
sequenceDiagram
participant P as Parent Redis
participant C as Child Process
participant F as Disk
P->>P: 触发 BGSAVE
P->>C: fork
par 父进程继续处理请求
P->>P: 读写内存数据
and 子进程生成快照
C->>F: 写临时 RDB 文件
C->>F: fsync / 完成写入
C->>F: 原子替换旧 RDB
end
C-->>P: 返回完成状态
优点
- 文件紧凑;
- 适合离线备份和跨机房归档;
- 大数据集重启通常比纯 AOF 更快;
- 父进程不负责逐条写入完整快照;
- 完成后通过临时文件原子替换,已有 RDB 不会被边写边破坏。
缺点
- RPO 取决于快照间隔;
- 进程崩溃可能丢失最近一次快照之后的全部写入;
fork()可能产生主线程停顿;- 子进程期间持续写入会产生 COW 内存峰值;
- 大数据集会造成磁盘和页缓存压力。
SAVE 与 BGSAVE
SAVE在主进程同步生成快照,会直接阻塞服务,生产通常不应随意使用;BGSAVEfork 子进程生成快照,父进程继续服务,但 fork 本身和 COW 仍可能引起延迟与内存压力。
5.2 AOF:记录数据变更历史
AOF 记录会改变数据集的命令,启动时通过重放命令恢复状态。
flowchart LR
A[写命令执行] --> B[AOF Buffer]
B --> C[write 写入页缓存]
C --> D{appendfsync}
D -->|always| E[每批写入后 fsync]
D -->|everysec| F[后台线程约每秒 fsync]
D -->|no| G[由操作系统决定刷盘]
三种 fsync 策略
| 策略 | 持久化语义 | 性能和风险 |
|---|---|---|
always |
每批追加后 fsync | 最强但延迟和吞吐成本最高 |
everysec |
通常每秒 fsync | 默认折中,极端故障可能丢约 1 秒数据 |
no |
仅 write,刷盘交给 OS | 性能较好,但数据丢失窗口由 OS 决定,可能更大 |
客户端收到成功响应并不总是表示数据已经进入非易失介质。使用 everysec 时,响应可能先返回,而 fsync 稍后发生。
5.3 Redis 7+ / Redis 8 的 Multi-Part AOF
Redis 7.0 之后不再只依赖一个不断增长的单体 AOF,而是使用:
- 最多一个 Base AOF;
- 一个或多个 Incremental AOF;
- Manifest 清单文件。
flowchart TD
M[Manifest] --> B[Base AOF<br/>RDB 或 AOF 格式]
M --> I1[Incremental AOF 1]
M --> I2[Incremental AOF 2]
M --> I3[Incremental AOF ...]
默认 aof-use-rdb-preamble yes 时,Base 部分可以使用 RDB 格式,提高生成和加载效率,后续增量仍以 AOF 记录。
5.4 AOF Rewrite 不是“压缩文本文件”
AOF Rewrite 的目标是根据当前内存状态,生成恢复当前数据所需的最短或更紧凑命令集合。
例如:
1 | |
旧 AOF 可能有 100 条命令,但重写后只需要表达当前 counter=100 的状态。
Redis 7+ 重写过程:
sequenceDiagram
participant P as Parent
participant C as Rewrite Child
participant D as Disk
P->>C: fork
P->>D: 打开新的 Incremental AOF,继续接收写入
C->>D: 根据 fork 时数据生成新的 Base AOF
C-->>P: Base 完成
P->>D: 生成临时 Manifest
P->>D: 原子替换 Manifest
P->>D: 清理旧 Base 与无用 Incremental 文件
相较 Redis 7 之前的单 AOF 重写,Multi-Part AOF 避免了重写期间大量写命令在内存中集中积累后一次性追加的问题,但 fork、COW、磁盘 I/O 和增量文件仍需要容量规划。
5.5 RDB 与 AOF 如何选择
| 维度 | RDB | AOF |
|---|---|---|
| 数据模型 | 时间点快照 | 写命令日志 + Base |
| 典型 RPO | 分钟级,取决于快照周期 | everysec 下约秒级风险窗口 |
| 恢复速度 | 大数据集通常更快 | 通常需要加载 Base 并重放增量 |
| 文件体积 | 通常更小 | 通常更大 |
| 持续磁盘写入 | 较少 | 持续追加 |
| fork | BGSAVE 需要 | Rewrite 需要 |
| 适合备份 | 很适合 | 也可用,但管理更复杂 |
常见策略:
纯缓存
- 可关闭持久化;
- 必须接受重启后缓存清空;
- 上游数据库需要能承受缓存重建流量;
- 需要防止重启后缓存雪崩。
可接受分钟级丢失
- RDB;
- 配合远端备份;
- 明确快照周期与恢复流程。
需要更小 RPO
- AOF
everysec+ RDB; - 使用 RDB 做备份和快速恢复基础;
- 使用 AOF 保留更细粒度变更。
极强数据安全需求
Redis 不是因为开启 appendfsync always 就自动等价于关系数据库。还要考虑:
- 主机和磁盘故障;
- 文件系统语义;
- 复制和故障转移;
- 操作误删;
- 异地备份;
- 恢复演练;
- 业务幂等和审计。
5.6 no-appendfsync-on-rewrite 的真实取舍
AOF Rewrite 或 BGSAVE 期间磁盘压力增大,fsync 可能导致延迟抖动。配置:
1 | |
可以在后台保存期间避免主进程 fsync,从而降低部分延迟风险,但此时持久化安全性会退化,最坏丢失窗口可能明显增加。
官方默认是 no,即优先保持更安全的持久化语义。不要为了压低一张 P99 图,悄悄扩大 RPO 而不通知业务——那不是优化,是把问题藏进事故报告。
5.7 持久化的生产检查项
1 | |
重点关注:
rdb_bgsave_in_progress;rdb_last_bgsave_status;rdb_last_cow_size;latest_fork_usec;aof_enabled;aof_rewrite_in_progress;aof_last_bgrewrite_status;aof_delayed_fsync;- 磁盘剩余空间;
- RDB/AOF 文件是否能在隔离环境成功恢复。
“文件生成成功”不是备份成功;“备份存在”也不是恢复成功。只有定期恢复演练通过,才说明备份链路可用。
六、Redis 主从复制原理
本章主要依据:Redis Replication、Redis 8.0 Release Notes 与 replication.c。
6.1 主从复制的三条主线
Redis 基础复制是 Primary—Replica 模型。它本身提供数据副本,不自动等于完整高可用;自动故障转移由 Sentinel 或 Cluster 等上层机制完成。
官方文档把复制概括为三种状态:
- 连接正常时,Primary 持续发送命令流;
- 短暂断线后,尝试部分重同步;
- 无法部分重同步时,执行全量重同步。
flowchart TD
A[Replica 连接 Primary] --> B{是否拥有可续接的 replid + offset}
B -->|是| C{Backlog 是否仍包含缺失数据}
C -->|是| PSYNC[Partial Resync<br/>发送缺失命令流]
C -->|否| FULL[Full Resync]
B -->|否| FULL
FULL --> RDB[生成或流式传输 RDB]
RDB --> LOAD[Replica 加载完整数据]
LOAD --> STREAM[继续应用复制命令流]
PSYNC --> ONLINE[在线复制]
STREAM --> ONLINE
6.2 正常在线复制
客户端写入 Primary 后:
- Primary 在本地执行命令;
- 将改变数据集的操作写入复制流;
- Replica 接收并按顺序重放;
- Replica 周期性上报已处理的复制 Offset。
传播内容不仅包括客户端显式写命令,也包括过期、淘汰等导致的数据变化。
默认复制是异步的:
- Primary 不需要每次都等 Replica 执行完成再返回客户端;
- 延迟较低;
- 但故障窗口内可能丢失已经被 Primary 确认、尚未复制完成的写入。
6.3 replid + offset:复制历史坐标
可以把复制流理解为一条不断增长的字节流:
replid标识一段复制历史;offset表示实例处理到这段历史的哪个位置;- Replication Backlog 保存最近一段命令流。
Replica 重连时携带自己的 replid + offset。如果 Primary 能识别这段历史,并且缺失部分仍在 Backlog 中,就只发送差异部分。
Backlog 太小的后果是:
- 网络抖动稍长就无法部分同步;
- 被迫执行 Full Sync;
- fork、网络、Replica 加载和 COW 压力明显增加。
因此 repl-backlog-size 应根据:
1 | |
估算,而不是永远使用默认值。
6.4 Full Sync
传统 Full Sync 过程:
- Primary 创建完整 RDB;
- 向 Replica 传输 RDB;
- 同步期间的新写入进入复制缓冲;
- Replica 加载 RDB;
- 再追赶增量命令流。
压力点包括:
- Primary fork;
- COW;
- RDB 生成和网络传输;
- 同步期间写入缓冲;
- Replica 清空旧数据;
- Replica 加载 RDB 时的主线程阻塞;
- 多个 Replica 同时全量同步造成的放大。
6.5 Redis 8 的全量复制改进
Redis 8 对 Full Sync 做了重要优化:RDB 基础数据传输与同步期间产生的命令流可以通过并行通道推进,从而降低过去“等 RDB 完成后再集中追增量”带来的缓冲峰值和同步时间压力。
sequenceDiagram
participant P as Primary
participant R as Replica
P->>R: 建立 RDB 数据通道
P->>R: 开始传输快照
par 基础快照
P-->>R: RDB 数据流
and 持续写入
P-->>R: Replication Command Stream
end
R->>R: 加载快照并衔接增量流
R-->>P: ACK Offset
这并不意味着 Full Sync 没有成本。RDB、fork、网络、加载、COW 和缓冲仍然存在,只是峰值和等待关系得到改善。
6.6 WAIT 能把 Redis 变成强一致吗
不能。
WAIT numreplicas timeout 可以要求当前连接之前的写入被指定数量的 Replica 确认,从而显著降低故障时丢写概率。
但它不等于:
- 共识协议提交;
- 线性一致读写;
- 故障转移绝不丢数据;
- Replica 已经完成磁盘持久化;
- 事务跨节点原子提交。
官方文档明确说明,WAIT 不会把一组 Redis 实例变成强一致 CP 系统。故障转移后是否丢写还与持久化、拓扑和具体故障顺序有关。
6.7 min-replicas-to-write 的作用
可配置:
1 | |
含义是:如果 Primary 发现满足延迟要求的 Replica 数量不足,就拒绝写入。
它可以减少 Primary 在长期失去副本保护时继续接受大量写入,但仍不是共识协议。它基于 Replica ACK 和延迟观测,存在时间窗口,也不能解决所有网络分区和故障转移场景。
6.8 Replica 读扩展的代价
Replica 可以承担读请求,但要接受:
- 复制延迟;
- 读到旧值;
- 主从切换时拓扑变化;
- Full Sync 期间的可用性与加载阻塞;
- 读后写、写后读一致性问题;
- 过期和删除传播的时间差。
适合下沉到 Replica 的场景通常是:
- 允许最终一致的报表;
- 可容忍短暂旧值的查询;
- 重型只读分析;
- 运维扫描。
支付、库存、锁状态等强依赖最新写入结果的请求,不应在没有一致性设计的情况下随意读 Replica。
6.9 一个危险配置:Primary 无持久化且自动重启
假设:
- Primary 不做持久化;
- Replica 有完整数据;
- Primary 崩溃后被进程管理器自动重启;
- Primary 以空数据集启动;
- Replica 重新向这个空 Primary 同步。
结果可能是 Replica 也被清空。
因此,若 Primary 关闭持久化,必须非常谨慎地设计重启、故障转移和角色恢复流程。复制不是备份,Replica 也不是防误操作保险箱。
6.10 复制、Sentinel 与 Cluster 的边界
| 能力 | Replication | Sentinel | Cluster |
|---|---|---|---|
| 数据副本 | 是 | 依赖 Replication | 是 |
| 自动故障检测 | 否 | 是 | 是 |
| 自动主从切换 | 否 | 是 | 是 |
| 数据分片 | 否 | 否 | 是 |
| 客户端路由变化 | 需自行处理 | 通过 Sentinel 获取主节点 | Cluster-aware Client |
七、Redis Cluster 原理
本章主要依据:Redis Cluster Specification 与 Scale with Redis Cluster。
7.1 为什么是 16384 个 Slot
Redis Cluster 不直接把每个 Key 固定映射到某个节点,而是:
1 | |
再把 16384 个 Slot 分配给不同 Primary。
flowchart LR
K[Key] --> H[CRC16]
H --> S[Hash Slot 0..16383]
S --> M{Slot Map}
M --> N1[Primary A]
M --> N2[Primary B]
M --> N3[Primary C]
引入 Slot 的好处:
- 迁移时移动 Slot,而不是重新计算所有 Key 的节点;
- 客户端可缓存
Slot -> Node映射; - 节点负责一段 Slot 范围;
- 故障转移时由 Replica 接管同一批 Slot。
7.2 Cluster 只有 DB 0
Redis Cluster 不支持 Standalone 中的多逻辑数据库切换,SELECT 不可用,只使用 DB 0。
生产上本来也不建议依赖 SELECT 0/1/2 作为租户或环境隔离。更清晰的方式是:
- 不同实例或 Cluster;
- 明确 Key Namespace;
- ACL;
- 资源与容量隔离。
7.3 Hash Tag:控制多个 Key 落到同一 Slot
Key 中第一个有效 {...} 子串用于计算 Slot:
1 | |
这三个 Key 使用 1001 计算 Slot,因此可以执行同 Slot 多 Key 命令或事务。
Hash Tag 的风险是:
- 标签粒度过粗会产生热点 Slot;
- 大量业务都使用同一个标签,相当于主动放弃分片;
- Slot 迁移时单个 Slot 过大;
- 扩容后数据仍无法均衡。
Hash Tag 应围绕“必须原子操作的数据边界”设计,而不是为了绕过 CROSSSLOT 把所有 Key 都塞进 {global}。
7.4 客户端如何路由
Cluster 节点不负责通用代理。客户端发错节点时,收到重定向:
MOVED
表示 Slot 的正式归属已经在另一个节点。客户端应更新本地 Slot Map,以后直接请求新节点。
ASK
通常发生在 Slot 迁移过程中,表示这一次请求临时去目标节点,并先发送 ASKING;客户端不应仅凭 ASK 永久改写整个 Slot Map。
sequenceDiagram
participant C as Cluster Client
participant A as Node A
participant B as Node B
C->>A: GET key
A-->>C: MOVED slot B
C->>C: 更新 Slot Map
C->>B: GET key
B-->>C: value
Cluster Client 的质量非常关键。它需要处理:
- Slot Map 初始化;
- MOVED;
- ASK;
- 连接池;
- 节点上下线;
- Pipeline 按节点拆分;
- 重试幂等;
- 超时与拓扑刷新;
- Read from Replica 时的只读路由。
7.5 Cluster Bus 与 Gossip
每个 Cluster 节点除了客户端端口,还通过 Cluster Bus 与其他节点通信。Cluster Bus 使用二进制协议,负责:
- 节点发现;
- PING/PONG;
- Gossip 状态传播;
- 故障信息;
- 配置纪元;
- Failover 协调;
- Slot 归属变更;
- Cluster 内部消息。
Cluster 不是由一个中心配置服务器实时指挥,而是节点通过 Gossip 汇聚状态。
7.6 从 PFAIL 到 FAIL
概念上:
- 某节点长时间无法联系另一个节点;
- 本地把对方标记为
PFAIL,即主观疑似下线; - 故障信息通过 Gossip 传播;
- 足够多负责 Slot 的 Primary 对故障形成确认;
- 节点被标记为
FAIL,即客观下线; - 如果故障节点有合格 Replica,则进入 Failover 选举。
flowchart TD
A[节点通信超时] --> B[本地主观 PFAIL]
B --> C[Gossip 传播故障报告]
C --> D{是否达到多数 Primary 确认}
D -->|否| B
D -->|是| E[标记 FAIL]
E --> F{是否有合格 Replica}
F -->|否| G[相关 Slot 不可用]
F -->|是| H[Replica 发起选举]
H --> I[获得多数投票]
I --> J[晋升为 Primary并接管 Slot]
7.7 Replica 选举与配置纪元
多个 Replica 不会无条件同时晋升。选举会考虑:
- 与旧 Primary 的复制 Offset;
- Replica 数据新鲜度;
- Replica 排名和延迟;
- 选举投票;
- Configuration Epoch。
Configuration Epoch 用于确定较新的集群配置。最终集群需要对“谁拥有这些 Slot”形成一致认识。
7.8 Cluster 的一致性边界
Redis Cluster 使用异步复制,因此存在已确认写入丢失窗口:
- 客户端写入 Primary;
- Primary 返回成功;
- 写入尚未到 Replica;
- Primary 故障;
- Replica 晋升;
- 该写入丢失。
网络分区时,少数派侧旧 Primary 在检测到自己无法联系多数 Primary 后,会停止接受写入,但检测不是零时间,因此仍存在窗口。
Redis Cluster 的设计优先级是:
- 高性能;
- 线性扩展;
- 可接受程度的写安全;
- 多数派可用。
它不是 Raft/Paxos 风格的强一致分布式数据库。
7.9 多 Key 命令为什么会 CROSSSLOT
假设:
1 | |
如果两个 Key 不同 Slot,Redis 无法在一个节点的单线程执行边界内完成原子操作,也没有跨分片二阶段提交,因此返回 CROSSSLOT。
解决方式:
- 使用 Hash Tag,把有原子关系的 Key 放到同一 Slot;
- 调整数据模型,让一个聚合对象使用 Hash/JSON;
- 客户端拆分请求,并接受非原子语义;
- 使用业务层 Saga、幂等和补偿;
- 选择更适合跨分片事务的数据系统。
7.10 扩容和 Reshard
Cluster 扩容通常是:
- 新增节点;
- 将部分 Slot 从旧节点迁移到新节点;
- 逐步迁移 Slot 内的 Key;
- 迁移期间客户端处理
ASK; - Slot 正式归属变更后处理
MOVED。
迁移成本取决于:
- Slot 内 Key 数量;
- 大 Key;
- 网络带宽;
- 写入速率;
- 迁移期间增量变更;
- 客户端重定向处理;
- Cluster 迁移缓冲。
所以容量规划不仅要看总数据量,还要看 Slot 分布和最大 Slot。
7.11 推荐拓扑不是协议硬要求
生产常见起步拓扑是:
- 3 个 Primary;
- 每个 Primary 至少 1 个 Replica;
- 共 6 个节点;
- Primary 与其 Replica 分布在不同宿主机或可用区。
这不是 Redis Cluster 协议唯一允许的拓扑,而是为了让多数 Primary、故障切换和副本隔离更合理。具体节点数应由容量、故障域、成本和可用性目标决定。
八、Redis 数据结构与内部编码
本章主要依据:Redis Data Types、Redis 8.8.1 源码与示例配置中的编码阈值。
8.1 Redis 是数据结构服务器,不只是字符串 KV
Redis 的逻辑模型是:
flowchart LR
DB[redisDb / Keyspace] --> K1[Key A]
DB --> K2[Key B]
DB --> K3[Key C]
K1 --> O1[Redis Object / KV Object]
K2 --> O2[Redis Object / KV Object]
K3 --> O3[Redis Object / KV Object]
O1 --> S[String / SDS]
O2 --> H[Hash / Listpack or Hashtable]
O3 --> Z[ZSet / Listpack or Dict + Skiplist]
逻辑类型和底层编码不是一一对应。Redis 会根据数据规模选择不同编码,在内存和 CPU 之间折中。
8.2 String
String 是二进制安全字节序列,最大值大小受 Redis 限制。常见用途:
- 缓存序列化对象;
- 计数器;
- 分布式锁值;
- Bitmap;
- Bitfield;
- 简单状态;
- Token。
内部通常基于 SDS,并可能使用整数、短字符串紧凑编码或普通动态字符串编码。
常见复杂度:
| 操作 | 典型复杂度 |
|---|---|
GET / SET |
O(1) |
INCR |
O(1) |
APPEND |
与追加长度相关,均摊通常较好 |
GETRANGE |
O(N),N 为返回长度 |
O(1) 不等于固定网络成本。一个 20 MB String 的 GET 即使查找是 O(1),序列化和传输仍然很重。
8.3 Hash
Hash 适合保存对象字段:
1 | |
小 Hash 通常使用 Listpack;超过元素数量或 Value 长度阈值后转为 Hash Table。
优点:
- 字段级读写;
- 小对象紧凑;
- 可用
HINCRBY原子更新计数; - Redis 8 支持字段级过期能力。
风险:
- 超大 Hash 形成 Big Key;
HGETALL返回全量;- 字段级 TTL 增加元数据和清理复杂度;
- 单 Hash 过度聚合会破坏 Cluster 并行度。
8.4 List
Redis List 按插入顺序保存字符串,常见底层结构是 Listpack 与 Quicklist 的组合或切换:
- 小列表可保持紧凑;
- 大列表使用由多个紧凑节点组成的 Quicklist;
- 可以配置节点大小和中间节点压缩深度。
适合:
- 简单 FIFO/LIFO;
- 固定长度最近记录;
- 阻塞队列原语。
不适合:
- 需要消费组、重试、确认和消息追踪的复杂消息系统;
- 大范围中间索引随机访问;
- 无界增长的日志。
复杂消息场景通常优先考虑 Streams。
8.5 Set
Set 保存唯一无序元素。
内部编码可能是:
- 全整数小集合:Intset;
- 小型普通集合:Listpack;
- 更大集合:Hash Table。
适合:
- 去重;
- 标签;
- 共同关注;
- 黑白名单;
- 随机抽样;
- 集合关系。
SUNION、SINTER、SDIFF 的成本取决于集合大小。大集合交集应评估主线程占用,必要时离线计算或在 Replica 执行只读分析。
8.6 Sorted Set
ZSet 为每个 Member 关联一个 Score,并按 Score 排序。
小 ZSet 可使用 Listpack;较大 ZSet 通常使用:
- Dictionary:按 Member 快速找到 Score;
- Skip List:按 Score 排序、范围扫描和排名。
适合:
- 排行榜;
- 延迟队列;
- 时间窗口;
- 优先级队列;
- 带权重排序。
1 | |
延迟队列用 ZSet 时,应注意:
- 多消费者抢占;
- 原子取出;
- 失败重试;
- 消息丢失;
- 时钟;
- 大量到期任务同时涌入。
单纯 ZRANGEBYSCORE + ZREM 分两步执行可能发生并发竞争,应使用原子命令、事务或 Function。
8.7 Streams
Streams 面向追加事件流,支持:
- 自动或指定 ID;
- 范围读取;
- 阻塞读取;
- Consumer Group;
- Pending Entries List;
- ACK;
- Claim;
- 截断。
内部使用 Radix Tree 等结构组织紧凑的消息节点。
适合:
- 轻量事件流;
- 工作队列;
- 消费组;
- 需要重试与 ACK 的任务。
需要关注:
- PEL 无界增长;
- 消费者假死;
- 未 ACK 消息回收;
- Stream 截断策略;
- 消息体过大;
- Redis 持久化与故障转移语义。
Redis Streams 不是天然替代 Kafka。是否适合取决于消息保留、吞吐、跨机房、分区、回放和数据安全需求。
8.8 Bitmap 与 Bitfield
Bitmap 本质上是对 String 的位操作,适合:
- 签到;
- 用户活跃状态;
- 布尔特征集合;
- 大规模 ID 状态。
1 亿个布尔状态理论位数据约为:
1 | |
实际还会有 Key 和对象开销,但仍远小于 1 亿个独立 Key。
Bitfield 可以在 String 上保存多个定长整数,并原子执行读取、写入和增量操作。
8.9 HyperLogLog 与概率结构
HyperLogLog 用固定且较小的空间估算基数,适合:
- UV;
- 去重计数;
- 可接受误差的巨大集合基数。
Redis 8 集成的概率结构还包括:
- Bloom Filter;
- Cuckoo Filter;
- Count-Min Sketch;
- Top-K;
- t-digest。
概率结构的价值是以可控误差换取内存与计算效率。它们不应被用在“绝不能误判”的财务唯一性、权限或强一致去重场景。
8.10 Geospatial
Redis Geo 基于经纬度编码和 Sorted Set 能力,适合:
- 附近门店;
- 附近骑手;
- 半径或矩形范围检索。
它适合实时近邻过滤,不是完整 GIS 系统。复杂多边形、道路网络、坐标投影和空间拓扑通常需要专业空间数据库或 Search 能力。
8.11 JSON、TimeSeries、Vector Sets 与 Search
Redis 8 将过去常见于 Redis Stack 的多种能力统一到 Redis Open Source 发行体系,包括:
- JSON 文档;
- TimeSeries;
- Search 与聚合;
- Vector Search / Vector Sets;
- 概率数据结构;
- Redis 8.8 的 Array 等新类型。
这意味着 Redis 8 已经不仅是传统缓存服务器,还可以承担:
- 文档字段更新;
- 二级索引;
- 全文检索;
- 向量相似度检索;
- 时序聚合;
- 实时查询。
但索引也需要内存,查询也需要 CPU。使用 Search/Vector 时,容量模型应包含:
- 原始文档;
- 索引;
- 向量;
- 索引构建峰值;
- 查询 Worker;
- 返回结果;
- Cluster 分片聚合。
不要拿只存 String 的内存比值,去估算带向量索引的 Redis 8 实例。
8.12 数据类型选型表
| 需求 | 优先考虑 | 不要忽略 |
|---|---|---|
| 简单缓存、计数器 | String | Value 大小、序列化、TTL |
| 对象字段更新 | Hash / JSON | Big Key、字段级 TTL、索引成本 |
| 去重、关系集合 | Set | 大集合运算成本 |
| 排行榜、优先级 | ZSet | Score 精度、热点 Key |
| 简单双端队列 | List | 无 ACK、复杂重试困难 |
| 消费组消息 | Stream | PEL、截断、持久化 |
| 布尔状态 | Bitmap | ID 稀疏导致空间浪费 |
| 近似基数 | HyperLogLog | 误差不可消除 |
| 会员存在性预判 | Bloom/Cuckoo | 假阳性或删除语义 |
| 地理近邻 | Geo | 不是完整 GIS |
| 文档与字段查询 | JSON + Search | 索引内存与更新成本 |
| 向量检索 | Vector / Search | 向量内存、召回率、索引参数 |
九、Redis 最佳实践与常见场景
本章主要依据 Redis 官方的 Pipelining、SCAN、Latency、Distributed Locks 等文档,并结合前述机制推导工程实践。凡涉及容量比例和批次大小的建议,都应以真实压测结果为准。
9.1 缓存:Cache-Aside 的正确打开方式
sequenceDiagram
participant App as Application
participant R as Redis
participant DB as Database
App->>R: GET key
alt Cache Hit
R-->>App: value
else Cache Miss
R-->>App: nil
App->>DB: SELECT
DB-->>App: value
App->>R: SET key value EX ttl
end
更新常见模式:
- 更新数据库;
- 删除缓存;
- 下次读取回填。
为什么通常选择删除而不是直接更新缓存:
- 缓存值可能由多个表聚合;
- 更新缓存与更新数据库难以形成跨系统事务;
- 并发下直接更新可能写入旧计算结果;
- 删除让读取路径重新构建权威状态。
但删除缓存也不是绝对一致,需要结合:
- 延迟双删;
- 消息通知;
- CDC;
- 版本号;
- 逻辑过期;
- 最终一致性容忍度。
9.2 缓存穿透、击穿、雪崩
穿透
查询不存在的数据,每次都落到数据库。
方案:
- 缓存空值并设置短 TTL;
- Bloom Filter;
- 参数校验;
- 风控和限流。
击穿
单个热点 Key 失效,大量请求同时回源。
方案:
- 单飞请求合并;
- 分布式互斥;
- 逻辑过期;
- 提前刷新;
- 热点永不过期 + 主动更新,但必须设计失效机制。
雪崩
大量 Key 同时失效或 Redis 整体不可用。
方案:
- TTL 加随机抖动;
- 分批预热;
- 限流、熔断、降级;
- 多级缓存;
- 隔离热点;
- 数据库保护;
- Redis 高可用与容量余量。
9.3 Session 与 Token
Redis 适合集中式 Session:
- TTL 与登录有效期一致;
- 按用户或设备维度建模;
- 登录刷新时谨慎续期;
- 删除或版本号控制强制下线;
- 避免在 Value 中保存过多权限快照;
- 需要考虑 Redis 故障时的认证降级策略。
JWT 并不等于完全不需要 Redis。黑名单、Refresh Token、设备会话和强制注销仍可能需要状态存储。
9.4 计数器与限流
简单计数器:
1 | |
INCR 与 EXPIRE 分两条命令存在中间失败窗口,可使用:
- Redis 8.8
INCREX等新能力; MULTI/EXEC;- Lua/Function;
- 客户端库提供的原子限流实现。
限流算法包括:
- 固定窗口;
- 滑动窗口计数;
- 滑动日志;
- Token Bucket;
- Leaky Bucket。
选择时看:
- 精度;
- 内存;
- 突发容忍;
- Cluster Slot;
- 脚本复杂度;
- 时钟;
- 限流失败时是 Fail-Open 还是 Fail-Close。
9.5 分布式锁
Redis 8.4+ 单实例锁的基本模式可使用唯一 Token 配合原子条件删除:
1 | |
释放时必须只删除自己持有的锁。Redis 8.4 引入 DELEX ... IFEQ ...;旧版本通常使用 Lua 比较 Value 后删除。
绝不能简单:
1 | |
因为锁可能已经过期并被另一个客户端重新获取。
更重要的是,分布式锁不是拿到 Key 就万事大吉:
- 业务执行时间可能超过 TTL;
- GC Pause 或线程停顿可能让持锁方失去租约却继续写资源;
- 主从异步复制与 Failover 可能破坏简单锁的互斥;
- 时钟变化会影响租约;
- 高一致场景应使用 Fencing Token;
- 需要明确续期、重试、超时和幂等。
如果业务真正要求“任何故障下都不能有两个执行者修改外部资源”,必须把外部资源的 Fencing Token 或版本校验纳入设计,而不是只依赖 Redis Key。
9.6 消息场景:Pub/Sub、List、Stream 怎么选
| 能力 | Pub/Sub | List | Stream |
|---|---|---|---|
| 离线消息保留 | 否 | 可保留 | 可保留 |
| 多消费者竞争 | 广播 | 可用阻塞弹出 | Consumer Group |
| ACK | 否 | 无原生完整 ACK | 是 |
| Pending/Claim | 否 | 需自建 | 是 |
| 回放 | 否 | 有限 | 支持 ID 范围 |
| 适合 | 在线通知 | 简单队列 | 可靠性更高的事件消费 |
Redis 消息能力适合低延迟和中等复杂度场景。若需要:
- 长期海量日志;
- 跨地域复制;
- 强持久化分区日志;
- 极长时间回放;
- 大规模消费者生态;
应认真评估 Kafka、Pulsar 等日志系统。
9.7 Pipeline 的正确使用
Pipeline 将多条命令一次发送,减少 RTT,并让 Redis 更高效地批量处理网络数据。
但 Pipeline 不是越大越好:
- 服务端必须缓存未返回的响应;
- 客户端也要保存请求与结果;
- 单批太大可能形成延迟尖峰;
- Cluster 需要按目标节点拆分;
- 重试时要处理部分成功;
- 非幂等命令不能盲目整体重放。
应通过压测确定批次大小,例如从几十或几百条开始,而不是一次塞几十万条。
9.8 禁止无边界命令
生产中应重点审计:
KEYS;HGETALL;SMEMBERS;- 大范围
ZRANGE; - 无限制
LRANGE; - 大集合交并差;
- 超长 Lua;
- 大 Key
DEL; - 返回全库或全集合的管理接口。
使用:
SCAN/HSCAN/SSCAN/ZSCAN;- 分页与范围限制;
UNLINK;- 异步任务;
- Replica 离线分析;
- 数据模型拆分。
SCAN 是增量遍历,不提供静态快照。遍历期间集合变化时可能出现重复或变化,需要业务去重和容忍。
9.9 Key 设计
推荐格式:
1 | |
例如:
1 | |
原则:
- 可读;
- 长度适中;
- 避免把敏感信息直接写入 Key;
- Hash Tag 只用于真实的同 Slot 原子边界;
- 不依赖模糊的数字数据库编号隔离环境;
- 设计可扫描的 Namespace,但不要依赖
KEYS; - 明确 TTL 所有权。
9.10 热 Key 与热 Slot
热 Key 的影响:
- 单节点 CPU 或网络打满;
- 单 Key 操作形成主线程热点;
- Replica 也可能被大流量压垮;
- Cluster 增加节点也无法自动拆分一个 Key。
处理方式:
- 本地缓存;
- 请求合并;
- 读副本;
- Key 分片;
- 预计算;
- 热点隔离实例;
- 降低单次返回大小。
Key 分片会牺牲原子性和查询便利,需要业务层聚合。
9.11 连接与超时
客户端应配置:
- 连接超时;
- 命令超时;
- 连接池上限;
- 空闲检测;
- 重试次数和退避;
- Cluster 拓扑刷新;
- 请求积压上限;
- 熔断和降级。
重试必须识别命令是否幂等。例如:
GET通常可安全重试;SET key value覆盖写通常较容易推理;INCR在响应丢失后盲目重试可能重复执行;XADD *重试可能产生两条消息。
9.12 安全
生产最低要求:
- 不暴露到公网;
- 使用网络隔离和防火墙;
- 使用 ACL 最小权限;
- 管理命令严格限制;
- 需要时启用 TLS;
- 密钥进入 Secret 管理系统;
- 避免在命令、Key、日志中泄露敏感数据;
- 定期升级安全补丁;
- 审计危险命令和异常连接。
9.13 操作系统与延迟
低延迟环境常见检查:
- 关闭 Transparent Huge Pages;
- 评估
vm.overcommit_memory; - 保证时间同步;
- 监控 Swap,通常应避免 Redis 工作集被换出;
- 使用低延迟磁盘;
- 避免与高 I/O、高内存抖动任务混部;
- 通过
redis-cli --intrinsic-latency测量宿主机基础延迟; - 观察
latest_fork_usec。
Redis 延迟问题可能来自命令、网络、fork、COW、fsync、Swap、虚拟化抖动或客户端 GC,不能一看到 P99 就只调线程数。
9.14 生产监控清单
流量与延迟
- QPS;
- 网络入站/出站;
- P50/P95/P99;
instantaneous_ops_per_sec;- Slow Log;
- Latency Monitor;
- 命令调用次数与总耗时。
内存
used_memory;used_memory_rss;used_memory_dataset;mem_fragmentation_ratio;mem_not_counted_for_evict;- 客户端输出缓冲;
evicted_keys;expired_keys;- Lazy Free Pending Objects。
持久化
- 最近 RDB/AOF 状态;
- Fork 时间;
- COW Size;
- AOF Delayed fsync;
- 重写时长;
- 磁盘空间;
- 备份恢复演练。
复制
master_repl_offset;- Replica Offset;
- Lag;
- Backlog 大小;
- Full Sync 次数;
- 复制断线;
- Replica 输出缓冲。
Cluster
- Slot 是否全部覆盖;
- PFAIL/FAIL;
- 节点 Link 状态;
- Slot 分布;
- 热 Slot;
- 迁移缓冲;
- Failover 次数与耗时;
MOVED/ASK异常增加。
9.15 上线前检查表
- 已确定 Redis 的角色:缓存、主存储、消息、锁、索引还是混合用途;
- 已明确允许的数据丢失窗口 RPO;
- 已明确恢复时间目标 RTO;
- 已用真实数据测量
MEMORY USAGE; - 已考虑 COW、复制、AOF、客户端缓冲和碎片;
-
maxmemory未设置为物理内存上限; - 淘汰策略与业务价值一致;
- 缓存 Key 有合理 TTL 和抖动;
- 已审计 Big Key、Hot Key、Hot Slot;
- 已禁止无边界命令;
- Pipeline 有批次上限;
- 重试区分幂等和非幂等;
- Cluster 多 Key 原子边界已经设计;
- 持久化文件已实际恢复验证;
- Replica 与备份不在同一故障域;
- 已配置 ACL、网络隔离和必要的 TLS;
- 已配置 Slow Log、Latency Monitor 和完整告警;
- 已演练节点故障、磁盘满、Replica 落后、Cluster Failover 和缓存全失效。
十、结论
Redis 8 的关键不在于“它是单线程还是多线程”这一道面试判断题,而在于理解不同工作被放在了不同执行边界:
- 网络 I/O 可以并行;
- 绝大多数核心命令仍由主执行线程串行修改键空间;
- 过期采用访问时检查与自适应主动抽样;
- 淘汰是在内存上限压力下按近似策略选择候选 Key;
- 事务提供顺序执行和不被插入,但没有关系数据库式自动回滚;
- Redis 实际 RSS 远大于简单的 Key+Value 字节数;
- RDB 是时间点快照,AOF 是变更日志与 Base/Increment 文件组合;
- 复制默认异步,
WAIT只能降低丢写概率,不能带来强一致; - Cluster 通过 16384 个 Slot 水平分片,由客户端处理重定向;
- 数据结构编码、Big Key、Hot Key、fork COW 和缓冲内存决定了生产上限。
最终,Redis 的正确使用方式不是把所有高并发问题都“塞进 Redis”,而是先明确:
- 数据是否允许丢;
- 是否允许旧读;
- 原子边界在哪里;
- 单 Key 是否会过热;
- 故障后如何恢复;
- Redis 不可用时业务如何降级。
理解这些边界之后,Redis 才不是一个“快但玄学”的黑盒,而是一套可以计算、验证和演练的实时数据系统。
参考资料
本文优先引用 Redis 官方文档和 Redis 8.8.1 源码。建议阅读时以当前版本文档和实际部署版本源码为准。
Redis Open Source 8.8 Release Notes
https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.8-release-notes/Redis Downloads
https://redis.io/downloads/Redis Open Source 8.0 Release Notes(I/O 线程与复制改进)
https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.0-release-notes/Redis 8.8.1
src/networking.c
https://github.com/redis/redis/blob/8.8.1/src/networking.cRedis 8.8.1
src/server.c
https://github.com/redis/redis/blob/8.8.1/src/server.cRedis 8.8.1
src/expire.c
https://github.com/redis/redis/blob/8.8.1/src/expire.cRedis 8.8.1
src/evict.c
https://github.com/redis/redis/blob/8.8.1/src/evict.cRedis 8.8.1
src/replication.c
https://github.com/redis/redis/blob/8.8.1/src/replication.cRedis 8.8.1 Example Configuration
https://github.com/redis/redis/blob/8.8.1/redis.confKey Expiration
https://redis.io/docs/latest/commands/expire/Key Eviction
https://redis.io/docs/latest/develop/reference/eviction/Redis Transactions
https://redis.io/docs/latest/develop/using-commands/transactions/MEMORY USAGE
https://redis.io/docs/latest/commands/memory-usage/MEMORY STATS
https://redis.io/docs/latest/commands/memory-stats/INFO Command
https://redis.io/docs/latest/commands/info/Memory Optimization
https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/Redis Persistence
https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/Redis Replication
https://redis.io/docs/latest/operate/oss_and_stack/management/replication/Redis Cluster Specification
https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/Scale with Redis Cluster
https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/Redis Data Types
https://redis.io/docs/latest/develop/data-types/Redis Pipelining
https://redis.io/docs/latest/develop/using-commands/pipelining/Diagnosing Latency Issues
https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/latency/Distributed Locks with Redis
https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/