Redis 缓存学习笔记(二):数据持久化

RDB和AOF

众所周知Redis是内存数据库,为了提高redis的可靠性,让redis在遇到神秘小故障导致的重启时不至于丢失所有的数据,我们需要将相关数据进行落盘;主流的落盘方式其实大家应该差不多能想到:

  • 要么存一份完整的数据拷贝,需要进行数据恢复时就拿出来;当然这种方案有个问题是要权衡数据拷贝的频次和能接受的丢数据时间窗
  • 要么存操作日志,数据恢复时重放所有的操作即可;这个方案的好处是数据不容易丢,毕竟追加写日志文件是一个相对拷贝整个内存数据要轻得多的操作,我们完全可以每次操作都进行一次日志文件刷盘,从而保证数据不丢------当然这也是有代价的,而且redis默认并不是这样做的,这个我们后文再讨论
  • 或者------能不能让快照负责快恢复、让日志负责少丢,各司其职一起用?

Redis 把前两种思路分别做成了 RDB(快照)和 AOF(日志);而第三种「快照+日志合体」的思路,Redis 5.0+ 默认用「混合AOF」实现了(见后文 AOF 部分)。

RDB

RDB 的核心是快照------把某一时刻的整份内存拍下来。听起来简单,但麻烦在于:Redis 是单线程在持续处理命令的,内存一直在变。要拍一张和当前状态一致的快照,又不能让主线程停下来等,怎么办?Redis 的答案是 fork + 写时复制(COW)

fork

fork 是 Unix/Linux 的一个系统调用,fork的本质是让OS创建一个和当前进程的页表完全相同的进程(父进程和子进程各自持有一份完全相同的页表,这里要注意fork时完全相同,不意味着之后也完全相同),底层指向相同的物理内存

python 复制代码
fork() 之前:1 个进程(Redis 主进程)
fork() 之后:2 个进程
   - 父进程:原来那个 Redis
   - 子进程:fork 出来的,和父进程几乎完全相同

COW

fork 时,OS 内核会将父子进程页表中一部分"私有可写页"的页表项标记为只读,这些页通常包括 Redis 真实数据所在的内存页。之后如果某个进程尝试写入这些页,MMU 会因为页表项只读而触发缺页异常,OS 内核接管后执行 COW(写时复制):复制原物理页的内容到一个新物理页,并把写入进程的页表项改为指向新页且恢复可写权限,然后该进程再完成写入。

fork + COW 怎么拍出一致快照

fork 之后分工是:子进程负责遍历内存、把数据序列化写进 dump.rdb,它可以慢慢写(写盘是 IO,不快);父进程继续接客户端命令、不停。

关键在 COW:当父进程收到写命令、要改某个页时,触发写时复制------父进程拿到一个新副本去改,而子进程的页表项依然指向 fork 那一刻的旧物理页。

所以子进程眼里看到的,永远是 fork 瞬间被冻结的那份内存,哪怕父进程一直在改。这就是 RDB 能拍出「一致快照、又不阻塞主线程」的真正原理。

那么代价是什么呢

  1. fork本身是阻塞的------由主线程执行,需要进行页表的复制,页表的复制和redis的实际的存储内容大小正相关
  • 几百 MB 的小实例:fork 几毫秒,几乎无感
  • 几 GB 的实例:fork 几十毫秒,开始能感觉到延迟尖刺
  • 几十 GB 的大实例:fork 可能几百毫秒甚至上秒 → 请求超时、主从同步延迟
  1. COW时如果有redis数据被频繁修改,会复制大量内存页,造成大量内存开销
  • 最坏情况:fork 期间所有页都被写过一遍 → 内存用量接近翻倍 → 可能触发 OOM,所以单机 Redis 的 maxmemory 一般不超过物理内存的 50%~70%(即留出 30%~50% 给操作系统和 COW 复制使用),防止 bgsave / AOF 重写时内存翻倍触发 OOM。

RDB的创建方式

手动输入命令创建生成RDB
命令 怎么干活 阻塞谁 何时用
SAVE 主线程亲自 序列化 + 写盘,不走 fork 主线程全程阻塞 几乎不用,太狠
BGSAVE fork 子进程写盘 只在 fork 瞬间阻塞主线程 日常手动触发就它
通过配置自动触发生成RDB

这是生产里最常见的来源,redis.conf 里配的规则:

save 3600 1 # 3600秒内至少1个key变化

save 300 100 # 300秒内至少100个key变化

save 60 10000 # 60秒内至少10000个key变化

Redis 内部有个周期性定时任务(serverCron,默认每 100ms 跑一次),周期性地去数「最近 N 秒内变了多少 key」,命中规则就触发 BGSAVE

其他事件连带触发生成RDB
  • SHUTDOWN:Redis 正常关闭时,会自动执行一次 SAVE
  • 主从全量复制:从节点第一次连主节点 / 断线太久,主节点会自动 BGSAVE 生成 RDB 传给从节点

RDB 的优缺点 / 适用场景

优点:
  • 恢复速度快,相比AOF省去了执行命令带来的时间开销
  • 单个 dump.rdb 文件,二进制压缩,体积小,"便携性"好
  • 对主线程影响可控,bgsave fork子进程写盘,主线程仅有创建子进程的开销
缺点:
  • 周期性快照带来的丢数据问题------一旦宕机,上次 bgsave 之后到崩溃之间的所有写入都会丢,RDB 保不住细粒度写入
  • redis数据量大的情况下带来的 fork时空开销------fork时复制页表会阻塞主线程,COW机制会带来最坏情况下接近翻倍的额外内存开销

综上其实RDB适合缓存业务------丢内存数据了大不了就从DB数据库中重新读数据建缓存

AOF

AOF的本质是将每一条写命令都按执行顺序写入一个专门的日志文件appendonly.aof中

  • 只记写命令:GET 这种读命令不记(读了也不改变状态,记它没意义)
  • 追加(append):永远是往文件尾巴加,不修改历史 → 所以叫「只追加文件」(Append-Only File)
  • 记执行后的命令:是先执行成功、再把命令写进日志(不是先写日志再执行,这点后面重写/恢复时会用到)

命令从「执行」到「落盘」的完整流程 大致如下

复制代码
客户端发来写命令(如 SET k v)
        │
        ▼
① Redis 主线程执行命令(改内存)   ← 命令先在内存里生效
        │
        ▼
② 执行成功后,把这条命令「追加」进 AOF 缓冲区 aof_buf(内存)
        │
        ▼
③ 根据 fsync 策略,把 aof_buf 的内容写到 OS 页缓存(write())
        │
        ▼
④ 调用 fsync(),把页缓存的数据真正刷到物理磁盘   ← 这一步才叫「落盘」

可以发现其实只有到了第④步,日志才真正的持久化保存到物理硬盘中;所以第④步执行是否足够频繁,决定了AOF到底是只丢有限的数据还是真的不丢数据

AOF的落盘策略

策略 多久 fsync 一次 特点 使用场景
always 每条命令都 fsync 最安全,最慢 几乎不用,太慢了,除非是资金类业务且能容忍慢
everysec 每秒 fsync 一次(默认) 折中 生产环境常用
no Redis 不管,交给 OS 最快,丢最多 纯缓存场景,或者主从架构场景,可以靠从节点进行数据同步

如果是纯缓存业务+有主从架构,那用什么级别?

取舍 怎么配 理由
性能拉满,持久化全靠从节点 **主关 AOF/RDB **,从开 AOF/RDB 最经典,主节点零持久化开销,但前提是主备倒换机制可靠,避免无持久化主节点空数据重启后反向污染从节点
想留个主节点本地保险 主用 no 策略 图快,反正有从节点和 DB 兜底
想主节点重启也能恢复 主用 everysec 默认甜点,最多丢1秒、几乎不阻塞

要注意的是:Redis 是一个"高性能内存组件",不是"强一致的数据保险箱"。 不管你用主从倒换、RDB、AOF 还是混合持久化,没有任何一种组合能保证"内存数据一条都不丢"。

混合AOF

通过RDB存储存量数据+AOF 存储fork之后的增量命令,实现结合RDB恢复快+AOF丢数据相对少的优点

重写流程
步骤 干什么 重点
1. fork 子进程 主进程 fork 出子进程 主进程继续扛流量,不阻塞
2. 子进程写 RDB preamble 把当前数据集写成 RDB 格式,作为新 AOF 开头 这就是"混合"的 RDB 部分
3. 父进程攒增量 重写期间新命令同时写 AOF 缓冲区 + 重写缓冲区 双写,防止丢
4. 子进程写完退出 发信号通知父进程 RDB preamble 落盘完成
5. 父进程追加增量 把重写缓冲区的命令追加到新 AOF 尾部 这就是"混合"的 AOF 部分
6. 原子 rename 新文件替换旧 AOF 文件 保证替换瞬间一致
恢复流程
场景 怎么恢复 速度
有 RDB preamble 先加载 RDB(二进制,快)→ 再重放尾部增量命令 🚀 快
无 preamble(旧格式) 全程重放所有 AOF 命令 🐢 慢

Redis 启动时会检查文件开头的 RDB 魔数判断是不是混合格式,是就走"快通道"。

延伸对比:为什么AOF是先执行再写日志,而Mysql是先写日志再执行?

MySQL 的 WAL 保护的是"已提交事务":事务对外宣布成功前,redo log 必须先可靠,因此崩溃后可以通过 redo/undo 恢复一致状态。MySQL 不在 COMMIT 时直接刷数据页,是因为数据页分散、体积大、随机 IO 多,而且数据页刷盘不能天然保证事务原子性。redo log 用顺序小写入记录"怎么重做",undo log 记录"怎么撤销",提交时只要先把日志弄可靠,就能让数据页以后由后台慢慢批量刷,既保证崩溃恢复,又大幅提高性能。

Redis AOF 记录的是"已成功执行的命令":命令先改内存,成功后再追加到 AOF,因此实现简单、日志干净,但如果执行成功后还没可靠写盘就宕机,仍可能丢失。两者差异来自定位不同:MySQL 追求事务 ACID,Redis 追求高性能内存数据结构服务。