上一篇【第28篇】数据库通知——Redis的事件订阅机制
下一篇【第30篇】RDB文件格式完全解析——一个快照文件里藏着什么秘密
如果说Redis是内存中的数据库,那RDB就是它的"时间胶囊"——把某一刻的内存状态完整地封存起来,万一哪天Redis挂了,打开胶囊就能还原现场。
引言:为什么需要持久化
Redis是内存数据库,数据都住在内存里。内存是什么?断电就没了。如果你的Redis存的是缓存数据也就罢了,丢就丢了,从数据库再查一遍就好。但如果是业务数据呢?用户积分、订单状态、配置信息——这些丢了可没法跟老板解释。
RDB(Redis Database)就是Redis提供的第一种持久化方案:把内存中的数据以快照的形式保存到磁盘。就像给你的Redis拍了一张全家福,不管之后发生了什么,这张照片永远记录了那个瞬间的状态。
SAVE命令:阻塞型的"危险操作"
SAVE命令是Redis最古老的持久化方式,执行后Redis主进程会阻塞,直到RDB文件生成完毕:
客户端发送 SAVE 命令 │ ▼ ┌────────────────────────────┐ │ Redis主进程被阻塞 │ │ │ │ 遍历所有数据库的所有键 │ │ 逐个写入RDB文件 │ │ │ │ 期间不能处理任何客户端请求 │ ← 所有请求排队等待 │ │ │ RDB文件生成完毕 │ │ 主进程恢复 │ └────────────────────────────┘SAVE的问题显而易见:如果Redis中有1000万个键,生成RDB可能需要几秒甚至几十秒。这几秒内Redis完全无法响应请求——对于在线服务来说,这无异于自杀。
# 执行SAVE,观察阻塞时间127.0.0.1:6379>SAVE OK(3.52s)# 大数据集可能需要更长时间踩坑提示:生产环境绝对不要使用SAVE命令!它会阻塞主进程,导致所有客户端请求超时。除非你在维护窗口期手动备份,且能容忍短暂的不可用。
BGSAVE命令:后台拍照的正确姿势
BGSAVE(Background Save)才是RDB持久化的正确打开方式。它的核心思想是:fork一个子进程来做脏活,父进程继续服务客户端。
BGSAVE的完整流程
┌─────────────────────────────────────────────────────────┐ │ BGSAVE 执行流程 │ │ │ │ 客户端: BGSAVE │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 父进程 fork() │ │ │ └──────┬───────┘ │ │ │ │ │ ┌────┴─────────┐ │ │ │ │ │ │ ▼ ▼ │ │ 父进程 子进程 │ │ (继续处理 (开始生成RDB) │ │ 客户端请求) │ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ 遍历键空间 │ │ │ │ │ 写入临时 │ │ │ │ │ RDB文件 │ │ │ │ └─────┬──────┘ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ 重命名为 │ │ │ │ │ dump.rdb │ │ │ │ └─────┬──────┘ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ 通知父进程 │ │ │ │ │ (信号机制) │ │ │ │ └────────────┘ │ │ │ │ │ ▼ │ │ 父进程收到通知 │ │ 更新 lastsave 时间 │ │ RDB完成! │ │ │ └─────────────────────────────────────────────────────────┘BGSAVE的关键步骤:
- 父进程调用
fork()系统调用创建子进程 - 子进程根据父进程的内存快照生成RDB文件(写到临时文件)
- 子进程完成后,将临时文件重命名为
dump.rdb - 子进程通过信号通知父进程RDB已完成
- 父进程更新
lastsave时间戳
# 执行BGSAVE,不阻塞主进程127.0.0.1:6379>BGSAVE Background saving started# 期间可以正常执行其他命令127.0.0.1:6379>GET user:1"Tom"SAVE vs BGSAVE 对比
| 维度 | SAVE | BGSAVE |
|---|---|---|
| 是否阻塞 | 完全阻塞主进程 | 仅fork瞬间短暂阻塞 |
| 执行者 | 主进程 | 子进程 |
| 客户端可用性 | 不可用 | 可用 |
| 适用场景 | 仅维护窗口 | 生产环境标准用法 |
| 内存开销 | 无额外开销 | fork需要额外内存 |
| 复杂度 | 低 | 高(涉及fork和COW) |
fork与写时复制(COW)
BGSAVE之所以能让父进程继续服务,关键在于fork()系统调用和**写时复制(Copy-On-Write, COW)**机制。
fork()的原理
fork()是Linux系统调用,它创建一个子进程,子进程是父进程的完整副本——包括内存空间、文件描述符等。但Linux采用了COW优化:
fork() 之后 │ ▼ ┌──────────────────────────────────────────┐ │ 物理内存页 │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ P1 │ │ P2 │ │ P3 │ │ P4 │ │ P5 │ │ │ └─┬──┘ └──┬─┘ └──┬─┘ └──┬─┘ └──┬─┘ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ ┌─────────────────────────────┐ │ │ │ 共享的页表(只读映射) │ │ │ └──────────────┬──────────────┘ │ │ │ │ │ ┌──────┴──────┐ │ │ │ │ │ │ 父进程页表 子进程页表 │ │ (指向同一 (指向同一 │ │ 物理页) 物理页) │ └──────────────────────────────────────────┘ 当父进程修改某个页时(比如修改P3): │ ▼ ┌──────────────────────────────────────────┐ │ 操作系统复制 P3 → P3' │ │ 父进程页表指向 P3'(可写) │ │ 子进程页表仍指向 P3(只读,不变) │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ P1 │ │ P2 │ │P3' │ │ P4 │ │ P5 │ ← 父进程用 │ └────┘ └────┘ └────┘ └────┘ └────┘ │ │ ┌────┐ │ │ │ P3 │ ← 子进程用(原版) │ │ └────┘ │ └──────────────────────────────────────────┘COW的核心思想:
- fork()后,父进程和子进程共享同一块物理内存(页表设为只读)
- 当父进程(或子进程)试图修改某个内存页时,触发页错误(page fault)
- 操作系统将该页复制一份,修改方使用新副本,未修改方继续使用原页
- 这样,fork()本身不需要复制所有内存,只有在修改时才逐页复制
COW对Redis的影响
COW机制意味着:
- fork()本身很快:只复制页表,不复制实际内存
- 子进程看到的是fork瞬间的数据快照:因为子进程的页表不会改变
- 父进程的写操作会触发页复制:每个被修改的页都需要复制一份
- 内存开销取决于写操作的覆盖范围:如果BGSAVE期间只有10%的页被修改,额外内存开销约为数据量的10%
踩坑提示:如果你的Redis实例占用了30GB内存,BGSAVE期间写操作修改了50%的页,那么额外需要约15GB内存。如果系统没有足够的空闲内存,就会触发OOM Killer或者swap,导致Redis性能急剧下降。建议:Redis服务器预留至少30%-50%的可用内存给COW使用。
fork大内存实例的注意事项
对于大内存Redis实例(比如超过10GB),fork本身也可能成为问题:
fork耗时随内存增长:虽然fork不需要复制内存,但复制页表本身也需要时间。30GB内存的实例fork可能需要数百毫秒。
fork期间主进程短暂阻塞:fork是在主线程中执行的,虽然时间短,但对于延迟敏感的应用(P99延迟<1ms),这可能导致偶尔的延迟毛刺。
使用THP(Transparent Huge Pages)会加剧问题:THP将4KB页合并为2MB大页,减少了页表条目数,但fork时需要处理的每个页更大,COW的代价更高。
# 检查THP状态cat/sys/kernel/mm/transparent_hugepage/enabled# 建议关闭THP(对Redis更友好)echonever>/sys/kernel/mm/transparent_hugepage/enabled自动触发RDB的条件
除了手动执行BGSAVE,Redis还可以根据配置自动触发RDB持久化。
save配置参数
在redis.conf中,save参数定义了自动触发BGSAVE的条件:
# 默认配置(Redis 7.0+)save36001# 3600秒(1小时)内有至少1次修改save300100# 300秒(5分钟)内有至少100次修改save6010000# 60秒(1分钟)内有至少10000次修改满足任意一个条件,就会触发BGSAVE。
Redis内部的dirty计数器和lastsave时间戳 │ ▼ ┌──────────────────────────────────────────┐ │ 每次写操作:dirty++ │ │ BGSAVE完成后:dirty = 0, lastsave = now │ │ │ │ serverCron每100ms检查: │ │ ┌──────────────────────────────────┐ │ │ │ if (now - lastsave >= 3600 │ │ │ │ && dirty >= 1) │ │ │ │ → BGSAVE │ │ │ │ │ │ │ │ if (now - lastsave >= 300 │ │ │ │ && dirty >= 100) │ │ │ │ → BGSAVE │ │ │ │ │ │ │ │ if (now - lastsave >= 60 │ │ │ │ && dirty >= 10000) │ │ │ │ → BGSAVE │ │ │ └──────────────────────────────────┘ │ └──────────────────────────────────────────┘踩坑提示:如果不想使用RDB持久化,可以注释掉所有save行,或者设置
save ""。但请注意,这并不意味着数据不会丢失——只是不会自动保存到磁盘。
禁用RDB的配置
# 方式一:redis.conf中save""# 方式二:运行时配置CONFIG SET save""其他触发RDB的时机
除了手动和自动触发,还有几种场景也会产生RDB:
| 触发场景 | 说明 |
|---|---|
| DEBUG RELOAD | 调试命令,重新加载Redis时会先保存RDB |
| FLUSHALL | 清空所有数据库时,如果配置了save,会先触发RDB(RDB文件为空) |
| 主从复制 | 主服务器执行全量同步时,会生成RDB发送给从服务器 |
| SHUTDOWN | Redis正常关闭时,如果配置了save,会执行BGSAVE(等子进程完成后再退出) |
主从全量同步中的RDB生成 │ ▼ ┌──────────┐ BGSAVE ┌──────────┐ │ 主服务器 │ ──────────► │ 子进程 │ │ │ │ 生成RDB │ │ │ └────┬─────┘ │ │ │ │ │ ┌───────────────┘ │ │ │ 发送RDB文件 │ │ ▼ │ │ ┌──────────┐ │ │ │ 从服务器 │ │ │ │ 接收RDB │ │ │ │ 载入数据 │ │ │ └──────────┘ └──────────┘踩坑提示:
FLUSHALL命令会生成一个空的RDB文件,覆盖之前的RDB备份!如果你不小心执行了FLUSHALL,而此时自动save已经触发,那之前的数据就再也找不回来了。建议在执行FLUSHALL之前先备份dump.rdb文件。
RDB文件的位置和命名
RDB文件的存储位置由两个配置参数控制:
# redis.confdir/var/lib/redis# RDB文件存放目录dbfilename dump.rdb# RDB文件名# 查看当前配置127.0.0.1:6379>CONFIG GETdir1)"dir"2)"/var/lib/redis"127.0.0.1:6379>CONFIG GET dbfilename1)"dbfilename"2)"dump.rdb"踩坑提示:
dir参数也影响AOF文件的存储位置。建议将RDB和AOF文件放在有足够磁盘空间的目录,并确保Redis进程有写入权限。生产环境建议放在独立的磁盘或SSD上,避免和日志等其他IO密集型操作争抢磁盘带宽。
RDB文件校验
Redis提供了redis-check-rdb工具来检查RDB文件的完整性:
# 检查RDB文件redis-check-rdb /var/lib/redis/dump.rdb# 输出示例:[offset0]Checking RDBfile/var/lib/redis/dump.rdb[offset26]AUX FIELD redis-ver='7.0.12'[offset40]AUX FIELD redis-bits='64'[offset13443]Selecting DB ID0[offset13567]CHECKSUM: OK如果RDB文件损坏,你可以尝试修复(但可能丢失部分数据):
# 注意:修复操作可能丢失损坏部分之后的所有数据# 修复前请先备份原始RDB文件!cpdump.rdb dump.rdb.bak redis-check-rdb--fix/var/lib/redis/dump.rdbLASTSAVE命令
LASTSAVE命令返回Redis最后一次成功保存RDB的Unix时间戳:
127.0.0.1:6379>LASTSAVE(integer)1687700123# 转换为可读时间# 在bash中:date-d@1687700123# Mon Jun 26 14:28:43 UTC 2023这个命令在监控中很有用——如果你设置了自动save,可以定期检查LASTSAVE是否在合理时间范围内更新:
# 监控脚本:检查RDB是否正常保存#!/bin/bashLASTSAVE=$(redis-cli LASTSAVE)NOW=$(date+%s)DIFF=$((NOW-LASTSAVE))if[$DIFF-gt3600];thenecho"WARNING: RDB not saved for${DIFF}s"fiRDB的优缺点
优点
恢复速度快:RDB是紧凑的二进制文件,恢复时直接载入内存,速度远快于AOF重放。对于大数据集,RDB恢复可能只需要AOF恢复时间的1/10甚至更快。
文件紧凑:RDB文件经过压缩,体积通常比AOF小很多。
对性能影响小:BGSAVE由子进程完成,父进程正常服务。fork+COW的额外开销可控。
适合备份:RDB文件是一个完整的时间点快照,非常适合定期备份到S3等远程存储。
缺点
- 数据丢失风险:RDB是定时快照,两次快照之间的数据修改在Redis崩溃后会丢失。最坏情况下,可能丢失最近一个save周期内的所有数据。
时间轴: │ ▼ 12:00 RDB保存 ← 此时数据完整保存 │ ▼ 12:05 写入key A 12:10 写入key B 12:15 写入key C │ ▼ 12:18 Redis崩溃! ← key A/B/C 丢失! │ (不在RDB中) ▼ 重启后从RDB恢复 ← 只能恢复到12:00的状态fork大内存实例的开销:如前所述,大内存实例的fork可能需要较长时间,且COW需要额外内存。
不适合实时性要求高的场景:如果你的业务要求每秒级的数据安全,RDB显然不够。
RDB优缺点与AOF的初步对比
| 维度 | RDB | AOF |
|---|---|---|
| 持久化方式 | 快照(全量) | 日志追加(增量) |
| 数据安全 | 可能丢失一个周期的数据 | 最多丢失1秒(默认配置) |
| 恢复速度 | 快(直接载入) | 慢(重放所有写命令) |
| 文件大小 | 紧凑 | 较大(重写后可缩小) |
| 系统开销 | fork时的短暂阻塞 | 每次写入都追加AOF |
| 适用场景 | 备份、灾备、可容忍少量丢失 | 数据安全要求高 |
生产配置建议
# redis.conf 生产环境推荐配置# 1. 开启BGSAVE,设置合理的save阈值save9001# 15分钟内有1次修改save30010# 5分钟内有10次修改save6010000# 1分钟内有10000次修改# 2. RDB文件名(建议包含端口号,避免多实例冲突)dbfilename dump-6379.rdb# 3. 存储目录(使用独立磁盘)dir/data/redis/rdb# 4. 启用压缩(对List/Set/Hash等大对象使用LZF压缩)rdbcompressionyes# 5. 启用RDB文件校验rdbchecksumyes# 6. 关闭THP# 在系统层面执行:# echo never > /sys/kernel/mm/transparent_hugepage/enabled# 7. 确保系统有足够内存# 建议可用内存 >= Redis最大内存 * 1.5踩坑提示:如果你的Redis实例同时开启了RDB和AOF,Redis重启时会优先使用AOF来恢复数据(因为AOF的数据更完整)。所以即使配置了RDB,如果AOF也是开启的,RDB更多是作为备份手段存在。
总结
RDB持久化是Redis最基础的持久化方案:
- SAVE是阻塞型的危险操作,生产禁用;BGSAVE通过fork子进程实现后台保存
- fork + COW机制让子进程获得内存快照的同时,父进程可以继续服务
- 自动触发条件通过
save参数配置,满足任意一个即触发BGSAVE - RDB的优点是恢复速度快、文件紧凑;缺点是有数据丢失风险
- 大内存实例需要关注fork耗时和COW内存开销
了解了RDB是怎么拍的快照,下一篇我们就来拆解这张"照片"本身——RDB文件的格式里到底藏着什么秘密?
上一篇【第28篇】数据库通知——Redis的事件订阅机制
下一篇【第30篇】RDB文件格式完全解析——一个快照文件里藏着什么秘密