Redis持久化实战:RDB、AOF与混合持久化原理及故障排查
上周半夜被同事电话叫醒说线上Redis重启之后内存里的数据全没了日志里也没有报错。我第一反应是让他看dir目录下有没有dump.rdb文件结果文件存在但修改时间是三天前。也就是说Redis一直在正常对外服务但持久化实际上已经悄悄停摆——这类问题我在排查过不少次之后决定把Redis持久化策略这个老话题重新梳理一遍包括原理、参数、选型和那些只有踩过坑才知道的细节。无论是刚学Redis的新人还是已经上线了但没细看过配置文件的老手这篇都值得花十分钟过一遍。Redis持久化最常见的两大方案就是RDB快照和AOF追加日志。很多人把它们当成二选一的关系其实从Redis 4.0之后官方主推的是两者配合的混合模式。这篇文章我会把两种机制各自的原理、关键参数、失效场景和线上实测数据都拆开讲透最后再给一套可以直接照着用的选型思路。1. 先搞清楚Redis持久化到底在防哪种丢数据很多人对持久化的理解就是重启后数据还在这个说法没错但不完整。Redis是内存数据库它的性能优势建立在全量数据都活在内存里这个前提上但内存是易失的进程退出、机器重启、断电、OOM被杀任何一个意外都可能让内存里的数据瞬间归零。持久化的本质就是给内存数据找一个落盘副本让Redis具备灾难恢复能力。1.1 一个让我意识到问题的线上事故那次线上事故其实不是因为没开持久化而是开了RDB但没理解它的触发逻辑。默认配置下Redis只在满足save 900 1、save 300 10、save 60 10000这三个条件之一时才会自动生成快照。同事的实例是一个高写入但key总量变化不大的场景——每秒写入量很大但大多数是更新已有key新增key达不到10000个的阈值结果快照就少了很多三天没落盘重启自然数据回滚。从那之后我养成了一个习惯每次排查数据丢失问题第一件事不是看配置而是先看INFO persistence输出。这个命令会告诉你最后一次RDB成功的时间、RDB失败次数、AOF重写状态等关键信息比翻日志快得多。另外建议监控系统里加上对rdb_last_bgsave_status和rdb_last_save_time的告警很多隐患能在事故前就暴露。1.2 持久化要解决的三种故障场景不同场景对持久化的要求差别很大先把故障分级才能理解为什么Redis提供了多种策略组合。进程被杀kill、OOMRedis进程异常退出但操作系统还活着。这种场景下内存数据全部丢失但磁盘文件完好恢复依赖于最近的RDB快照或AOF日志。机器断电/重启比进程被杀更彻底除了数据丢失还要考虑磁盘本身的完整性。如果AOF正在写入时断电可能出现文件尾部截断Redis需要能识别并容忍这种半截写入。磁盘损坏/误删这是最严重的场景单节点的持久化文件可能整体不可用。所以持久化永远不能替代备份RDB文件定时拷贝到其他机器是必须的后面我会专门讲。1.3 先记住结论RDB管快照AOF管日志两种方案本质上解决的是不同粒度的问题。RDB是某一时刻的全量数据二进制快照恢复速度快、文件紧凑但两次快照之间的数据一定会丢。AOF是每一条写命令的追加日志理论上可以把数据丢失窗口从小时级压缩到秒级甚至零丢失代价是文件大、恢复慢、写入有一定的性能开销。理解了这两个方案的边界很多所谓的最佳实践就不用死记了。比如为什么默认配置下数据会丢因为RDB默认策略就是可接受分钟级甚至更长时间的数据丢失来换性能。而为什么金融类场景必须开AOF因为丢失一笔交易记录的代价远大于多花那几毫秒的写入耗时。持久化策略没有银弹本质上就是在性能、文件大小、恢复速度和数据可靠性之间来回权衡。2. RDB快照机制dump.rdb背后的fork与写时复制RDB方案的核心产物就是那个dump.rdb文件。它记录的是某个确切时间点Redis里的全量数据格式是紧凑的二进制加载速度比AOF快一个数量级。很多初学Redis的人以为RDB就是定期把内存数据拷一份到磁盘这个理解太粗糙了——如果真这么干几GB的数据在拷贝期间Redis还能不能正常服务显然不能。所以RDB内部的设计远比拷贝两个字复杂。2.1 RDB的触发方式不止自动save这一条路RDB快照的触发总共有四种方式很多人只知道前两种手动执行BGSAVE后台异步生成快照不阻塞主进程这是生产环境最常用的手动方式。手动执行SAVE同步生成快照会阻塞所有客户端请求只适合数据量极小或维护窗口期使用。我见过有人误在线上执行SAVE导致几秒卡顿这个命令基本可以当成禁用列表看待。满足配置的自动触发由save指令设定的条件决定比如默认的save 3600 1表示3600秒内至少有1次key变化就触发一次快照。Redis正常关闭时SHUTDOWN默认会执行一次SAVE后再退出。但如果是SHUTDOWN NOSAVE或者异常崩溃就不会生成新的RDB文件。自动触发的判断逻辑很多资料里写得含糊这里补充清楚Redis内部有一个dirty计数器每次数据变更都会累加然后serverCron每100ms检查一次只要当前时间距上次成功快照超过save配置的秒数、且dirty次数达到配置的变更数就触发一次BGSAVE。所以save 900 1的意思是900秒内发生至少1次写入而不是每900秒存一次。2.2 BGSAVE的秘密fork子进程与写时复制BGSAVE走的是fork()系统调用创建一个与主进程完全相同的子进程然后子进程负责把内存里的数据写入临时RDB文件写完后再用rename原子替换旧的dump.rdb。父进程在这期间继续正常处理请求全程不被阻塞。这里最关键的就是写时复制Copy-On-Write, COW。fork出来的子进程和父进程共享同一份物理内存页最初不需要拷贝任何数据。但是只要父进程在执行写命令时修改了某个内存页内核就会把这一页复制一份给父进程使用子进程看到的仍然是fork那一刻的旧数据。所以RDB捕捉的是fork瞬间的一致快照而不是开始写文件的瞬间。这个机制带来的一个隐蔽代价是快照期间的大量写入会让物理内存消耗翻倍增长。因为每修改一个内存页就多占一份内存极端情况下一个4GB的实例在BGSAVE期间可能实际占用7-8GB物理内存。如果机器内存偏紧这就会触发OOM。这也是为什么很多大实例把自动RDB关掉手动在低峰期执行BGSAVE。2.3 这些RDB参数不看会吃亏# redis.conf 中 RDB 相关核心配置 save 3600 1 save 300 100 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb dir /var/lib/redisstop-writes-on-bgsave-error yes这个参数值得单独拎出来讲。默认情况下如果BGSAVE因为磁盘已满或权限问题失败Redis会拒绝所有写请求只保留读能力。这个设计的初衷是既然持久化已经得不到保障宁可拒写也不能让数据悄悄丢,但对很多业务来说这等于磁盘故障引发了服务雪崩直接把写流量全打挂了。我个人的建议是如果Redis只当缓存、接受丢失把它设为no如果Redis当数据库、数据不能丢保持yes并且把失败告警拉响。rdbcompression默认开启用LZF算法压缩字符串值能省不少磁盘空间代价是BGSAVE时多消耗一些CPU。如果瓶颈在CPU可以关掉。rdbchecksum默认开启加载RDB文件时会做CRC64校验能发现文件损坏但加载大文件时会增加一些时间。这俩我一般保持默认。2.4 为什么RDB注定有一个数据丢失窗口想要彻底理解RDB的局限性把数据写入周期画一遍就清楚了t0时刻触发BGSAVEt1时刻fork完成t1到t2时刻之间所有写入只存在于内存和AOF缓冲区中如果没开AOF的话t2时刻快照落盘完成。如果Redis在t2之前崩溃那么t1之后、t2之前的所有数据全部丢失即使t2完成了从上一次快照完成到这次快照完成之间的窗口期数据同样不在新快照里。这个窗口有多大取决于你配置的save策略和数据量。默认配置下极端情况可能丢几十分钟甚至更久的数据。所以如果你的业务要求最多丢1秒RDB单独使用是不可能达标的。这也是AOF存在的根本原因——它记录的是命令本身落盘频率可以做到每秒甚至每条。3. AOF追加日志三种刷盘策略与自愈式重写AOFAppend Only File的设计思路简单粗暴既然内存里的数据每次变化都是因为执行了写命令那我就把每一条写命令按顺序追加到一个日志文件末尾。恢复的时候从头到尾重放这些命令目标数据集就被重建出来了。它不追求快照时刻而是通过连续性实现很小的丢失窗口。3.1 appendfsync三种策略性能和可靠性的三档选择AOF默认是关闭的需要手动打开appendonly yes appendfilename appendonly.aof appendfsync everysecappendfsync控制的是日志写入磁盘的时机这个参数直接决定AOF能丢多少数据。落盘这件事有两层先写入操作系统的page cachewrite再把cache里的数据刷到磁盘fsync。write本身很快但系统崩溃时cache里的数据可能丢失所以必须靠fsync来控制持久化点。三种策略的实测定级如下策略写入时机性能影响理论上限丢失量适合场景always每条命令都fsync最大吞吐量可能下降一半以上最多丢1条命令金融、订单、强一致场景everysec每秒批量fsync一次较小实测日常读写影响可接受最多丢1秒数据绝大多数互联网业务no完全交给操作系统最小视OS刷盘周期最多能丢几秒到几十秒可接受较大丢失的纯缓存我线上主力用的是everysec性能和数据安全的平衡最好。always真的只建议在丢一条都等于事故的场景下开而且要有心理准备高并发写入时Redis的QPS会明显下滑。测试过一个简单的SET基准always比everysec大约慢50%到80%具体要看写入的批量和硬件。3.2 AOF重写机制为什么文件不会无限膨胀AOF把每条写命令都追加到文件里那么同一个key被更新一万次文件里就有一万条记录恢复时要重放一万遍文件体积一定爆炸。重写机制就是来解决这个问题的它不是修改原AOF文件而是基于当前内存中的数据集重新生成一份从零到当前状态的最小命令集。举个例子。假设某个key执行了SET K 1、SET K 2、SET K 3三条命令原AOF里会有三条记录但重写后的AOF只需要一条SET K 3因为只看最终结果的话前两条都是无用功。类似地INCR连续执行100次原文件里是100条INCR重写后变成一条SET K 100。所有历史命令在这里得到去重和数据压缩。3.3 重写期间的新写命令去哪了AOF重写不是Lock住Redis慢慢生成的。BGREWRITEAOF同样走fork子进程路线子进程基于fork时点的内存数据生成新的AOF文件主体。但fork之后主进程还在继续接收新写入这部分增量命令如果直接丢掉新AOF就会缺少这段时间的数据。Redis的处理是重写期间所有新写命令同时追加到旧AOF和一块重写缓冲区里子进程生成完主体文件后主进程再把重写缓冲区里的命令追加到新AOF尾部最后rename覆盖旧文件。这个过程细看有几个需要注意的配置auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb no-appendfsync-on-rewrite no前两个配置决定自动重写的触发时机当AOF文件大小比上次重写后的大小增长超过100%即翻倍且当前文件超过64MB就自动触发一次重写。no-appendfsync-on-rewrite是需要权衡的如果设为yes重写期间不执行fsync可以缓解磁盘压力但代价是重写期间新写入的数据在突发断电时丢失窗口变大如果保持no则每条/每秒都正常fsync保证重写期间数据可靠性不受影响但磁盘IO压力会叠加。我建议一般情况下保持默认no只有实测发现重写期间写延迟明显飙升时才考虑开启。还有一个容易被忽略的点重写缓冲区如果一直有大量写入而子进程生成主体文件的速度跟不上缓冲区会持续膨胀最终可能拖垮内存。Redis的应对是如果重写缓冲区超过一定阈值就再fork一次但我见过极端写入场景下AOF重写反复卡住的情况。解决思路一般是把自动重写阈值调大或者直接在低峰期手动执行BGREWRITEAOF总之不要在高峰期跟它硬碰硬。3.4 AOF文件损坏了怎么办既然AOF是持续追加写入的文件断电、磁盘异常都可能导致文件尾部出现半截命令。Redis提供了两条腿走路加载时如果遇到不完整的命令会根据aof-load-truncated参数的设置决定是直接报错拒绝启动还是忽略尾部残缺继续加载。aof-load-truncated yes生产环境我建议保持yes让Redis能自动从异常中恢复——丢一条半截命令总比整个实例起不来强。但如果你的场景是绝不能有半点数据隐性问题那把yes改成no启动失败后人工介入用redis-check-aof工具修复。修复命令很简单redis-check-aof --fix appendonly.aof它会从头扫描文件定位损坏点并把损坏部分截断掉再做一次完整性校验。注意如果是RDB文件损坏工具对应是redis-check-rdb dump.rdb。4. 到底选RDB还是AOF混合持久化才是答案先给结论Redis 4.0及以上版本生产环境我基本都会同时开启RDB和AOF并打开混合持久化。真正该纠结的从来不是用哪个而是怎么配合才能既满足可靠性又不牺牲性能。4.1 从默认配置看官方演进思路Redis很长一段时间的默认持久化配置只有RDB的save规则AOF默认关闭。很多人据此认为官方推荐用RDB。这其实是过度解读——默认关闭AOF主要是为了让新用户开箱即用不踩性能坑并不代表RDB可以满足数据安全要求。从4.0引入混合持久化开始官方释放的信号就很明确了想要数据安全RDB和AOF不是替代关系而是互补关系。aof-use-rdb-preamble这个参数在4.0之后默认就是yes意味着即使只开了AOF它生成的文件也不再是纯命令日志而是RDB的二进制快照 快照之后的增量命令这就是混合持久化。4.2 混合持久化兼顾快照速度和恢复效率解释一下混合持久化的工作方式AOF重写时不是生成纯命令日志而是先以RDB格式写入fork时点内存数据的二进制快照作为文件前缀然后把重写缓冲区里的增量写命令以AOF格式追加在文件后面。这样一个AOF文件就成了RDB头部 AOF尾部的混合体。好处一目了然。恢复时Redis先加载RDB前缀这个动作比分批重放千万条命令快得多加载到RDB头部结束的位置再继续重放后面一小段AOF命令补上从fork到宕机前一刻的增量数据。我实测过一个大约2GB数据量的实例纯AOF文件恢复需要两分多钟混合持久化文件恢复只有十来秒差距非常显著。4.3 不同业务场景的参考配置与选型建议这里给四套可以直接参考的组合纯缓存场景可接受全部丢失save 关闭RDBappendonly no关闭AOF。要什么持久化丢了正好省内存重启后自然冷启动重新回填。缓存为主可接受分钟级丢失只开RDB按业务低谷期手动BGSAVE或用宽松的save策略。推荐配置save 3600 1或直接在每天低峰期用crontab执行redis-cli bgsave。数据不能丢、但没到强一致大多数业务appendonly yesappendfsync everysec 开着RDB兜底 混合持久化。这套组合是我在线上用得最多的。金融级强一致任何一条都不能丢appendonly yesappendfsync alwaysRDB照常开定期备份RDB文件。性能下降要有心理预期必要时通过扩容或拆分key来补偿。4.4 持久化和备份是两回事别混为一谈很多事故的后半段都是同一个剧本Redis持久化文件还在但整台机器没了。RDB和AOF文件都存在本地磁盘机器宕了、硬件坏了、机房出问题本地文件再完整也白搭。所以生产环境必须再叠一层异地备份。我的做法是每天凌晨低峰期执行BGSAVE然后用crontab脚本把生成的dump.rdb拷贝到另一台存储机器或云对象存储保留最近7到30天。恢复演练每季度做一次确保备份文件真的能加载、数据量级和数据内容没跑偏。光是每个实例都落了RDB文件从来不是高枕无忧的理由该文件在另一台机器上也能恢复才是。5. 上线前和故障后我最常被问到的持久化问题最后这部分写给正在被线上问题折磨的人。感觉每天都有一堆人在不同的群里问类似的问题为什么重启后数据少了为什么SAVE卡了一下为什么AOF重写把磁盘写满了这些问题的答案往往不在某一个配置项上而在对持久化机制整体运作的理解。5.1 fork阻塞为什么Redis会出现幽灵停顿之前说了BGSAVE和BGREWRITEAOF都会fork子进程而fork()本身是阻塞主进程的。这个阻塞时间取决于实例内存大小和系统内存管理效率。单线程的Redis在fork期间无法处理任何请求表现出来的现象就是明明没有慢查询客户端却偶尔卡顿几十甚至几百毫秒。排查手法很简单INFO stats里看latest_fork_usec字段单位是微秒。如果这个值持续走高或者偶发超过几百毫秒基本就能定位到fork阻塞。缓解手段有三个方向一是控制单实例内存不建议大到让fork时间破百毫秒一般经验值是单实例几GB内相对安全二是把自动RDB或自动AOF重写尽量安排在低峰期错开业务高峰三是如果有条件用物理机或内存性能好的云主机内核的fork效率会明显好于超卖的廉价虚拟化环境。5.2 大key对持久化的实际影响一个几MB甚至几十MB的字符串、一个包含百万元素的hash或list就是大key。大key对持久化的危害体现在几个环节fork时因为要复制大内存页的指针表时间更长BGSAVE写快照时大key整体序列化需要连续占用磁盘IOAOF重写时大key的重新序列化同样是CPU和内存的双重压力。定位大key用Redis自带命令就行redis-cli --bigkeys会扫描并输出最大的前几个key和它们的类型、大小。我一般建议同时配合定期巡检因为大key往往是慢慢长大的不是一天突然出现的。如果已经发现了常规出路是拆分把大hash拆成多个小hash把大list做分片或者用UNLINK命令异步删除以便释放内存而不是用阻塞的DEL。5.3 重启恢复顺序和数据校验工具Redis有RDB和AOF同时存在时启动加载的顺序是优先AOF。为什么因为AOF记录的粒度更细、数据更新。而混合持久化出现后AOF文件本身就包含RDB前缀所以这个顺序逻辑依然成立。如果你改了appendonly no重启后Redis才会去找RDB文件加载。很多人改完配置不重启或者改了参数但忘了AOF文件还躺在目录里结果数据恢复的来源跟预期不一致排查起来很绕。加载完成之后推荐用redis-cli -a 密码 dbsize配合业务侧抽样确认数据条数符合预期。注意RDB加载是不打印数据的只会在日志里显示DB loaded from disk等提示要确认加载的是哪个文件、用了多久直接看启动日志最靠谱。真遇上文件损坏不要慌用前面提到的redis-check-rdb和redis-check-aof工具先做诊断再决定是修复还是从备份恢复。5.4 关于磁盘和内核参数的一些提醒持久化最终都落在磁盘上所以磁盘状态直接决定持久化的可靠性和性能。先说常见的坑RDB快照或AOF重写时临时文件写完后通过rename替换旧文件这个操作需要磁盘有足够的剩余空间至少能容纳一个和当前数据集差不多大小的写缓冲。很多人在磁盘用量比例很高时才重视空间结果重写一半直接失败stop-writes-on-bgsave-error yes生效后业务写请求全面被拒。所以我建议磁盘空间监控的告警阈值至少设在60%以下给BGSAVE和AOF重写留出余量。另一个内核参数是vm.overcommit_memory。Redis在BGSAVE的文档里明确建议把它设为1否则在内存占用较高时fork子进程很可能因为内存申请被拒而失败。临时改一下sysctl vm.overcommit_memory1永久生效就写进/etc/sysctl.conf。另外transparent_hugepage建议关闭echo never /sys/kernel/mm/transparent_hugepage/enabled这个和fork性能、内存复制开销相关很多云主机上默认打开会导致fork变慢和延迟抖动。按我自己的经验把持久化问题当成一个整体来看比孤立的调某个参数有效得多。先明确业务能接受丢多少数据再选型组合选完型把每个参数的含义搞清楚再动配置上线后监控RDB状态、AOF重写时间、fork耗时和磁盘余量最后定期做恢复演练确保备份是真的能用的。这套流程走顺了Redis的持久化才算真正省心。