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 重写等工作。

这套设计解决了几个相互冲突的问题:

  1. 核心键空间操作尽量避免锁竞争;
  2. 单条普通命令天然具备清晰的串行执行边界;
  3. 网络 I/O 不再完全受限于单个线程;
  4. 磁盘持久化、内存释放等慢操作尽可能移出主执行路径;
  5. 通过复制和 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 Notesnetworking.cserver.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
SET user:1001 Mario EX 60

编码成 RESP 后类似:

1
2
3
4
5
6
7
8
9
10
11
*5\r\n
$3\r\n
SET\r\n
$9\r\n
user:1001\r\n
$5\r\n
Mario\r\n
$2\r\n
EX\r\n
$2\r\n
60\r\n

Redis 收到字节后,不会立刻调用 setCommand(),而是先经历:

  1. Socket 可读事件触发;
  2. 数据进入客户端查询缓冲区;
  3. RESP 解析器判断是否已经得到一条完整命令;
  4. 构造 argc/argv
  5. 识别命令、参数和涉及的 Key;
  6. 将命令交给主执行线程;
  7. 进入统一命令入口进行校验;
  8. 调用具体命令处理函数。

在 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 根据命令表查找 SETGETHSET 等命令,并检查参数数量。不存在的命令或错误参数会在真正访问数据之前被拒绝。

2. ACL 和认证

检查当前连接是否已经认证,以及用户是否有权:

  • 执行当前命令;
  • 访问当前 Key;
  • 访问当前命令类别;
  • 使用受保护的管理命令。

3. Cluster Slot

Cluster 模式下,Redis 会提取命令涉及的 Key 并计算 Slot。如果多 Key 命令跨 Slot,通常返回 CROSSSLOT;如果请求发到错误节点,则返回 MOVEDASK

4. 内存与淘汰

写命令执行前可能触发内存检查和淘汰。如果无法释放足够内存,并且命令属于会增加内存的类型,Redis 返回 OOM 错误。

5. 实例状态

包括:

  • 当前是否正在加载数据;
  • 当前节点是否为只读 Replica;
  • AOF/RDB 是否出现阻止写入的错误;
  • 是否满足 min-replicas-to-write 等写安全条件;
  • 是否处于暂停状态;
  • 是否有超时脚本或繁忙模块;
  • Pub/Sub 客户端是否允许执行该命令;
  • 是否需要延迟或阻塞当前客户端。

通过这些检查后,Redis 才会进入命令处理函数。

2.5 call() 与具体命令处理函数

概念上,命令执行链路可简化为:

1
2
3
4
5
6
7
8
aeMain / event loop
-> readQueryFromClient
-> processInputBuffer
-> processCommand
-> call
-> command->proc(client)
-> addReply
-> pending writes

其中:

  • processCommand() 负责统一入口与状态检查;
  • call() 负责执行前后统计、传播控制和慢日志等工作;
  • command->proc(client) 是具体命令函数,如 setCommand()getCommand()
  • addReply() 一类函数把结果放入客户端输出缓冲区。

一次写命令完成后,还可能触发:

  • AOF 传播;
  • 复制流传播;
  • Keyspace Notification;
  • Client Tracking 失效消息;
  • 慢日志和命令统计;
  • 被阻塞客户端唤醒;
  • 过期或淘汰统计更新。

因此,“命令执行完”并不等于“只修改了一个 Hash 表”。Redis 还要维护整套可观测性、持久化和复制语义。

2.6 一个 SET 命令的完整链路

假设执行:

1
SET order:1001 PAID EX 60
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 执行 HGETALLSMEMBERS
  • 返回数百万成员的范围查询;
  • 大集合的交并差运算;
  • 复杂 Lua 脚本或 Function;
  • KEYS *
  • 大 Key 同步删除;
  • 某些高复杂度模块命令;
  • 在单次 Pipeline 中堆积过量请求与响应。

I/O 线程好比增加了收银、传菜和打包人员,但厨房里某道核心菜仍可能占住主灶台。网络吞吐提高了,并不会自动消除算法复杂度和大对象处理成本。


三、过期键、内存淘汰策略与事务

本章主要依据:EXPIREKey EvictionTransactionsexpire.cevict.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”,而是:

  1. 从过期字典中抽样;
  2. 删除样本中的过期 Key;
  3. 如果样本中过期比例仍高,继续一轮;
  4. 受 CPU 和时间预算约束,避免过期清理长期霸占主线程;
  5. 在普通周期和事件循环休眠前存在不同强度的清理机会。

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-evictionlazyfree-lazy-expire 等选项可以让某些淘汰或过期释放异步化。

异步释放降低主线程阻塞,但会形成“逻辑上已删除、物理内存稍后归还”的时间差。监控时要同时观察待释放对象与 RSS,而不能只看 Key 数量。

3.10 Redis 事务的真正语义

Redis 事务围绕:

  • MULTI
  • EXEC
  • DISCARD
  • WATCH

工作。

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 没有自动回滚

错误分为两类:

  1. 入队前错误:命令名、参数数量等错误,事务可在 EXEC 时被整体拒绝;
  2. 执行期错误:例如对 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 USAGEMEMORY STATSINFOMemory 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
SET user:1001 Mario

业务字符串总长度很小,但实际内存还包括:

  • Key 对象;
  • Value 对象;
  • SDS Header;
  • 字典 Entry 或 Key-Value 对象;
  • Hash Table Bucket;
  • 指针与对齐;
  • allocator size class 浪费;
  • TTL 元数据;
  • 可能的引用和访问元数据;
  • Rehash 期间的临时双表开销。

所以不能用:

1
Key 长度 + Value 长度

直接估算 Redis 容量。

推荐使用:

1
2
3
4
5
MEMORY USAGE user:1001
MEMORY USAGE myhash SAMPLES 0
MEMORY STATS
INFO memory
MEMORY DOCTOR

MEMORY USAGE 对嵌套类型默认抽样,SAMPLES 0 表示扫描全部成员进行估算,但对超大集合可能非常昂贵,不应在高峰期随意执行。

4.3 数据结构编码会让内存发生阶跃变化

Redis 会根据元素数量和元素长度,为小聚合对象选择紧凑编码。当超过阈值后,再转换为普通结构。

Redis 8.8.1 默认配置中常见阈值包括:

1
2
3
4
5
6
7
8
9
10
11
12
hash-max-listpack-entries 512
hash-max-listpack-value 64

set-max-intset-entries 512
set-max-listpack-entries 128
set-max-listpack-value 64

zset-max-listpack-entries 128
zset-max-listpack-value 64

list-max-listpack-size -2
list-compress-depth 0

这意味着同样的业务对象:

  • 在阈值以内可能使用 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 USAGEM_i
  • 峰值增长系数为 G_i

则数据集初步估算:

1
Dataset ≈ Σ(N_i × M_i × G_i)

这是数据集估算,不是最终 maxmemory,更不是宿主机 RSS。

第三步:加上固定与动态开销

1
2
3
4
5
6
7
8
9
10
Redis_Peak_RSS ≈
Dataset
+ Keyspace_Overhead
+ Client_Buffers
+ Replication_Backlog_And_Output_Buffers
+ AOF_Buffers
+ Cluster_Buffers
+ Allocator_Fragmentation
+ Fork_COW_Peak
+ Process_And_OS_Overhead

第四步:反推 maxmemory

工程上可采用:

1
2
3
4
5
6
7
maxmemory ≈
Physical_RAM
- OS_Reserve
- Non_Evictable_Buffers
- Expected_COW_Peak
- Fragmentation_Headroom
- Safety_Margin

这不是官方固定比例,而是一种预算思维。

4.5 容量示例

假设在预生产导入真实数据后,某类缓存对象的平均 MEMORY USAGE 为 420 Byte,峰值预计 8000 万个:

1
420 × 80,000,000 ÷ 1024³ ≈ 31.29 GiB

这只是对象样本的内存,还没有考虑:

  • 数据库字典增长和 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 为什么会形成峰值

执行 BGSAVEBGREWRITEAOF 时,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 persistenceINFO memory 中的实际观测。

4.7 内存碎片怎么看

常见关系:

1
2
3
allocator_frag_ratio ≈ allocator_active / allocator_allocated
allocator_rss_ratio ≈ allocator_resident / allocator_active
rss_overhead_ratio ≈ used_memory_rss / allocator_resident

解释时必须结合绝对值:

  • 小实例因为基数很小,比例可能看起来很高;
  • 比例接近 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 内存峰值;
  • 大数据集会造成磁盘和页缓存压力。

SAVEBGSAVE

  • SAVE 在主进程同步生成快照,会直接阻塞服务,生产通常不应随意使用;
  • BGSAVE fork 子进程生成快照,父进程继续服务,但 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
2
3
4
INCR counter
INCR counter
INCR counter
...执行 100 次

旧 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
no-appendfsync-on-rewrite yes

可以在后台保存期间避免主进程 fsync,从而降低部分延迟风险,但此时持久化安全性会退化,最坏丢失窗口可能明显增加。

官方默认是 no,即优先保持更安全的持久化语义。不要为了压低一张 P99 图,悄悄扩大 RPO 而不通知业务——那不是优化,是把问题藏进事故报告。

5.7 持久化的生产检查项

1
2
3
4
INFO persistence
INFO memory
LATENCY LATEST
LATENCY DOCTOR

重点关注:

  • 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 ReplicationRedis 8.0 Release Notesreplication.c

6.1 主从复制的三条主线

Redis 基础复制是 Primary—Replica 模型。它本身提供数据副本,不自动等于完整高可用;自动故障转移由 Sentinel 或 Cluster 等上层机制完成。

官方文档把复制概括为三种状态:

  1. 连接正常时,Primary 持续发送命令流;
  2. 短暂断线后,尝试部分重同步;
  3. 无法部分重同步时,执行全量重同步。
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 后:

  1. Primary 在本地执行命令;
  2. 将改变数据集的操作写入复制流;
  3. Replica 接收并按顺序重放;
  4. 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 过程:

  1. Primary 创建完整 RDB;
  2. 向 Replica 传输 RDB;
  3. 同步期间的新写入进入复制缓冲;
  4. Replica 加载 RDB;
  5. 再追赶增量命令流。

压力点包括:

  • 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
2
min-replicas-to-write 1
min-replicas-max-lag 10

含义是:如果 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 SpecificationScale with Redis Cluster

7.1 为什么是 16384 个 Slot

Redis Cluster 不直接把每个 Key 固定映射到某个节点,而是:

1
slot = CRC16(key) mod 16384

再把 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
2
3
order:{1001}:header
order:{1001}:items
order:{1001}:payment

这三个 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 从 PFAILFAIL

概念上:

  1. 某节点长时间无法联系另一个节点;
  2. 本地把对方标记为 PFAIL,即主观疑似下线;
  3. 故障信息通过 Gossip 传播;
  4. 足够多负责 Slot 的 Primary 对故障形成确认;
  5. 节点被标记为 FAIL,即客观下线;
  6. 如果故障节点有合格 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 使用异步复制,因此存在已确认写入丢失窗口:

  1. 客户端写入 Primary;
  2. Primary 返回成功;
  3. 写入尚未到 Replica;
  4. Primary 故障;
  5. Replica 晋升;
  6. 该写入丢失。

网络分区时,少数派侧旧 Primary 在检测到自己无法联系多数 Primary 后,会停止接受写入,但检测不是零时间,因此仍存在窗口。

Redis Cluster 的设计优先级是:

  • 高性能;
  • 线性扩展;
  • 可接受程度的写安全;
  • 多数派可用。

它不是 Raft/Paxos 风格的强一致分布式数据库。

7.9 多 Key 命令为什么会 CROSSSLOT

假设:

1
MGET user:1 user:2

如果两个 Key 不同 Slot,Redis 无法在一个节点的单线程执行边界内完成原子操作,也没有跨分片二阶段提交,因此返回 CROSSSLOT

解决方式:

  • 使用 Hash Tag,把有原子关系的 Key 放到同一 Slot;
  • 调整数据模型,让一个聚合对象使用 Hash/JSON;
  • 客户端拆分请求,并接受非原子语义;
  • 使用业务层 Saga、幂等和补偿;
  • 选择更适合跨分片事务的数据系统。

7.10 扩容和 Reshard

Cluster 扩容通常是:

  1. 新增节点;
  2. 将部分 Slot 从旧节点迁移到新节点;
  3. 逐步迁移 Slot 内的 Key;
  4. 迁移期间客户端处理 ASK
  5. 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
HSET user:1001 name Mario age 28 status active

小 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。

适合:

  • 去重;
  • 标签;
  • 共同关注;
  • 黑白名单;
  • 随机抽样;
  • 集合关系。

SUNIONSINTERSDIFF 的成本取决于集合大小。大集合交集应评估主线程占用,必要时离线计算或在 Replica 执行只读分析。

8.6 Sorted Set

ZSet 为每个 Member 关联一个 Score,并按 Score 排序。

小 ZSet 可使用 Listpack;较大 ZSet 通常使用:

  • Dictionary:按 Member 快速找到 Score;
  • Skip List:按 Score 排序、范围扫描和排名。

适合:

  • 排行榜;
  • 延迟队列;
  • 时间窗口;
  • 优先级队列;
  • 带权重排序。
1
2
3
ZADD leaderboard 100 user:1
ZADD leaderboard 120 user:2
ZREVRANGE leaderboard 0 9 WITHSCORES

延迟队列用 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
100,000,000 bit ÷ 8 ≈ 11.92 MiB

实际还会有 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 能力。

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

更新常见模式:

  1. 更新数据库;
  2. 删除缓存;
  3. 下次读取回填。

为什么通常选择删除而不是直接更新缓存:

  • 缓存值可能由多个表聚合;
  • 更新缓存与更新数据库难以形成跨系统事务;
  • 并发下直接更新可能写入旧计算结果;
  • 删除让读取路径重新构建权威状态。

但删除缓存也不是绝对一致,需要结合:

  • 延迟双删;
  • 消息通知;
  • 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
2
INCR api:count:20260727
EXPIRE api:count:20260727 86400

INCREXPIRE 分两条命令存在中间失败窗口,可使用:

  • 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
SET lock:order:1001 <unique-token> NX PX 30000

释放时必须只删除自己持有的锁。Redis 8.4 引入 DELEX ... IFEQ ...;旧版本通常使用 Lua 比较 Value 后删除。

绝不能简单:

1
DEL lock:order:1001

因为锁可能已经过期并被另一个客户端重新获取。

更重要的是,分布式锁不是拿到 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
<system>:<module>:<entity>:<id>:<attribute>

例如:

1
2
finance:settlement:bill:1001
finance:settlement:{merchant-88}:period:2026-07

原则:

  • 可读;
  • 长度适中;
  • 避免把敏感信息直接写入 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 的关键不在于“它是单线程还是多线程”这一道面试判断题,而在于理解不同工作被放在了不同执行边界:

  1. 网络 I/O 可以并行;
  2. 绝大多数核心命令仍由主执行线程串行修改键空间;
  3. 过期采用访问时检查与自适应主动抽样;
  4. 淘汰是在内存上限压力下按近似策略选择候选 Key;
  5. 事务提供顺序执行和不被插入,但没有关系数据库式自动回滚;
  6. Redis 实际 RSS 远大于简单的 Key+Value 字节数;
  7. RDB 是时间点快照,AOF 是变更日志与 Base/Increment 文件组合;
  8. 复制默认异步,WAIT 只能降低丢写概率,不能带来强一致;
  9. Cluster 通过 16384 个 Slot 水平分片,由客户端处理重定向;
  10. 数据结构编码、Big Key、Hot Key、fork COW 和缓冲内存决定了生产上限。

最终,Redis 的正确使用方式不是把所有高并发问题都“塞进 Redis”,而是先明确:

  • 数据是否允许丢;
  • 是否允许旧读;
  • 原子边界在哪里;
  • 单 Key 是否会过热;
  • 故障后如何恢复;
  • Redis 不可用时业务如何降级。

理解这些边界之后,Redis 才不是一个“快但玄学”的黑盒,而是一套可以计算、验证和演练的实时数据系统。


参考资料

本文优先引用 Redis 官方文档和 Redis 8.8.1 源码。建议阅读时以当前版本文档和实际部署版本源码为准。

  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/

  2. Redis Downloads
    https://redis.io/downloads/

  3. 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/

  4. Redis 8.8.1 src/networking.c
    https://github.com/redis/redis/blob/8.8.1/src/networking.c

  5. Redis 8.8.1 src/server.c
    https://github.com/redis/redis/blob/8.8.1/src/server.c

  6. Redis 8.8.1 src/expire.c
    https://github.com/redis/redis/blob/8.8.1/src/expire.c

  7. Redis 8.8.1 src/evict.c
    https://github.com/redis/redis/blob/8.8.1/src/evict.c

  8. Redis 8.8.1 src/replication.c
    https://github.com/redis/redis/blob/8.8.1/src/replication.c

  9. Redis 8.8.1 Example Configuration
    https://github.com/redis/redis/blob/8.8.1/redis.conf

  10. Key Expiration
    https://redis.io/docs/latest/commands/expire/

  11. Key Eviction
    https://redis.io/docs/latest/develop/reference/eviction/

  12. Redis Transactions
    https://redis.io/docs/latest/develop/using-commands/transactions/

  13. MEMORY USAGE
    https://redis.io/docs/latest/commands/memory-usage/

  14. MEMORY STATS
    https://redis.io/docs/latest/commands/memory-stats/

  15. INFO Command
    https://redis.io/docs/latest/commands/info/

  16. Memory Optimization
    https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/

  17. Redis Persistence
    https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/

  18. Redis Replication
    https://redis.io/docs/latest/operate/oss_and_stack/management/replication/

  19. Redis Cluster Specification
    https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/

  20. Scale with Redis Cluster
    https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/

  21. Redis Data Types
    https://redis.io/docs/latest/develop/data-types/

  22. Redis Pipelining
    https://redis.io/docs/latest/develop/using-commands/pipelining/

  23. SCAN
    https://redis.io/docs/latest/commands/scan/

  24. Diagnosing Latency Issues
    https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/latency/

  25. Distributed Locks with Redis
    https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/


Redis 8 深度解析:从命令执行、内存管理到持久化、复制与 Cluster
https://allendericdalexander.github.io/2026/07/27/components/redis-8-deep-dive/
作者
AtLuoFu
发布于
2026年7月27日
许可协议