Redis 开了持久化,是不是就不会丢数据了?说实话,我刚入行那年也是这么想的,结果某次线上 Redis 因为服务器内存压力太大直接被系统杀掉,重启之后发现缓存穿透把底库打崩溃了,一查数据全没了——因为当时用的 Redis 还带着默认配置,默认的 RDB 快照策略在那种场景下根本没触发。那次事故之后,我就专门把 Redis 持久化这一套彻底研究了一遍,踩了很多坑,才搞明白快照文件、追加日志、刷盘策略、重写机制到底是怎么配合工作的。
这篇文章不是把 Redis 官方文档再抄一遍,我想以实际操作的视角,把 Redis 持久化的底层原理、配置参数、选型思路和排查技巧完整拆开来讲。不管你是刚接触 Redis 的新人,还是已经在生产环境维护 Redis 的工程师,看完之后应该都能对持久化有一个系统的理解,知道什么场景该开哪个方案,出了问题怎么定位。
1. 为什么 Redis 需要持久化:先搞懂这个问题想解决什么
Redis 的本质是内存数据库,内存的特点是读写极快,但断电即失。如果 Redis 只用来做缓存,数据丢了可以从上游数据库重新加载,问题不大。但如果你把 Redis 当数据库用,存了用户会话、订单状态、排行榜、地理位置这类业务数据,丢一次数据就是事故。持久化要解决的核心问题就是:在 Redis 进程退出、服务器重启、甚至宕机之后,把内存里的数据重新恢复出来,让数据不丢或者少丢。
持久化的本质其实很简单:把内存中的数据以某种格式保存到磁盘上,下次启动时再从磁盘加载回内存。但难点在于,Redis 是一个高性能的内存系统,它不能为了持久化把性能拖垮,所以衍生出了两种核心方案:
- RDB(Redis DataBase):把某个时刻的全量数据做一次二进制快照,存成
.rdb文件。 - AOF(Append Only File):把每次写操作以日志形式追加到
.aof文件里,恢复时逐条重放这些写命令。
两个方案的取舍本质上是在"性能"和"数据安全"之间做平衡。RDB 方式更接近全量备份,恢复速度极快,但两次快照之间丢失的数据窗口很大;AOF 方式记录粒度细,最多丢几秒甚至不丢数据,但日志文件会持续增长,恢复时需要逐条回放,速度相对慢,而且频繁刷盘对性能也有影响。
这里有一个新手很容易忽略的前提:持久化不等于高可用。持久化解决的是"重启不丢数据",而高可用比如哨兵模式、集群模式解决的是"节点故障后自动切换主从"。两者是不同层级的事情,但在实际架构里经常一起出现。我见过有团队觉得配了哨兵就不用开持久化了,结果主节点挂了,从节点顶上,但从节点一重启数据也丢了,最后还是伤到了业务,因为从节点的持久化也没开。
2. RDB 快照持久化:原理、配置与实操
2.1 RDB 到底是怎么把数据存下来的
RDB 的核心工作是"把当前内存中的全量数据生成一份紧凑的二进制文件"。这一步最理想的情况是不阻塞 Redis 主进程,于是 Redis 选择了一种非常聪明的做法:调用 fork() 创建子进程,由子进程负责把数据写入临时文件,父进程继续处理客户端请求。
很多人第一次接触 RDB 原理时最困惑的是"子进程怎么保证拿到的是某一时刻的一致性快照,而不是写到一半数据变了"。答案在于操作系统提供的"写时复制"机制(Copy On Write,COW)。子进程 fork 出来之后,和父进程共享同一份内存页。子进程读取这些共享页时,只要父进程不修改它们,子进程看到的就是 fork 时刻的数据状态。一旦父进程收到写请求需要修改某个共享页,操作系统就会把这一页拷贝一份副本给父进程来改,而子进程看到的仍然是原来那页没改过的数据。所以子进程遍历内存做快照的时间点,虽然不可能做到严格意义上的一瞬间,但逻辑上能拿到一致性的数据视图。
RDB 的触发方式不只是配置文件里那条save规则,一共有几种常用路径:
save 3600 1 300 100 60 10000:如果满足"3600 秒内至少 1 次 key 变化,或 300 秒内至少 100 次变化,或 60 秒内至少 10000 次变化"之一,就自动触发 BGSAVE(后台保存)。- 手动执行
BGSAVE:异步生成快照。 - 手动执行
SAVE:同步生成快照,期间会阻塞 Redis,一般只用于极端维护场景。 - 执行
SHUTDOWN、收到系统SIGTERM信号时,如果开启了 RDB,Redis 会在退出前尝试生成最后的快照。 - 主从复制首次全量同步时,主节点如果有需要,也会自动生成 RDB 发给从节点。
这个"变化次数 + 时间窗口"的设计,本质上是把"数据写入量"作为触发条件。如果你的业务数据规模很大、写入量密集,快照触发频率就会变高,磁盘 IO 压力随之增加;反之,低频写入场景可能很久才触发一次快照。配置时不能照搬模板,要结合业务实际写入量来调。
2.2 RDB 配置参数与实操配法
RDB 相关的配置主要在redis.conf文件中,我来逐条拆解。
# 快照触发条件,可以配置多条,满足任意一条就会触发 save 3600 1 save 300 100 save 60 10000 # 生成快照文件名 dbfilename dump.rdb # 快照文件保存目录 dir /var/lib/redis # 生成失败后是否拒绝写入 stop-writes-on-bgsave-error yes # 是否压缩 RDB 文件 rdbcompression yes # 是否对 RDB 文件做 CRC64 校验 rdbchecksum yes我建议的实际配置思路是这样的。如果 Redis 只作为缓存使用,数据允许在重启后重建,那可以干脆关掉 RDB 和 AOF,或者只保留一个频率很低的 RDB,比如save 900 1。如果 Redis 承载了不能丢的核心数据,那就必须开启持久化,而且 AOF 通常是比 RDB 更可靠的选择,RDB 更多负责"快速恢复"。
stop-writes-on-bgsave-error这个参数值得单独说一下,绝大多数人容易忽略。默认值是 yes,含义是:如果快照生成失败(比如磁盘满了、权限不对、IO 错误),Redis 会停止接受所有写命令,返回错误给客户端。这个设计本意是"既然你要求持久化,那就别在持久化失败时继续写入,否则数据从根上就没有保证"。但从业务视角看,这个策略有时候很吓人,因为你的缓存服务可能只是因为临时磁盘抖动就拒绝写入,导致大量异常告警。如果你的业务能接受"持久化失败时丢掉这段时间的数据,但服务继续运行",可以把这项设成 no。
还有一个细节是 RDB 的默认文件名dump.rdb。多实例部署在同一台机器时,务必给每个实例设置独立的dbfilename,比如dump-6379.rdb、dump-6380.rdb,否则两个实例往同一个目录写同一个文件,轻则互相覆盖,重则文件损坏。
2.3 RDB 踩坑记录:数据窗口、大 Key 与阻塞
RDB 最典型的短板就是数据丢失窗口。想象一个业务场景:每天凌晨 2 点触发了一次快照,结果凌晨 2 点 42 分服务器断电,那 42 分钟内所有写入全部丢失。这就是 RDB 的特性,在配置策略上只能缩短触发间隔,无法真正消除窗口。
另一个容易被低估的问题是 fork 的成本。Redis 执行 BGSAVE 时虽然主进程不阻塞,但 fork 本身需要复制页表,实例内存大到 10GB 以上时,fork 耗时可能达到数百毫秒甚至秒级。我在某项目中就遇到过 Redis 内存 18GB、写入量极大,每次自动 BGSAVE 触发后,Redis 的延迟从正常的 1ms 以内飙到 200ms 上下,客户端大批超时。后来排查下来发现,就是 fork 瞬间页面表复制导致 CPU 毛刺,加上写时复制期间写量越大,父进程需要复制的内存页越多,毛刺越明显。
还有一个很容易被忽略的问题:大 key。Redis 生成 RDB 时是序列化整个内存里的数据,如果某个 key 是几百 MB 的 Hash 或 ZSet,这个 key 会被整体序列化写入,耗时会指数级上升,同时大 key 可能在 fork 后的写时复制过程中被父进程频繁访问,进一步放大内存页复制的开销。所以做 RDB 持久化之前,最好先排查一下有没有大 key。可以用redis-cli --bigkeys做一次扫描,把大 key 拆掉或迁走,能避免大量潜在的故障。
3. AOF 追加日志持久化:原理、配置与实操
3.1 AOF 为什么能比 RDB 少丢数据
AOF 的思路和 RDB 完全不同。RDB 是"定期给数据拍一张快照",AOF 则是"每次写操作都记录一条日志,恢复时按日志重放"。Redis 会把每个导致数据变更的命令(SET、LPUSH、SADD、INCR、DEL 等)以协议格式追加到aof_buf缓冲区,然后根据你配置的刷盘策略决定何时把缓冲区内容写到磁盘。
这里必须分清两个层级的概念:写入操作系统缓冲区与刷盘。Redis 把命令追加到缓冲区之后,调用write()将数据提交给操作系统,操作系统通常会先放到内存页缓存里,稍后找机会落盘。如果这时候断电,写到页缓存的这部分数据依然会丢。Redis 真正能控制的是fsync()的调用时机,它会强制把页缓存里的数据刷到磁盘物理存储上。
AOF 提供了三个刷盘策略,分别代表三种取舍:
appendfsync always:每次发生写命令,都会执行 fsync,理论上最多丢一个命令的数据,数据安全性最高,但性能开销也最大。实测在普通机械盘和低端 SSD 上,吞吐可能直接拦腰斩断。appendfsync everysec:每秒执行一次 fsync,最多丢 1 秒的写入数据,这是绝大多数生产环境使用的默认策略,性能和可靠性平衡得比较好。appendfsync no:不主动执行 fsync,完全由操作系统决定何时刷新,性能最好,但宕机时可能丢几十秒甚至更长时间的数据。
我个人的实践倾向是:线上业务默认用 everysec。always 除非是那种"一分钱都不能错"的账户类或者订单状态强一致的场景,否则真的没必要承担那份性能损耗;no 基本不推荐,除非你确定自己对数据丢失无所谓。
还有个机制叫 AOF 重写(BGREWRITE AOF)。AOF 文件会一直追加,时间长了文件膨胀到几个 GB 甚至几十 GB,恢复时逐条回放要花很长时间。重写的思路是:fork 一个子进程,读取当前内存里的完整数据,用最小命令集重新生成一个新的 AOF 文件。比如某个 key 被 SET 了 100 次,旧文件里可能有 100 条命令,重写后只会剩下最终状态那一条。重写期间新的写命令会同时写入旧的aof_rewrite_buf缓冲区,等子进程重写完成后,再把缓冲区的增量命令追加到新文件末尾,最后原子替换旧文件。整个过程父进程不阻塞,只是同样有 fork 开销。
3.2 AOF 配置参数详解与开启步骤
AOF 配置可以说非常少,但每个参数都直接决定数据安全等级。
# 开启 AOF appendonly yes # 刷盘策略 appendfsync everysec # 重写触发条件,相对于上次重写后的文件大小,增长多少百分比时触发 auto-aof-rewrite-percentage 100 # 重写触发的最小文件大小,低于这个值不会触发重写 auto-aof-rewrite-min-size 64mb # 重写后是否直接使用新的 AOF 文件,默认使用临时文件再 rename aof-use-rdb-preamble yes开启 AOF 的具体操作不难:修改配置后重启 Redis 实例,或者直接在redis-cli里执行CONFIG SET appendonly yes动态开启。如果是生产环境,我建议优先用改配置文件方式,因为动态修改只是在内存里生效,一旦重启就恢复原样,如果你忘了同步配置文件,重启后 AOF 悄然关闭,那才是大坑。
还有一件事要特别提醒:AOF 开启后,Redis 在启动时会优先使用 AOF 文件恢复数据。原因很简单,AOF 保存的数据通常比 RDB 更新、更完整。如果你同时开了 RDB 和 AOF,Redis 不会傻乎乎地去读 RDB,而是直接读 AOF。这解释了为什么很多人做了 RDB 快照之后,重启却发现数据不是快照那一刻的状态——因为 AOF 里的数据更新,系统默认选择更全的数据来恢复。
AOF 文件损坏也是一个需要提前心理准备的故障点。断电、磁盘故障、人为误改文件,都可能导致 AOF 文件结构与协议格式对不上,Redis 启动时直接拒绝加载并报错。这时候不要慌,Redis 默认会尝试用redis-check-aof工具修复文件,但更稳妥的流程是:先备份坏掉的 AOF 文件,再用工具修复,修复后如果数据能接受再加载;修不起来就只能退回到 RDB 文件恢复,那就得承受 RDB 窗口内的数据丢失代价。我在实际故障中踩过一次,当时 AOF 文件损坏,修复工具直接丢掉了文件尾部大段合法日志,因为那个文件本来就有大量重复命令,我后来把修复后的 aof 重放,发现至少丢了 1 万条有效命令对应的最终状态,最后只能从早上的 RDB 快照恢复,白白丢了半天数据。自此之后,我就养成了备份 AOF 文件的习惯,定时把 rdb 和 aof 都远程做冷备,而不是只看本机存了没。
4. 混合持久化与生产环境选型思路
4.1 混合持久化:把 RDB 和 AOF 的优点尽量都拿到
RDB 恢复快但丢数据多,AOF 丢数据少但恢复慢,这就像既想要鱼又想要熊掌。Redis 在 4.0 之后引入了混合持久化,用aof-use-rdb-preamble yes开启。它的原理是:AOF 文件在重写时,不再是纯文本命令日志,而是在文件头部先写一段 RDB 格式的二进制全量快照,快照之后追加重写期间产生的增量写命令。这样的文件加载速度接近 RDB,数据安全性又接近 AOF。
这个方案的覆盖面很广。Redis 能从 2.6 时代坚持到 4.0 以上版本,我自己从 4.0 和 5.0 两个版本的实践看,混合持久化基本就是生产环境的标配了。唯一要注意的是,只有同时开启 AOF 且重写动作发生后的 AOF 文件才是混合格式;如果开启混合持久化之前生成的旧 AOF 文件,那还是纯文本命令日志,Redist 加载时都能识别,这不冲突。
关于加载顺序,Redis 启动时如果开启了 AOF,会优先加载 AOF 文件。如果刚好这个 AOF 是混合格式文件,它的前导部分是 RDB 二进制段,Redis 会把这段解出来快速载入,然后继续重放后面的增量命令。这个流程看起来复杂,但对运维来说无需手动干预,放心用即可。
4.2 不同业务场景下该怎么选持久化方案
持久化方案的选型,我最常建议用一个非常朴素的判断标准:如果这台 Redis 挂了,数据丢了,业务能不能接受?
第一种场景:Redis 只做纯缓存,数据全部可以从数据库重建或由外部系统回填。这种情况连持久化都可以不开,最多开一个低频率 RDB 帮助加速重启后热缓存预热,AOF 完全没必要开。开了不仅白白增加磁盘 IO,还拖慢写入性能,纯浪费。
第二种场景:Redis 当数据库或状态库用,比如排行榜、计数器、会话状态、任务队列。这类数据不能丢,必须开 AOF 加混合持久化,刷盘策略选 everysec。同时再配合一个较密度的 RDB 快照,方便在极端情况下快速恢复和冷备。
第三种场景:内存里保存强一致、关键交易链路的数据。这种我不建议只用 Redis 默认的持久化,最好再加一层方案,比如定期全量导出发送给外部存储服务,或者开启主从复制配合持久化,主从都落盘,减少单点风险。不管 Redis 持久化做得多完善,都不能把背锅希望全押在一个进程上,任何内存系统的磁盘持久化都有薄弱环节,比如 AOF 的 always 策略也不能完全避免操作系统刷盘边界上的极端丢数据情况。
选型的时候还可以参考恢复时间这个指标。RDB 加载 10GB 文件可能在几十秒内搞定,但同样的数据量如果变成上百 GB 的纯 AOF 日志,重放时间可能是几分钟甚至几十分钟。生产环境的恢复速度直接决定 RTO(恢复时间目标),我见过一个团队因为 AOF 文件过大,重启恢复花了 40 多分钟,业务停摆时间完全不可接受,后来改成混合持久化,恢复时间缩到分钟级,同时利用订阅发布机制做了双读补偿,效果立刻不同。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 问题 | 原因 | 排查与解决 |
|---|---|---|
| 配置了 save,但 RDB 文件一直不变 | 触发条件不满足,比如写入量没有达到变化次数阈值 | 用info persistence查看 rdb_last_bgsave_status、rdb_changes_since_last_save,或手动BGSAVE验证 |
| 重启后数据没有恢复,或恢复的不是最新数据 | 同时开启了 AOF,Redis 默认加载 AOF,RDB 被忽略 | 确认 appendonly 配置,检查 AOF 文件是否完整,必要时在停机状态下删除或隔离旧 AOF |
| AOF 文件越来越大 | 没有触发重写,或触发阈值设置过高 | 调低 auto-aof-rewrite-percentage 和 min-size,手动BGREWRITEAOF |
| 磁盘满导致写不进去 | 磁盘 IO 或容量问题 | 及时清理旧备份,迁移 Redis 数据目录到更大分区,用独立挂载点分离数据与日志 |
| fork 阻塞导致延迟毛刺 | 实例内存大,fork 复制页表耗时 | 限制实例内存足迹,拆分为多实例,降低 fork 频率,避免与高峰流量重叠 |
| 启动时报 AOF 文件损坏 | 断电、磁盘坏道、人为改文件 | 备份 bad aof,用redis-check-aof修复,修复不了从 RDB 冷备恢复 |
| 设置 appendonly yes 后没生效 | 动态修改配置后没同步到持久化文件 | 修改 redis.conf 并确认 CONFIG REWRITE,或重启验证 |
5.2 一些实践心得与避坑经验
我处理 Redis 持久化问题的经验里,有几点很少能在官方文档里看到,但价值非常高。
第一,监控是持久化方案的半条命。开启持久化之后,一定要把info persistence输出里的关键字段接入监控告警:rdb_last_bgsave_status、aof_last_bgrewrite_status、aof_last_write_status、aof_current_size。这些字段分别能第一时间告诉我们 BGSAVE 是否成功、AOF 重写是否正常、最新一次写盘是否失败。持久化最怕的不是坏,而是坏了你不知道,还继续往实例里写核心数据。
第二,生产环境磁盘尽量独立。Redis 实例的数据目录最好别和系统盘共用,尤其别和日志盘或者其他高 IO 应用共用。AOF 刷盘策略 everysec 虽然每秒只有一次,但一旦触发重写,临时文件 + 旧文件 + 新文件同时存在,瞬间磁盘占用可能翻倍,如果磁盘容量本来就只有 20GB,而 Redis 内存 10GB,重写时很可能直接撑爆磁盘。建议预留数据文件 3 倍左右的空间余量,才能比较从容。
第三,持久化文件不要只依赖本机。本机磁盘如果整体物理损坏,那所有 RDB 和 AOF 都跟着没了。强烈推荐把 dump.rdb 和 appendonly.aof 定时通过定期复制工具送到对象存储或备份服务器,频率可以不强求,一天一次或几小时一次都行,但这决定了你能不能从最恶劣的物理故障中活下来。
第四,关闭 Redis 时尽量走安全措辞。用SHUTDOWN而不是直接kill -9,这样 Redis 有机会做一次可控的数据落盘。虽然 AOF 每秒刷盘能兜底到最后 1 秒的写入,但 RDB 模式下 SHUTDOWN 触发最后的快照,可以显著缩短丢失窗口。这个操作习惯成本极低,回报却很直观。
最后,再分享一个小技巧。排查持久化问题的时候,用DEBUG OBJECT key去看大 key,用INFO keyspace看整体 key 规模,用redis-cli --rdb手动生成快照,这些工具能帮你快速判断是数据规模导致的问题还是配置不当导致的问题。线上一旦出现持久化相关告警,第一时间抓info persistence,第二时间看磁盘df -h和iostat,第三时间看 Redis 日志文件,大多数问题都能在这个顺序里找到答案。