拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis持久化详解:RDB与AOF原理、对比及生产实践

Redis持久化详解:RDB与AOF原理、对比及生产实践

1. 先聊一个残酷的现实:持久化没配好,Redis就是一只会漏水的桶

1.1 一次让我印象深刻的线上宕机事故

有次半夜被电话叫醒,说线上Redis主节点挂了,业务方反馈用户购物车和登录状态丢了。我当时第一反应是"重启恢复不就完了",结果重启之后实例正常起来,内存里的数据却少了一大截——往前查了查,最近一个多小时写入的key全部消失。

原因说出来其实很尴尬:那台实例用的是默认RDB配置,save规则是3600秒内至少有1次写、300秒内至少有100次写、60秒内至少有10000次才触发快照。而这是一个低峰期的缓存业务,写量根本达不到60秒1万次的阈值,于是最后一次有效快照还是一个小时前的。进程被系统OOM Killer干掉的那一刻,内存里那一小时的新数据跟着一起没了。

这个事故让我记住一件事:Redis默认配置不等于安全配置。很多人以为Redis是数据库,重启不会丢数据,但实际上它首先是一个内存数据库,持久化需要主动设计和验证。

1.2 内存数据库为什么必须考虑持久化

Redis所有数据默认都活在内存里,读写速度快,靠的就是"不需要碰磁盘"这个特性。但代价是:进程异常退出、断电、操作系统重启、容器被调度到另一台机器,内存里的东西说没就没。

持久化解决的不仅是"宕机不丢数据"这一个问题,它还承载着三层职责:

  • 容灾恢复:进程崩溃、机器故障后,能把数据重新加载回来。
  • 数据迁移与备份:从一台机器到另一台机器,从本地到云主机,把RDB文件拷贝过去就能恢复。
  • 审计与分析:AOF文件里保留了写命令的文本记录,某些场景下能回溯操作轨迹。

Redis官方提供两种持久化方案,RDB(Redis Database,内存快照)和AOF(Append Only File,追加写文件)。两者思路完全不同,一个像给整个数据集拍合影,一个像给每条写操作记账。搞懂它们各自的原理、代价和适用边界,比单纯背配置项重要得多。

1.3 动手前需要准备的环境

后面我会给出大量配置和命令,建议你本地装一个Redis实例跟着操作。安装方式不多说,官网下载源码编译、apt install redis-server、brew install redis、docker run -d --name redis-test redis:7.0都可以,版本尽量选Redis 5.0以上,最好直接上7.x,因为新版AOF已经改成Multi Part结构,排查思路和老版本有些差异。

装好后用redis-cli info persistence看一眼当前状态,你会看到rdb_bgsave_in_progress:0、aof_enabled:0之类的字段。后面所有实验都从这个输出开始。

2. RDB:一张按时间点拍摄的全量快照

2.1 RDB的工作方式:fork与写时复制

RDB的思路用一句话概括:在某个时间点把内存里的全量数据打成二进制包,落盘成dump.rdb。

触发方式分两种,save和bgsave。save是同步阻塞式的,执行期间Redis主进程停止处理所有命令,数据量大的时候会造成明显卡顿,生产环境基本没人直接用。真正用的是bgsave:主进程调用fork()创建一个子进程,由子进程负责把数据写入临时RDB文件,写完后再原子替换旧文件,主进程继续处理命令。

这里最关键的是fork之后的内存共享机制。子进程刚被创建出来时,和父进程共享同一份物理内存页,并不需要把整个数据集复制一遍,所以fork瞬间很快。但Redis是写入频繁的数据库,父进程里每发生一次写操作,操作系统就会把对应的内存页标记为脏页并复制一份给子进程,这就是写时复制(Copy On Write,COW)。

你可以把父子进程想象成两个人在共看一本书,本来都翻同一页。父进程要在页上做笔记,系统会先复印一张干净的页面给他,保证子进程看到的还是fork那一刻的内容。

这个机制带来的副作用很直接:bgsave期间写入越频繁,被复制的脏页越多,Redis实际内存占用就越高。之前我有个24GB的实例,高峰期触发bgsave,内存直接从24GB涨到接近30GB,如果机器内存本来就紧张,很容易触发swap甚至OOM。

2.2 RDB的触发条件与配置项

默认配置是Redis 7.0版本中自带的:

save 3600 1 save 300 100 save 60 10000 dbfilename dump.rdb dir ./ stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes

save后面的两个数字含义是"时间间隔 + 修改次数",只要两个条件同时满足任一组的阈值,就触发一次bgsave。也就是说:

  • 3600秒(1小时)内至少有1次写入;
  • 300秒(5分钟)内至少有100次写入;
  • 60秒内至少有1万次写入。

用save ""可以完全关闭RDB。stop-writes-on-bgsave-error yes意思是如果bgsave失败(常见原因是磁盘满、权限不对),Redis会拒绝写入命令,这个策略虽然粗暴,但能防止"自以为有持久化、实际根本没备份"的假象。

除了自动触发,还有几种场景会触发bgsave:

  • 执行redis-cli bgsave手动触发;
  • shutdown关闭Redis时,如果RDB已启用,会尝试保存快照;
  • 主从复制时,从节点全量同步也可能触发主节点bgsave。

实际运维中我很少依赖自动save规则,因为低峰期写量达不到阈值,快照间隔会被拉得很长,数据丢失窗口不可控。更可靠的做法是:写一个定时任务,凌晨低峰期手动bgsave,把RDB文件异地备份。

2.3 dump.rdb文件长什么样

RDB文件是二进制格式,不是给人直接读的。文件内容包括:

  • 固定开头:"REDIS"五个字符加上版本号;
  • 数据库编号与哈希表大小;
  • 序列化后的key-value数据;
  • 结尾的CRC64校验和,用于加载时检查文件是否完整。

rdbcompression yes表示对字符串类型的value使用LZF算法压缩,所以实际文件体积通常比内存数据小。rdbchecksum yes会在写入和加载时计算CRC64,文件损坏的话加载会失败,Redis会拒绝启动。

想快速确认文件是否正常,可以用:

redis-cli --rdb dump.rdb

或者干脆把实例停掉重启,看日志里有没有DB loaded from disk这类输出。

2.4 RDB的"省心"之处和"致命"短板

RDB最让人舒服的地方是运维简单:

优势说明
文件紧凑压缩后的二进制文件体积小,适合备份、传送到异地
恢复极快加载RDB是直接反序列化灌入内存,比重放命令快一个数量级
迁移友好拷贝一份rdb文件到新机器即可完成数据迁移
不影响主线程bgsave由子进程完成,fork之外不阻塞查询

短板同样明显:

  • 数据丢失窗口大。两次快照之间所有写入都会丢,默认配置下窗口可能以小时计。
  • fork过程可能卡顿。实例内存越大、fork耗时越长,我实测遇到过24GB实例在写入高峰期fork花了1.2秒,期间所有命令排队。
  • 没有命令粒度。你无法得知快照之间到底执行过哪些写操作,出了人为问题(比如误删数据)只能回滚到上一个快照点,中间数据只能手动补。

3. AOF:每一笔写操作都被记账,但那本账也有烦恼

3.1 AOF写入链路与三档刷盘策略

AOF的思路和RDB完全不同:把每一条写命令以Redis序列化协议(RESP)的文本格式追加到文件末尾。你执行SET name tom,AOF文件里就多一行对应的命令记录。重启时逐条重放这些命令,就能还原数据。

写入链路大概是这样的:

  1. 客户端发来写命令,Redis在主进程中执行命令、修改内存数据;
  2. 同时把这条命令追加到内存中的aof_buf缓冲区;
  3. 按照刷盘策略决定什么时候把缓冲区内容真正交给操作系统写入磁盘;
  4. 操作系统把page cache中的数据持久化到磁盘。

关键在第三步,也就是appendfsync配置项,它决定数据落盘的实时性:

appendfsync 值行为数据安全性能代价
always每条写命令都调用fsync()强制落盘理论上零丢失最大,写延迟明显升高
everysec每秒调用一次fsync(),批量刷盘最多丢1秒数据较小,推荐
no交给操作系统自行决定何时刷盘最危险,可能丢数秒甚至更多数据最小

很多人不理解fsync()到底做了什么。操作系统收到write调用后,数据先进page cache,真正的磁盘写入由内核决定。如果内核还没写盘就断电或者崩溃,page cache里的内容就丢了。fsync()的作用是强制把脏页刷到磁盘,确保数据不太监。

everysec是大多数生产实例的默认选择,它把数据丢失窗口控制在一秒以内,同时每秒只调用一次fsync,对性能影响很小。如果你觉得"绝对不能让命令丢",才需要开always,但要做好写QPS下降的心理准备。no我基本不建议使用,省下的性能远比不上数据风险。

3.2 AOF重写:为什么文件会越来越大

AOF是追加写,同样的key被反复修改一百次,就会有一百条命令记录在里面,比如:

SET counter 0 INCR counter INCR counter ...(重复100次)

文件越来越大,加载时越来越慢,磁盘占用也越来越难看。解决方案是AOF重写:根据当前内存里的数据,生成一组能还原现状的最小命令集合。比如上面100条INCR,重写后只保留一条SET counter 100。

手动触发用:

redis-cli bgrewriteaof

自动触发由配置控制:

auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

含义是:当前AOF文件超过auto-aof-rewrite-min-size(默认64MB)且文件大小比上一次重写时增长超过100%时,触发一次新的重写。注意,AOF重写同样走fork子进程,不会阻塞主线程,但会和RDB的bgsave一样带来内存瞬间膨胀的问题。

Redis 7.0之后AOF文件从单个appendonly.aof改成Multi Part结构:一个基础文件(base,通常是某个时间点的RDB格式或AOF格式全量数据)加若干个增长文件(incr,记录基础文件之后的新写入)。这样做的好处是重写不再需要重写整个超大文件,只需要生成新的base,增量部分单独存放,更稳。

3.3 AOF文件格式与重启加载

AOF文件是可读的文本文件,比如执行:

SET username tom

对应的AOF记录是:

*3 $3 SET $8 username $3 tom

*3表示这条命令有三个参数,$3表示接下来这个参数长度是3个字节,以此类推。这种格式叫RESP协议,Redis客户端和服务器之间通信用的也是它,所以AOF其实就是在"录制"客户端发来的写命令。

重启加载时,Redis从头逐条解析并执行这些命令。数据量小的时候还好,一旦AOF文件撑到几个GB,重放过程可能要好几分钟。这个阶段Redis对外不可用(除非配置了主从),所以AOF恢复速度和RDB完全没法比。

3.4 AOF的短板

AOF的优点很吸引人——丢失窗口小、文件可读可修复,但它有自己的代价:

  • 性能开销:即使开everysec,高频写入场景下磁盘I/O压力也比RDB大;开always则直接影响主线程写延迟。
  • 文件体积:同一份数据集,AOF通常比RDB大不少(写到同样的命令,RDB存的是最终结果的压缩快照)。
  • 恢复慢:逐条重放命令,远慢于RDB的直接反序列化。
  • 配置复杂度:追加写、重写、Multi Part,概念比RDB多得多,理解和排查成本都更高。

4. 正面对比:RDB和AOF在不同维度上的优劣格局

4.1 一张核心对比表看穿本质

对比维度RDBAOF
数据安全丢失窗口大,取决于save触发间隔always下零丢失,everysec丢1秒
恢复速度极快,直接加载二进制快照慢,逐条重放命令
文件体积小,LZF压缩大,命令文本累积
对主进程影响fork时有短暂阻塞,写多时内存膨胀fsync频率决定I/O开销,重写时同样fork
运维友好度黑白分明,配置简单概念多,重写机制需要理解
适用场景备份、迁移、缓存数据安全敏感的在线业务

举一个我实测过的恢复耗时对比:一个500万key、约6GB内存的实例,RDB文件大概1.8GB,加载耗时约45秒。同一实例开启AOF后文件膨胀到5GB多,重启重放耗时接近4分钟。对这个体量的业务来说,30秒和4分钟的差距是不可接受的。

4.2 写放大与读放大的心理账

用数据库领域的视角看,RDB和AOF的差别本质上是写放大和读放大的权衡。RDB每触发一次就是全量数据落盘,写放大极高(哪怕只改了1个key,也要把几GB快照重写一遍),但读取时放大很低,一次加载到位。AOF每次写入只有一条命令,写放大低,但恢复时需要重放大量历史命令,读放大高。

理解了这层关系,你就会明白为什么不存在"完美的持久化方案"——RDB用频繁的写放大换来了快速恢复,AOF用稀疏的写放大换来了数据安全。混合持久化就是试图在两者之间找到平衡点。

4.3 一个常见的误解:AOF不是绝对保险箱

AOF看起来比RDB稳,但千万别把"开启AOF"等同于"数据绝对不丢"。

  • 如果业务在高峰期刷盘策略是everysec,那一秒内的数据照样丢;
  • AOF重写期间父进程收到的新命令,会先记在重写缓冲区,如果此时进程崩溃,这些命令是否写进新文件取决于源码实现,历史上出过bug;
  • 人为误操作不在AOF的保护范围内——你执行了DEL key,AOF忠实地把这条命令写下来,重启后照样执行删除。

持久化只解决"进程异常退出导致数据消失"这一类问题,不解决"人把数据改坏了"的问题。后者需要靠备份时间点恢复、命令审计和权限管控。

5. 生产环境最容易踩的五个持久化坑,含完整排查链路

5.1 fork耗时长导致主线程卡顿

症状:监控显示Redis响应延迟周期性出现尖峰,每次持续几百毫秒到几秒,业务方反馈接口超时。

排查链路:

  1. 执行redis-cli --latency -i 1,观察延迟曲线的尖峰节奏;
  2. 打开redis-cli info stats,重点看latest_fork_usec字段,单位是微秒,如果这个值超过500000(即500毫秒),说明fork阶段把主线程堵了很久;
  3. 再查info memory,看used_memory_rss / used_memory的比值,以及bgsave期间瞬时内存峰值;
  4. 用redis-cli --bigkeys扫描是否有超大key——大key会让fork后COW复制更多内存页,加剧fork耗时。

解决思路:

  • 控制单实例内存,实践经验是不要让单实例RSS超过10GB,业务量大就拆分实例或集群;
  • 避免在写入高峰期自动触发bgsave,把save规则调低或改为定时手动执行;
  • 如果已经严重到影响业务,考虑切换AOF everysec并在低峰期重写。

5.2 AOF文件尾部损坏导致的启动失败

症状:重启Redis时日志报错Bad file format reading the append only file,实例启动失败。

原因:AOF文件尾部损坏,常见于断电、docker容器被强杀、磁盘写入期间文件被截断。Redis加载AOF时会校验每条命令的RESP格式,读到坏数据直接报错退出。

排查与修复链路:

  1. 先备份损坏文件,千万不要在原文件上直接操作:cp appendonly.aof appendonly.aof.bak;
  2. 用官方自带的redis-check-aof检查修复:redis-check-aof --fix appendonly.aof;
  3. 它会扫描文件并截断到最后一个合法命令位置,确认后生成可用的文件;
  4. 启动Redis,验证数据完整性:对比dbsize、抽查关键key;
  5. 如果数据依然不对,就用备份文件重新加载,并考虑从从节点补数据。

这个问题的关键教训是:修复工具只会保证文件语法合法,不会保证你的数据都在。它把损坏位置之后的所有内容都砍掉了,中间可能包含大量有效命令。所以修复完必须做数据抽样比对。

5.3 同时开启RDB和AOF时的优先级陷阱

我见过有人同时开了两种持久化,重启后发现数据不对,却怎么都想不明白。原因很简单:当AOF和RDB同时存在时,Redis重启优先加载AOF文件,而不是RDB。因为设计者默认AOF比RDB更新、数据更完整。

这个陷阱最坑的地方在于,如果你只是修改了RDB相关的配置,比如改了dbfilename,重启后发现数据没刷新,还以为配置不生效。其实是因为AOF文件里存的数据覆盖了RDB文件的数据。

生产实践经验:

  • 如果只想要RDB来恢复,先把AOF关掉,重启一次让AOF文件失效;
  • 如果只想要AOF,那把save配置全部注释掉,避免RDB持续生成干扰认知;
  • 不要同时维护两份文件还指望它们保持一致,除非你理解混合持久化模式。

5.4 flushall误操作之后,如何抢救

这是运维事故里最容易让人手足无措的场景。Redis重启后并不会因为数据被清空而报错,你只能靠持久化文件来恢复。

假如你执行了flushall,并且启用了AOF:

  • 立刻找到当前实例使用的AOF文件,复制一份出来;
  • 用redis-check-aof检查是否为有效RESP命令流;
  • 把AOF文件末尾的SELECT、FLUSHALL相关命令删掉(可以用文本编辑工具,但要小心文件结构),再把文件放到一个临时Redis实例加载,也可以直接把处理过的文件放回生产实例重启。

如果只有RDB:

  • 立即找到最近一份有效dump.rdb文件,停止实例,用这份文件替换当前目录文件后重启;
  • 如果不知道哪份是有效的,先备份现场,再用rdb文件加载到一个隔离实例里验证内容。

这类事故最有效的防线其实是事前配置:把flushall、flushdb、key *这类高危命令通过rename-command重命名,或者给应用方只分配无flushall权限的用户。我后来对所有生产实例一律执行了命令禁用,再也不赌人性。

5.5 关闭持久化的"裸奔"模式,什么时候能忍

很多团队因为业务是纯缓存,直接把RDB和AOF全部关掉,觉得丢了数据也无所谓。这本身没有问题,但有几个前提值得确认:

  • 上游数据库或消息队列里有原始数据,缓存丢了可以重建;
  • 实例加入主从架构,即使主节点挂了,从节点能顶上,全量同步重建数据;
  • 你对"丢失所有缓存数据"这件事有明确的业务预案,而不是等到线上事故了才讨论。

最危险的情况是:自认为纯缓存无所谓,结果后面有人把业务关键数据放进去,或者主从架构没有正确配置,主节点挂了连个替补的都没有。所以每次关掉持久化之前,我都会问一句:这个Redis如果今天数据全没了,业务还能不能正常走完一天?

6. 我的选型建议:混合持久化如何"既要又要"

6.1 混合持久化:RDB做骨架,AOF做肉

Redis 4.0之后提供了一种混合持久化方案,配置项是aof-use-rdb-preamble yes。开启后,AOF文件不再全是命令文本,而是开头先写入一个RDB格式的全量快照,后面的增量部分继续用AOF命令格式追加。

加载时Redis优先读取AOF文件头部的RDB段,直接把全量数据反序列化进内存,然后接着重放后面的增量命令。这样:

  • 恢复速度快,接近纯RDB的水平(不用逐条重放全量历史命令);
  • 数据丢失窗口小,增量部分仍然按appendfsync策略刷盘。

这是我目前最推荐的生产配置。它不是银弹,但确实把RDB和AOF的互补性发挥了出来。

6.2 三类业务场景的推荐配置

场景一:纯缓存、数据可重建

save "" appendonly no

最多加一个低频RDB用于缓存预热。

场景二:一般业务缓存,可容忍秒级丢失

save 3600 1 appendonly yes appendfsync everysec aof-use-rdb-preamble yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

既有RDB做快速恢复,又有AOF兜底秒级数据增长。

场景三:金融、订单、强一致场景

save 3600 1 appendonly yes appendfsync always aof-use-rdb-preamble yes

always是官方唯一能做到逐命令落盘的策略,代价是写性能明显下降,必须通过压测确认该实例的写QPS天花板。

6.3 监控、备份和恢复演练的日常操作

配置再好,没有日常巡检也是白搭。我日常做的几件事:

# 查看持久化状态 redis-cli info persistence # 手动RDB快照,随后把rdb文件异地备份 redis-cli bgsave # 手动触发AOF重写,控制文件体积 redis-cli bgrewriteaof

备份策略上,我习惯每天凌晨低峰期执行一次bgsave,把dump.rdb传到独立存储保留最近7天;AOF文件则由Redis自动重写机制控制,不额外处理。每季度至少做一次全流程恢复演练:拿一台空闲机器,导入最新的持久化文件,启动实例,记录从备份文件就绪到数据完整可服务的时间。这个时间就是你真正遭遇故障时的期望恢复时长。

另外强烈建议在压测环境就验证好持久化相关参数。我见过不少团队生产环境上线后才发现stop-writes-on-bgsave-error把写入全部拒绝了,原因只是磁盘用满。压测阶段把磁盘写满、模拟断电、模拟进程kill,能暴露很多文档里不会写的真实行为。

以我个人的经验来说,Redis持久化的价值不在于"出事的时候能救命",而在于"你提前想好了各种故障场景下到底要付出多少数据代价"。RDB和AOF不是竞争对手,它们是同一枚硬币的两面:一个让你恢复得快,一个让你丢得少。生产上别二选一,混合持久化加周期性备份,再配上一条从节点和定时恢复演练,这套组合拳打下来,Redis才真正算得上是一个让人睡得着觉的存储组件。最后分享一个小建议:不管选哪种方案,都要把info persistence里各个字段的含意吃透,至少做到看一眼就知道当前实例处于什么保护状态,这比背十条监控规则都管用。

返回列表