如果你在某天早上打开监控,发现Redis的used_memory从5GB跌到了几十MB,那不用怀疑,这台实例大概率在夜里经历过一次“失忆”——进程被重启或者直接崩溃了。内存数据库天生就是这副脾气:数据跑得飞快,断电就清零。Redis持久化解决的就是这个“失忆”问题,它提供了两条主路径:RDB和AOF。RDB是把内存数据定期拍成一张“快照”存到磁盘,AOF则是把每次写命令追加到日志文件。这两条路径各有脾气,也各有用途,今天我就从原理、配置、实操和踩坑四个角度把它们彻底讲透。
1. 先从“为什么”说起:Redis持久化的使命
1.1 内存数据库的软肋:进程一退,数据归零
Redis的核心数据都存在内存里,这是它能做到微秒级读写的原因,但也是它最脆弱的地方。内存不具备持久性,进程退出、系统断电、内核panic,都会让RAM里的数据瞬间蒸发。Redis作为缓存时,数据丢了还能接受,顶多去后端数据库重新拉一份回来;可一旦你把它用于会话管理、消息队列、计数器或者订单状态这类业务数据,丢失就意味着线上事故,轻则用户被踢下线,重则账目对不上。
我见过很多团队一开始只用Redis做纯缓存,后来慢慢把越来越多的数据放了进去,却忘了配置持久化。直到某次机房断电,几十个实例重启后数据全部归零,才意识到问题的严重性。所以说,持久化不是“可选项”,而是Redis在生产环境中的一条底线。
1.2 持久化的两种形态:快照 vs 追加日志
Redis官方给了两种核心持久化方案,设计哲学截然不同。
RDB(Redis Database File)是“快照”思路:在某个时间点,把内存中的全量数据序列化成一个二进制文件,存到磁盘。恢复的时候,一次性加载这个文件就能把数据填回内存,速度很快。但它的代价是“只能记录快照生成那一刻的数据”,两次快照之间的更新,如果发生宕机,就会全部丢失。
AOF(Append Only File)是“追加日志”思路:Redis每执行一条写命令,都会把这个命令以文本协议的形式追加到AOF文件末尾。恢复的时候,只要按顺序重新执行一遍文件里的所有命令,就能还原出最终的数据状态。由于命令是逐条记录的,数据丢失窗口可以压到很小,甚至做到基本不丢。
一句话概括:RBF追求的是“保存你快照瞬间的状态”,AOF追求的是“保存你所有的操作过程”。这两者在数据可靠性、文件体积、恢复速度和性能开销上各有取舍,所以很多生产环境会把两者同时打开,让它们互相补位。
2. RDB快照:把内存“拍照”到磁盘
2.1 核心原理:fork + 写时复制
RDB的实现并不像“把内存数据写到文件”这么简单。因为Redis是单线程模型,如果直接在主进程里全量扫描并写文件,整个服务在写文件期间都会阻塞,这对线上业务是无法接受的。于是Redis选择了fork子进程的方案。
当你执行bgsave时,Redis会调用fork()创建一个子进程。fork的瞬间,子进程会复制父进程的页表,但不复制实际内存数据,父子进程暂时共享同一批物理内存页。之后,父进程继续照常服务读写请求,而子进程则在后台把内存里的数据遍历并写入临时文件。
这里最关键的是写时复制(Copy-on-Write,COW):当父进程在fork之后要修改某个内存页时,操作系统会把该页复制一份给父进程,让父进程在新副本上修改,而子进程持有的还是fork时刻的旧页。这样,子进程看到的始终是fork那一刻的完整数据集,不会出现“一边写一边变”的脏数据。代价是,若fork后父进程改动的内存页越多,COW需要复制的页就越多,会消耗额外的内存和CPU。这也是很多大实例在bgsave后出现短暂内存尖峰的原因。
2.2 触发RDB的三种方式:save、bgsave、自动触发
RDB的触发方式有三种,我分别说一下适用场景。
第一种是save命令。这是一个同步操作,Redis会在主进程里直接生成RDB文件,生成期间阻塞所有客户端请求。在几百GB的内存实例上,save可能会阻塞几十秒甚至几分钟,所以几乎只有在停机维护、需要立刻拿到一份干净快照时才用。
第二种是bgsave命令。这是推荐的日常操作,Redis会fork出一个子进程在后台生成RDB,主进程继续服务。执行后可以通过info persistence查看rdb_bgsave_in_progress,等它变成0就说明写完了。
第三种是配置自动触发。在redis.conf里,save m n表示“m秒内至少有n个key发生变化时,自动执行一次bgsave”。默认配置通常是:
save 900 1 save 300 10 save 60 10000意思是900秒内有1次变更会保存,300秒内有10次变更会保存,60秒内有10000次变更会保存。多条规则可以同时存在,满足任意一条就会触发。如果想彻底禁用RDB,可以设置save "",但一般不建议这么做。
还有一点要注意:正常执行shutdown命令时,Redis会主动触发一次RDB保存(除非加nosave参数),以便重启后能快速恢复。所以很多运维同学会发现,明明没有配置自动保存,dump.rdb文件却一直在更新,就是这个原因。
2.3 RDB文件的加载与恢复
Redis启动时,会按照配置的dbfilename和dir去查找对应的RDB文件。如果文件存在,就把它加载进内存;加载完成前,Redis不会对外提供服务。如果文件不存在,就启动一个空实例。
这里有一个容易踩的坑:如果你同时开启了AOF,Redis优先加载的是AOF文件而不是RDB。很多人在改了配置后以为RDB还在,结果重启后发现数据跟RDB不一致,就是因为AOF同样存在且优先级更高。在混合持久化模式下,加载逻辑还会更复杂,后面单独讲。
RDB文件是二进制格式,版本兼容性是一个现实问题。不同大版本的Redis生成的RDB文件可能不兼容,小版本之间通常没问题,但大版本升级时最好先做一次redis-check-rdb校验,并提前做恢复演练,不要在升级当天直接替换二进制。
2.4 RDB的优势和短板
RDB最大的优势就是“快”和“省”。文件紧凑,体积小,适合做备份文件,可以直接传到对象存储或磁盘阵列。恢复速度也是所有持久化方案中最快的,因为它是整体加载,不需要逐条执行命令。再加上bgsave不会阻塞主进程,对在线业务影响很小。
但它的短板同样明显。最要命的是数据丢失窗口大:默认配置下,极端情况可能丢失最后一次快照到宕机之间的所有写入。如果你的业务对数据完整性要求高,仅靠RDB是远远不够的。此外,fork大内存实例时的瞬时开销不可忽视,在内存紧张或高负载环境下,bgsave引起的阻塞和内存抖动可能比想象中严重。
3. AOF日志:把每个写命令“记账”下来
3.1 写入链路:命令 → 缓冲区 → 刷盘
AOF的工作流程是:客户端发送一条写命令,Redis执行完命令之后,会把这条命令以Redis序列化协议(RESP)的格式追加到内存中的AOF缓冲区(aof_buf),然后根据配置的刷盘策略,把缓冲区内容写入操作系统内核的页缓存,再通过fsync强制落盘。
很多初学者会把“写入文件”和“落盘”当成一回事,其实中间隔着一层操作系统缓存。write()只是把数据从用户态拷贝到内核态的缓冲区,如果此时机器突然断电,内核缓冲区里的数据一样会丢。只有执行了fsync(),数据才真正写入磁盘物理介质。认识到这个区别,才能理解为什么AOF的刷盘策略会直接影响数据丢失窗口。
Redis 7.0开始,AOF文件被设计成多part结构,由manifest文件管理,支持更细粒度的重写和加载。不过基本的数据流和策略并没有变,网上很多老教程讲的是单文件AOF,看的时候要注意版本差异。
3.2 三种刷盘策略:always、everysec、no
AOF刷盘策略由appendfsync参数控制,可选值有三个,我整理成一张对比表:
| 策略 | 行为 | 数据丢失风险 | 性能影响 |
|---|---|---|---|
| always | 每次执行写命令后立即fsync | 进程崩溃最多丢一条命令 | 吞吐量严重依赖磁盘性能,写入延迟高 |
| everysec | 每秒后台线程执行一次fsync | 极端情况丢失约1秒数据 | 性能均衡,生产环境最常用 |
| no | 由操作系统决定何时刷盘 | 可能丢失最后一段时间写入的数据 | 性能最好,但不可控 |
实际使用中,everysec是绝大多数生产环境的默认选择。它把数据丢失窗口控制在1秒以内,而对性能的影响远小于always。我实测过always在普通云硬盘上确实惨不忍睹,写吞吐能掉一个量级,除非你的业务对数据零丢失有硬性要求,并且磁盘是高性能SSD,否则别轻易开。
而no策略基本等于放弃AOF的数据安全意义,只有你能接受“重启丢大量数据”时才考虑。
3.3 AOF重写:防止日志无限膨胀
AOF的文件大小会随着写命令不断增长。举个简单的例子:某个key执行了10000次INCR,AOF里就会记下10000条INCR命令,但最终这个key的值可能就是10000。恢复时逐条执行这10000条命令,完全没有必要,一条SET key 10000就搞定了。AOF重写(BGREWRITEAOF)就是用来解决这个问题的。
重写的原理和bgsave类似:fork出子进程,根据当前内存中的数据集,生成一套重建当前数据所需的最小命令序列,写入一个临时AOF文件。与此同时,父进程会把重写期间收到的新的写命令记录到AOF重写缓冲区。当子进程写完临时文件后,父进程把缓冲区里的新命令追加到临时文件末尾,最后用rename原子替换旧的AOF文件。
触发方式有手动bgrewriteaof命令,也有自动配置。默认参数是auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb。意思是当前AOF文件大小比上一次重写时增长了100%,并且文件大小超过64MB,才会触发重写。把这两个值调低,会更激进地控制AOF体积,但也会引入更频繁的fork开销。
3.4 AOF加载与损坏处理
AOF文件是文本协议格式,每一行都清晰可读,比如*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n。Redis启动时如果检测到AOF开启且文件存在,就会按顺序逐条扫描命令并执行,直到文件末尾。这个重放过程比加载RDB慢得多,文件越大,启动越慢。
一旦AOF文件尾部因为异常写入被截断,Redis默认会拒绝启动,并在日志里报出类似“Bad file format reading the append only file”的错误。这时需要用官方工具redis-check-aof --fix修复。它会把损坏区域扫描出来,然后提示你是否截断损坏部分。选择同意后,工具会生成一个修复后的文件,你可以用它替换原文件再启动。注意,修复通常会丢失截断点之后的数据,所以最好先备份源文件,在副本上操作。
3.5 AOF的优点和短板
AOF的最大卖点是数据安全性高。always模式下最多丢一条命令,everysec下最多丢1秒数据,这在绝大多数业务场景里都能接受。文件可以人工查看和审计,出了问题还能用工具修补,后期可以按需重放。
短板也很明显:文件体积通常远大于RDB,恢复时要逐条执行命令,启动速度慢。另外,如果刷盘策略配置不当,always会拖垮写入性能,everysec虽然均衡,但需要对磁盘IO有基本保障。重写机制虽然能控制体积,但重写本身也消耗CPU和内存,频繁重写同样有风险。
4. RDB和AOF:怎么选、能不能一起用
4.1 两者关键指标对比
很多人在选型时纠结到底用RDB还是AOF,我建议先看下表,按自己的核心诉求做取舍:
| 维度 | RDB | AOF | 备注 |
|---|---|---|---|
| 数据丢失窗口 | 最近一次快照之后全部丢失 | always几乎不丢;everysec最多丢1秒 | 业务对丢失敏感度决定 |
| 恢复速度 | 快,直接加载二进制快照 | 慢,需要逐条重放命令 | 大实例差距明显 |
| 文件体积 | 小,紧凑 | 一般偏大,重写后仍可能比RDB大 | 影响备份和传输成本 |
| 性能开销 | bgsave期间fork和COW开销大 | always有明显写延迟;everysec影响较小 | 取决于服务器内存和磁盘 |
| 可读性 | 二进制不可读 | 文本协议可读 | 便于审计和手动恢复 |
| 备份友好度 | 高,拷贝一个文件即可 | 中,文件变化频繁,需要快照配合 | 依赖文件系统能力 |
| 修复工具 | redis-check-rdb | redis-check-aof | 两者都是官方工具 |
如果你的数据丢了可以接受从上游重新加载,那RDB完全够用;如果丢了就要出事故,AOF几乎是必须的。
4.2 混合持久化:Redis 4.0的救场设计
Redis 4.0引入了混合持久化,核心思想是“用RDB打底,用AOF补增量”。开启方式为aof-use-rdb-preamble yes。在这种模式下,AOF文件的前缀是一段RDB格式的全量快照数据,后面才是RDB快照生成之后的增量写命令。
比如某个时刻触发了AOF重写,子进程会把当前全量数据以RDB格式写入AOF临时文件,之后父进程的增量命令再以RESP格式追加到文件尾部。这样生成的AOF文件兼具两种格式:加载时先快速载入RDB部分,再重放少量增量命令,恢复速度接近RDB,数据安全性接近AOF。
Redis 5.0之后,这个开关默认就是开启的,所以现在很多人在AOF文件里看到一堆二进制乱码是正常的,别以为是文件坏了。代价是AOF不再是纯文本,失去了“人肉可读”这个优势,但工程收益远远大于损失。
4.3 直接可用的配置建议
以我个人的经验,不同场景有比较成熟的默认组合。
纯缓存场景:Redis只做缓存,数据丢了可以从数据库重建,那就完全可以把RDB和AOF都关掉,或者只保留一个低频RDB用于快速预热。省掉持久化开销,换来更高吞吐和更低的fork风险,性价比很高。
业务数据场景:建议开启AOF,appendfsync everysec,同时开启aof-use-rdb-preamble yes的混合持久化。这样数据最多丢1秒,恢复速度又不会像纯AOF那样慢。RDB可以保留,但它的角色是“每日备份”和“兜底”,不是主要的恢复手段。
内存很大的实例:比如超过20GB,每次bgsave的fork和COW开销已经不能忽视。如果业务不能接受频繁阻塞,要么把RDB触发条件调得很保守,要么干脆关掉RDB只留AOF,AOF重写虽然也fork,但通常频率比RDB自动保存低得多。实在不行就把大实例拆成多个小分片,让单实例内存控制在8GB以内。
5. 实操:从配置到恢复的一整套动作
5.1 配置文件关键项
下面是一份可以直接参考的核心配置,我加了注释,方便你对照自己的环境调整:
# AOF开关 appendonly yes appendfilename "appendonly.aof" # 刷盘策略:always / everysec / no appendfsync everysec # AOF自动重写触发条件 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 混合持久化,Redis 5.0后默认开启 aof-use-rdb-preamble yes # RDB触发条件,默认三条 save 900 1 save 300 10 save 60 10000 # RDB文件名和目录 dbfilename "dump.rdb" dir /data/redis注意dir参数非常关键,它决定了RDB和AOF文件放哪里。生产环境一定要把dir指向持久化磁盘路径,比如独立的云盘或本地数据盘,别放在随实例销毁的临时目录里。另外,stop-writes-on-bgsave-error建议保持默认yes,这样一旦RDB写失败,Redis会暂停写入,提醒你赶紧处理,而不是静静地把数据丢到下一次成功备份之后。
5.2 手动触发与验证
配置改完之后,不要等自动触发,手动做一次验证。先执行redis-cli,然后依次操作。
redis-cli bgsave bgrewriteaof info persistenceinfo persistence输出里,重点看这几个字段:
rdb_bgsave_in_progress:当前是否正在生成RDB;rdb_last_bgsave_status:上一次RDB保存结果,ok表示成功;aof_rewrite_in_progress:当前是否正在重写AOF;aof_last_bgrewrite_status:上一次AOF重写结果;aof_enabled:确认AOF是否开启。
如果状态都是ok,再看一下数据目录里文件是否有更新:
ls -lh /data/redis正常情况下,你会看到dump.rdb和appendonly.aof都出现了,且时间戳是最近的。我在每次上线部署时都会做一遍这个检查,确认持久化不是“看着开了,实际没生效”。
5.3 恢复演练:模拟宕机与数据还原
持久化配置好了,不代表万无一失。我建议至少做一次完整的恢复演练,把“数据丢失—文件损坏—手动恢复”这个过程走一遍。
先准备一个测试实例,写入一批key,执行SAVE确保RDB文件存在:
set user:1 alice set user:2 bob save停掉Redis(使用shutdown nosave模拟直接断电后的文件状态),然后把dump.rdb复制一份做备份:
redis-cli shutdown nosave cp /data/redis/dump.rdb /data/redis/dump.rdb.bak启动Redis,确认数据已经恢复。接着我们把备份文件改回去,再模拟“文件坏了”的情况:用文本编辑器或truncate破坏dump.rdb尾部,然后启动Redis,大概率会看到加载失败。这时候用redis-check-rdb dump.rdb检查,再决定是修复还是用备份覆盖。
AOF的演练类似,更常见的是模拟文件尾部截断。先确认AOF正常,执行bgrewriteaof,然后停实例,用head或dd复制一个截断后的AOF文件来测试:
redis-cli shutdown nosave cp /data/redis/appendonly.aof /data/redis/appendonly.aof.bak head -c -100 /data/redis/appendonly.aof > /tmp/appendonly_bad.aof mv /tmp/appendonly_bad.aof /data/redis/appendonly.aof启动Redis,观察启动日志,看到异常格式时,用redis-check-aof --fix修复副本,再替换回去启动。这个流程走一遍,你对AOF故障的处理就不会慌。
5.4 用官方工具修复损坏文件
当RDB或AOF文件损坏时,官方提供了两个命令行工具:redis-check-rdb和redis-check-aof。它们通常和redis-server一起安装,不用额外配置。
RDB修复比较简单,直接检查:
redis-check-rdb dump.rdb如果文件完整,会输出类似“RDB file is OK”;如果损坏,会报出错误位置。redis-check-rdb一般不会自动修复,因为RDB是二进制快照,局部损坏很难恢复,最靠谱的手段还是用备份文件。
AOF修复则更实用一些:
redis-check-aof --fix appendonly.aof修复过程中会分析文件,找到第一条损坏的命令偏移量,然后询问是否截断。选择确认后,它会保留损坏点之前的数据,丢弃之后无法解析的部分。我强烈建议先在副本上执行修复,确认结果再替换原文件,因为截断是一条不归路,操作前一定备份。
6. 我踩过的坑:常见问题与排查实录
6.1 fork阻塞与延迟毛刺
很多团队配置完RDB后,过一段时间会在监控里发现莫名的延迟尖峰,甚至出现“Redis is blocked for 200ms”的日志。这种问题十有八九出在fork上。实例内存越大,fork耗时越长,而Linux的fork需要复制页表,在内存碎片多或页表本身庞大时尤其吃力。
排查方法是看info stats里的latest_fork_usec,它记录最近一次fork消耗的微秒数。如果经常超过10万微秒(100ms),那就说明当前实例的fork代价已经不小了。应对措施有几个:调低RDB触发频率,甚至关闭RDB只依赖AOF;把实例内存降下来;升级服务器CPU和内存带宽。另外,rdb-save-incremental-fsync yes可以让子进程边生成边增量刷盘,减少对内核缓存区的冲击,也能减轻一些延迟。
6.2 大key的连环爆炸
大key是持久化场景里的“隐形炸弹”。一个几十MB的hash,如果被频繁更新,COW就会反复复制这个大对象所在的物理页。这不仅让bgsave/AOF重写变慢,还可能导致内存急剧上涨,最终触发OOM。
排查大key可以用redis-cli --bigkeys扫描,虽然会阻塞主线程,但好在扫描是分批的,危险可控。找到大key后,能拆分的拆分,不能拆分的考虑改数据结构。删除时千万别用DEL,它会在主线程同步删除并释放内存,瞬时阻塞;用UNLINK让它异步回收。这个细节在你已经处于高负载时会起决定性作用。
6.3 磁盘写满与写入保护
持久化本质上是写磁盘的活,所以磁盘空间不足是迟早会遇到的问题。RDB保存失败或AOF追加失败时,日志里会看到“No space left on device”之类的错误。如果你保留了stop-writes-on-bgsave-error yes,后续写命令直接报错,很多业务会误以为是Redis本身出问题,实际上只是磁盘被写满了。
处理顺序应该是:先确认磁盘占用量,清理日志和临时文件;如果腾不出空间,临时放宽写入限制;最后再扩容持久化目录,并把持久化文件挪到独立磁盘。注意不要长期关闭stop-writes-on-bgsave-error,否则RDB失败后Redis还会继续写,数据丢失窗口会被无限拉大。
6.4 AOF损坏后的紧急恢复
AOF损坏最常见的是文件尾部截断,通常发生在机器突然掉电时。启动时日志提示“bad file format”,不少人第一反应是直接删掉AOF文件重启,这在很多情况下能启动,但数据就全没了。
我踩过一次坑后总结了一套固定流程:先复制一份损坏文件,再用redis-check-aof --fix修复副本,修复完再放到数据目录启动。如果修复后依然启动失败,就把RDB备份恢复上去,或者把AOF文件的前半部分手动清掉,只留一个空AOF,让它从RDB全量同步兜底。Redis 7.0之后拆分了多个AOF文件,如果appendonlydir目录里的manifest文件损坏,修复会更麻烦,但基本原则不变:先备份,再修复,永远不要在生产文件上直接操作。
6.5 持久化与主从复制配合的注意事项
我最初做主从架构时,把从库的持久化整个关掉了,想着反正有主库在,从库重启后跟着主库全量同步就行。结果主库在高峰期做bgsave时CPU飙升,从库又因为网络抖动触发重连,重新全量同步,主库压力瞬间翻倍,线上延迟一路飙红。
后来我的做法是:从库也开启RDB,至少让从库在自身重启时能快速地从本地RDB恢复,减少不必要的全量同步;同时主库保持AOF开启,保证极端情况下的数据安全。另外要注意,主从切换后,新主库的持久化配置必须第一时间检查,很多人只在主库配置了AOF,切换后才发现新主库的appendonly是no,数据全裸奔。
7. 一点个人实战体会
最后说说我自己的经验。如果是业务型Redis,我的默认组合是:appendfsync everysec+ 混合持久化 + 每日定时bgsave备份。这个组合在数据安全和性能之间最均衡,实测下来我们的峰值写入延迟没有明显恶化。而纯缓存实例,我会把持久化全部关掉,靠主从复制和云盘快照来兜底,因为缓存场景为了那点恢复能力承担fork开销不值当。
还有两个小习惯想分享:一是每次上线前检查info persistence,确认rdb_last_bgsave_status和aof_last_bgrewrite_status都是ok,别让配置在加载时被静默忽略;二是给持久化目录单独挂盘,和业务日志分开,这样即使日志写满盘,也不会拖累RDB和AOF的写入。持久化这件事,平时安静得像不存在一样,但真正出事的时候,你之前的每一次配置和演练都会变成你的底牌。