REDIS 7 · RDB · AOF · FORK · COPY-ON-WRITE · FSYNC

内存一断电就清零,所以要么拍照,要么记账

Redis 的数据都在内存里,进程一退出就没了。持久化就是把它存到磁盘上,重启时再读回来。有两种办法:隔一段时间给整个内存拍张照(RDB),或者把每一条写命令记进账本(AOF)。

RDB:拍照dump.rdb

某一刻整个内存长什么样,压缩成一个二进制文件。文件小、恢复快,但两张照片之间写入的数据,宕机就丢了。

AOF:记账appendonly.aof

每执行一条写命令,就在文件末尾追加一行。重启时从头再执行一遍。丢得少,但文件大、恢复慢。

同样在 12:41 断电,各丢多少
RDB丢 11 分钟
AOF丢约 1 秒

白色竖线是 RDB 拍照的时刻(示意:每 15 分钟一次);AOF 按默认的每秒刷盘。绿色是重启后还在的数据,红色是丢掉的。

主进程redis-server

用一个线程处理所有命令,它一卡,所有客户端都卡。

持久化尽量别让它干重活
子进程fork()

主进程复制出来的分身,替它把内存写到磁盘。

写 RDB、重写 AOF 都靠它
照片dump.rdb

某一刻的全量数据,紧凑的二进制文件。

适合备份、恢复快
账本appendonly.aof

每条写命令按顺序追加,重启时从头重放。

丢得少,越记越长

RDB:怎么拍一张不卡的照片

直接让主进程去写文件(SAVE 命令)会把它卡住几十秒,生产环境不能用。平时用的是 BGSAVE:fork 一个子进程去写,主进程接着处理请求。

1满足条件,触发配置的 save 规则到了、手动 BGSAVE、从节点要全量同步(正常关机时是直接 SAVE)
2fork 出子进程主进程会停一下,内存越大停得越久,大约每 GB 十几毫秒
3子进程写临时文件把 fork 那一刻的内存写进 temp-<pid>.rdb,压缩、刷盘
4改名替换写完后原子地改名成 dump.rdb,旧照片这时才被替换
刚 fork 完:两个进程共用同一份内存
主进程
123456
子进程
123456

fork 只复制页表,不复制数据,所以很快。灰色表示两边指向同一块物理内存。

主进程改了第 3 页:只复制这一页
主进程
123′456
子进程
123456

这叫写时复制(Copy-On-Write)。子进程看到的永远是 fork 那一刻的样子。写得越多,复制的页越多,最坏情况内存接近翻倍。

什么时候自动拍照Redis 7 的默认规则
save 3600 1 300 100 60 10000
# 3600 秒内至少 1 次修改,或
# 300 秒内至少 100 次修改,或
# 60 秒内至少 10000 次修改,就 BGSAVE 一次

save ""                          # 关掉 RDB
stop-writes-on-bgsave-error yes   # 拍照失败就拒绝写入
RDB 的好处和代价

好处:一个紧凑的文件,拷走就是一份完整备份;重启时直接加载,比重放命令快得多;平时写盘的活都在子进程,主进程几乎不受影响。

代价:两次拍照之间的写入,宕机就丢,通常是几分钟;数据量大时 fork 会让主进程停顿,写多时写时复制会多占内存。

AOF:每一笔都记账

打开 appendonly yes 后,每条写命令执行完,会先放进内存里的缓冲区,再写进文件。写进文件还不等于落到磁盘:操作系统会先放在自己的缓存里,要等 fsync 才真正写到盘上。

执行命令SET stock 100 追加到 aof_bufRedis 进程内存 write()进了操作系统缓存,进程挂了也不丢 fsync()真正落盘,断电也不丢

always每条都刷盘

断电丢
几乎不丢
性能
最慢,每次写都等磁盘

everysec默认,每秒刷一次

断电丢
约 1 秒(极端情况 2 秒)
性能
好,后台线程去刷

no交给操作系统

断电丢
Linux 通常最多约 30 秒
性能
最快
和 MySQL 对比这和 MySQL 的 innodb_flush_log_at_trx_commit 是同一个取舍:设成 1 相当于 always,设成 2 相当于 everysec(进程挂了不丢,断电丢约 1 秒)(数据库第 01 篇)。不同的是,MySQL 先写日志再改数据,Redis 先执行命令再写日志。

账本越记越长:AOF 重写

同一个 key 改了一万次,账本里就有一万行,其实只需要最后一行。文件比上次重写后大一倍、并且超过 64 MB 时(默认),Redis 会 fork 子进程重写一份最短的账本。

重写前(9 条)
  1. SET stock 100
  2. DECR stock
  3. DECR stock
  4. DECR stock
  5. SADD tags a
  6. SADD tags b
  7. SREM tags a
  8. SET tmp x
  9. DEL tmp
重写后(2 条)
  1. SET stock 97
  2. SADD tags b

子进程不读旧文件,而是直接看内存里现在是什么,写出能还原它的最少命令。

宕机实验台:选一种配置,拖动宕机时刻

Redis 每秒写入 100 次,跑了 5 分钟。选持久化方式和宕机方式,拖动红线,看重启后能恢复到哪一刻、丢了多少。

持久化:
怎么宕机:
第 203 秒

示意模型:RDB 按 save 60 1000 规则大约每分钟拍一次,每次子进程写 8 秒;混合持久化在第 150 秒做过一次 AOF 重写;断电时 everysec 按丢 1 秒、no 按丢 30 秒算(最坏情况)。

混合持久化:照片打底,账本补尾巴

AOF 重写时,子进程不再写一堆命令,而是直接写一张 RDB 照片当开头,之后的写命令再追加在后面。重启时先加载照片(快),再重放后面那一小段命令(少)。Redis 5 起默认开启(aof-use-rdb-preamble yes),前提是打开了 AOF。

Redis 7 的 AOF 目录多文件 AOF
appendonlydir/
├── appendonly.aof.1.base.rdb   # 上次重写时的照片(RDB 格式)
├── appendonly.aof.1.incr.aof   # 之后追加的写命令
└── appendonly.aof.manifest     # 清单:现在用的是哪几个文件
重启时先读哪个
appendonly 开了吗?
开了读清单 → 加载 base 照片 → 重放 incr 命令
没开有 dump.rdb 就加载它都没有空库启动

开了 AOF 就只认 AOF,即使旁边还有一个更新的 dump.rdb 也不会读。

不持久化只开 RDBAOF everysecAOF always混合(推荐)
断电丢多少全部几分钟约 1 秒几乎不丢约 1 秒
进程被 kill 丢多少全部几分钟几乎不丢几乎不丢几乎不丢
文件大小—小大大中
重启速度最快(空的)快慢,要逐条重放慢快
平时开销没有定期 fork每秒刷盘 + 重写时 fork每条写都等磁盘同 everysec
适合纯缓存,数据库里都有能接受丢几分钟,要做备份要少丢数据极少用大多数需要持久化的场景

怎么选

先问一句:Redis 里的数据丢了,能不能从别处补回来?

纯缓存,数据库里都有可以不持久化save "" · appendonly no

重启后空着,按需从库里加载。注意预热和击穿(Redis 第 01 篇)。想重启后快点热起来,可以只开 RDB。

能接受丢几分钟只开 RDBsave 规则 + 定期拷走 dump.rdb

比如排行榜、统计数据。RDB 文件拷到别的机器,就是一份备份。

会话、计数、分布式锁混合持久化appendonly yes · everysec

最多丢一秒左右,重启也快。官方的建议也是 RDB 和 AOF 一起开。

一条都不能丢别只靠 Redis放进数据库

就算 always,主从复制也是异步的,主节点挂了切到从节点,没同步过去的写入一样会丢。

一份常用配置redis.conf
save 3600 1 300 100 60 10000      # RDB 规则(Redis 7 默认)
appendonly yes                     # 默认 no,要手动打开
appendfsync everysec               # always / everysec / no
aof-use-rdb-preamble yes           # 混合持久化
auto-aof-rewrite-percentage 100    # 比上次重写后大一倍就重写
auto-aof-rewrite-min-size 64mb     # 小于这个大小不重写
aof-load-truncated yes             # 文件末尾不完整时,截掉最后一条后照常启动

Redis 6 和 Redis 7

Java 这边不用改:持久化是 Redis 服务端的事,Boot 2 还是 Boot 3、JDK 8 还是 17 都一样。版本差异在 Redis 自己身上。

Redis 6.x 及以前

单文件 AOF
  • AOF 就是一个 appendonly.aof 文件
  • 重写期间,主进程把新写入同时攒在一个重写缓冲区里,重写结束时再追加到新文件,多占内存,收尾时还会卡一下
  • 默认 RDB 规则是 save 900 1 / 300 10 / 60 10000

Redis 7.0 起

多文件 AOF
  • AOF 拆成 base + incr + 清单,放在 appendonlydir 目录
  • 重写时主进程直接开一个新的 incr 文件接着写,不再需要重写缓冲区
  • 默认 RDB 规则改成 save 3600 1 300 100 60 10000
  • 可以给 AOF 打时间戳(aof-timestamp-enabled),配合 redis-check-aof 恢复到某个时间点
Java 能感受到的一是 fork 时的延迟毛刺,客户端超时设得太短会偶尔报错;二是拍照失败后写入被拒,Spring Data Redis 会抛出带 MISCONF 字样的异常。
没变的:RDB 靠 fork + 写时复制AOF 三种刷盘策略重写按内存现状来写开了 AOF 就优先加载 AOF

常踩的坑

持久化的坑,一半是 fork 带来的,一半是以为“开了就不会丢”。

MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

后台拍照失败(磁盘满了、目录没权限),Redis 默认拒绝一切写入。先修磁盘,再看日志里 BGSAVE 失败的原因。

每隔几分钟,请求就慢一下

fork 时主进程停住,实例越大停得越久。INFO stats 里的 latest_fork_usec 能看到上次停了多久。单个实例别太大,内存大就拆成集群。

一拍照就内存不够,甚至被系统杀掉

写得多时,写时复制让内存涨很多。maxmemory 要留出余量;启动日志里关于 overcommit 的警告要处理,把 vm.overcommit_memory 设成 1,否则 fork 可能直接失败。

开了透明大页,fork 后延迟和内存都变差

透明大页让每次写时复制要复制 2 MB 而不是 4 KB。Redis 启动时检测到它开着会打警告,按提示关掉(新版本 Redis 也会尝试只对自己关掉)。

主节点重启,从节点的数据全没了

主节点没开持久化又设置了自动重启:它空着启动,从节点跟它同步,于是也被清空。主节点要么开持久化,要么别自动重启。

重启要半小时

只开 AOF、从没重写过,要把几千万条命令重放一遍。打开自动重写和混合持久化。

Asynchronous AOF fsync is taking too long (disk is busy?)

磁盘太慢,每秒一次的刷盘跟不上,Redis 开始拖慢写入。换更快的盘,或者别和其他重 IO 的程序共用一块盘。

误执行了 FLUSHALL

开了 AOF 且还没重写的话,马上停止 AOF 重写、停掉 Redis,把 AOF 末尾那条 FLUSHALL 删掉再启动,就能找回来。一旦重写了就没了,所以要靠 RDB 备份。

断电后 AOF 文件坏了,Redis 起不来

末尾只写了半条命令时,默认 aof-load-truncated yes 会截掉它照常启动;坏在中间就要用 redis-check-aof --fix 修,修之前先备份。

备份和数据放在同一块盘上

dump.rdb 只在本机,盘坏了一起没。定期把 RDB 拷到别的机器或对象存储,并且真的试着恢复一次。