第
8 篇 讨论内存触顶时的 key
驱逐;本篇进入持久化第一条路径:RDB
快照。RDB
把某一时刻的内存数据集序列化成紧凑二进制文件,恢复时直接
load 进结构,比重放 AOF
命令快,但两次快照之间的写入默认会丢——这是快照语义,不是实现
bug。
本文锚定 Redis OSS 7.4.x
rdb.c /
rdb.h(RDB_VERSION
12)。不粘贴本机 BGSAVE 延迟或 COW
体积数字;fork
代价与数据集、写压力强相关,需在生产相似环境实测。
本文是「Redis / 缓存内核」系列第 9 篇(共 16 篇)。→ 系列目录
版本锚定:Redis OSS 7.4 / 8.x;RDB 格式版本 12(
REDIS0012魔数)。
一、问题:快照是时间点副本,不是连续日志
数据库持久化两条经典路线在 Redis 里并存:
| 路线 | Redis 实现 | 恢复速度 | 丢失窗口 |
|---|---|---|---|
| Snapshot | RDB | 快(顺序读二进制) | 上次成功快照 → 崩溃 |
| Command log | AOF | 慢(重放 RESP) | 取决于 appendfsync(第 10 篇) |
RDB 对应文献里的 checkpoint / snapshot
recovery:在 \(t_0\) 冻结一致视图,崩溃后回到
\(t_0\)。与 LSM 的 SST
快照不同,Redis RDB 在单进程内存状态上通过
fork
获得子进程只读视图,父进程继续服务写请求——依赖 OS
copy-on-write(COW):子进程读页时与父共享物理页,父修改时复制页。
工程痛点:大内存 + 写密集 → COW 破裂页多 → 父进程 RSS 近乎翻倍、fork 本身阻塞主线程数毫秒到数秒——第 15 篇 会收束排障;本篇钉机制与格式。
二、SAVE 与 BGSAVE:同步 vs 后台
2.1 SAVE:阻塞式
SAVE 命令在主线程调用 rdbSave()
→
rdbSaveInternal(),直接写磁盘直到完成。期间事件循环无法处理其他命令——生产禁用,仅适合维护窗口或空实例验证。
2.2 BGSAVE:fork 子进程
BGSAVE 走
rdbSaveBackground():
int rdbSaveBackground(int req, char *filename, rdbSaveInfo *rsi, int rdbflags) {
if (hasActiveChildProcess()) return C_ERR;
if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
/* Child */
retval = rdbSave(req, filename, rsi, rdbflags);
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent 继续服务 */
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
return C_OK;
}
}约束:
- 同一时刻只能有一个
RDB或AOF rewrite子进程(hasActiveChildProcess())。 - 子进程标题
redis-rdb-bgsave,可绑bgsave-cpulist。 - 完成后父进程
stopSaving(),更新lastsave;失败则lastbgsave_status = C_ERR。
复制全量同步也可通过 rdbSaveToSlavesSockets
走 diskless RDB(socket 直传),格式与文件
RDB 相同,省略本地临时文件。
sequenceDiagram
participant M as Parent main thread
participant C as Child redis-rdb-bgsave
participant D as Disk / Socket
M->>C: redisFork CHILD_TYPE_RDB
Note over M: continues serving writes<br/>COW on modified pages
C->>D: rdbSaveRio walk keys
C-->>M: exit status
M->>M: rename temp.rdb, update lastsave
2.3 COW 语义边界
fork 后子进程遍历的是逻辑上一致的内存图;父进程新写入的 key 不会进入本次 RDB。父进程修改已有 key 会触发 COW 复制底层页——内存压力来自「变更页」而非「子进程读」本身。大 key(巨型 Hash/Stream)的单页或连续页变更成本高,与 第 7 篇 lazyfree 删除大对象叠加时,RSS 曲线常呈锯齿。
子进程在 7.4 可对已序列化对象调用
dismissObject()
提示内核回收页(madvise
类路径),减轻双份占用——是否生效取决于 OS 与分配器。
三、RDB 格式锚点
rdbSaveRio() 是写路径核心;读路径对称为
rdbLoadRio()。
3.1 文件头
snprintf(magic, sizeof(magic), "REDIS%04d", RDB_VERSION);
/* Redis 7.4: RDB_VERSION = 12 → "REDIS0012" */随后可选 AUX
字段(RDB_OPCODE_AUX):redis-ver、redis-bits、ctime、used-mem
等元数据,供运维与兼容性检查。
3.2 逻辑结构(opcode 驱动)
| Opcode / 段 | 含义 |
|---|---|
RDB_OPCODE_SELECTDB |
切换 DB 编号 |
RDB_OPCODE_RESIZEDB |
预告 hash 大小(加速 rehash) |
RDB_OPCODE_SLOT_INFO |
Cluster slot 映射(集群) |
RDB_OPCODE_EXPIRETIME_MS |
紧随 key 的毫秒过期 |
RDB_OPCODE_IDLE |
LRU idle(可选) |
RDB_OPCODE_FREQ |
LFU 频率(可选) |
RDB_OPCODE_FUNCTION2 |
Functions(7.x) |
RDB_OPCODE_MODULE_AUX |
模块辅助数据 |
| 类型字节 + key + value | 各 OBJ_*
编码(string/list/hash/zset/stream…) |
RDB_OPCODE_EOF |
结束 |
| CRC64 | 校验和(可选 rdbchecksum) |
值编码与 第
3–6 篇 的 type/encoding
一一对应:如 ziplist/listpack 紧凑形式、skiplist+dict 的
ZSet 等。读入时可能触发 encoding
升级(与在线写入路径相同规则)。
3.3 单 key 写序
rdbSaveKeyValuePair() 顺序:可选过期 opcode
→ rdbSaveObjectType → key →
rdbSaveObject(按类型分支)。过期时间用毫秒(RDB_OPCODE_EXPIRETIME_MS);旧版秒级
EXPIRETIME 仍可读。
四、触发方式与配置
| 机制 | 配置 / 命令 |
|---|---|
| 手动 | SAVE、BGSAVE |
| 定时 | save <sec> <changes>,在
serverCron 检查 dirty 计数 |
| 关闭前 | shutdown save(默认) |
| 复制 | 全量同步前子进程或 diskless 生成 RDB |
stop-writes-on-bgsave-error yes 时,上次
BGSAVE
失败则拒绝写——避免在无持久化保障下继续累积无法快照的数据。
无盘实例(appendonly yes
且禁用 RDB):官方支持纯 AOF;RDB 仍可作为复制与混合 AOF
base 的载体(第
11 篇)。
五、学术谱系与争论
谱系:System R 影子页 / fuzzy checkpoint → Redis antirez 时代 RDB 二进制格式 → 7.x 多模块、Function、Cluster slot 扩展 opcode。
争论:
- RDB vs 仅 AOF:RDB 恢复更快、文件更紧凑;丢失窗口大。金融级「每条提交可恢复」不应单靠定时 RDB。
- fork vs 无 fork 快照:fork 模型简单且与单线程兼容,但内存倍化风险使大实例运维谈 BGSAVE 色变;diskless 缓解磁盘 IO,不消除 COW。
- checksum:
rdbchecksum防静默损坏;与 数据完整性 端到端校验互补,不覆盖传输后内存位翻转。
开放问题:TB 级内存实例上 fork 延迟与
COW 峰值仍缺通用公式;Linux MADV_DONTNEED /
dismiss 与 jemalloc
交互在不同内核版本上行为不一,需结合
INFO persistence 与 /proc
实测。
六、常见误解
6.1 「BGSAVE 不阻塞主线程」
fork 瞬间与 COW
压力仍影响延迟;只是不阻塞在写完整文件上。LATENCY DOCTOR
可能出现 fork 事件。
6.2 「RDB 包含 fork 之后的写入」
不包含。RDB 是子进程开始遍历时的切片;之后写入靠 AOF 或下一次快照。
6.3 「SAVE 比 BGSAVE 更安全」
两者快照语义相同;SAVE 只是阻塞更久,不增加持久性级别。
6.4 「RDB 文件是命令日志」
不对。RDB 是序列化对象;AOF 才是 RESP 命令文本(或混合 preamble 的 RDB 段)。
七、小结
三句话小结
- BGSAVE 通过
fork子进程写rdbSaveRio,父进程继续写,一致性停在 fork 时刻。 - COW 使父进程修改页可能双倍占内存;大 key 与写密集是主要风险。
- 格式 以
REDIS0012开头,opcode 流编码多 DB、过期、LRU/LFU 与各 encoding 对象。
上一篇:maxmemory 淘汰
下一篇:AOF 命令日志
参考资料
- Redis OSS 7.4
源码:
src/rdb.c(rdbSaveBackground、rdbSaveRio、rdbSaveKeyValuePair)、src/rdb.h(RDB_VERSION、RDB_OPCODE_*)。 - Redis Documentation v7.x, Persistence、RDB、Replication(diskless)。
- 数据完整性:fsync 与端到端校验(静默损坏语境)。
- 系列 index、第 10 篇 AOF、第 11 篇混合持久化。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Redis / 缓存内核】AOF:命令日志 · rewrite · appendfsync
从 aof.c 拆解 AOF 多段清单、feedAppendOnlyFile 写路径、后台 rewrite 与 appendfsync always/everysec/no 各自的丢失窗口及 fsync 失败语义。
【Redis / 缓存内核】生产排障:大 key · fork · 慢日志与可复现清单
按 Redis Latency Monitoring 与 INFO/SLOWLOG 信号,建立大 key、fork 延迟、阻塞命令、内存碎片四类故障的排查决策树;给出可在本机 redis-server 上执行的复现命令清单,不粘贴伪造输出。
【Redis / 缓存内核】事件循环 · 对象编码 · 淘汰 · RDB/AOF · Cluster
补齐内存 KV / 缓存服务器内核层:从 ae 事件循环与 redisObject 多态编码,到 TTL/maxmemory 近似淘汰、RDB/AOF 持久化语义,再到复制、Cluster hash slot 与 Memcached 对照。
【Redis / 缓存内核】TTL 与过期:expires dict · 惰性/主动删除 · lazyfree
拆解 Redis 7.4 的 TTL 机制:expires 辅助表、lookup 路径惰性删除、activeExpireCycle 自适应采样,以及 lazyfree 异步释放大对象对延迟的影响。