
上一篇【第28篇】数据库通知——Redis的事件订阅机制下一篇【第30篇】RDB文件格式完全解析——一个快照文件里藏着什么秘密如果说Redis是内存中的数据库那RDB就是它的时间胶囊——把某一刻的内存状态完整地封存起来万一哪天Redis挂了打开胶囊就能还原现场。引言为什么需要持久化Redis是内存数据库数据都住在内存里。内存是什么断电就没了。如果你的Redis存的是缓存数据也就罢了丢就丢了从数据库再查一遍就好。但如果是业务数据呢用户积分、订单状态、配置信息——这些丢了可没法跟老板解释。RDBRedis Database就是Redis提供的第一种持久化方案把内存中的数据以快照的形式保存到磁盘。就像给你的Redis拍了一张全家福不管之后发生了什么这张照片永远记录了那个瞬间的状态。SAVE命令阻塞型的危险操作SAVE命令是Redis最古老的持久化方式执行后Redis主进程会阻塞直到RDB文件生成完毕客户端发送 SAVE 命令 │ ▼ ┌────────────────────────────┐ │ Redis主进程被阻塞 │ │ │ │ 遍历所有数据库的所有键 │ │ 逐个写入RDB文件 │ │ │ │ 期间不能处理任何客户端请求 │ ← 所有请求排队等待 │ │ │ RDB文件生成完毕 │ │ 主进程恢复 │ └────────────────────────────┘SAVE的问题显而易见如果Redis中有1000万个键生成RDB可能需要几秒甚至几十秒。这几秒内Redis完全无法响应请求——对于在线服务来说这无异于自杀。# 执行SAVE观察阻塞时间127.0.0.1:6379SAVE OK(3.52s)# 大数据集可能需要更长时间踩坑提示生产环境绝对不要使用SAVE命令它会阻塞主进程导致所有客户端请求超时。除非你在维护窗口期手动备份且能容忍短暂的不可用。BGSAVE命令后台拍照的正确姿势BGSAVEBackground Save才是RDB持久化的正确打开方式。它的核心思想是fork一个子进程来做脏活父进程继续服务客户端。BGSAVE的完整流程┌─────────────────────────────────────────────────────────┐ │ BGSAVE 执行流程 │ │ │ │ 客户端: BGSAVE │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 父进程 fork() │ │ │ └──────┬───────┘ │ │ │ │ │ ┌────┴─────────┐ │ │ │ │ │ │ ▼ ▼ │ │ 父进程 子进程 │ │ (继续处理 (开始生成RDB) │ │ 客户端请求) │ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ 遍历键空间 │ │ │ │ │ 写入临时 │ │ │ │ │ RDB文件 │ │ │ │ └─────┬──────┘ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ 重命名为 │ │ │ │ │ dump.rdb │ │ │ │ └─────┬──────┘ │ │ │ │ │ │ │ ┌─────▼──────┐ │ │ │ │ 通知父进程 │ │ │ │ │ (信号机制) │ │ │ │ └────────────┘ │ │ │ │ │ ▼ │ │ 父进程收到通知 │ │ 更新 lastsave 时间 │ │ RDB完成 │ │ │ └─────────────────────────────────────────────────────────┘BGSAVE的关键步骤父进程调用fork()系统调用创建子进程子进程根据父进程的内存快照生成RDB文件写到临时文件子进程完成后将临时文件重命名为dump.rdb子进程通过信号通知父进程RDB已完成父进程更新lastsave时间戳# 执行BGSAVE不阻塞主进程127.0.0.1:6379BGSAVE Background saving started# 期间可以正常执行其他命令127.0.0.1:6379GET user:1TomSAVE vs BGSAVE 对比维度SAVEBGSAVE是否阻塞完全阻塞主进程仅fork瞬间短暂阻塞执行者主进程子进程客户端可用性不可用可用适用场景仅维护窗口生产环境标准用法内存开销无额外开销fork需要额外内存复杂度低高涉及fork和COWfork与写时复制COWBGSAVE之所以能让父进程继续服务关键在于fork()系统调用和**写时复制Copy-On-Write, COW**机制。fork()的原理fork()是Linux系统调用它创建一个子进程子进程是父进程的完整副本——包括内存空间、文件描述符等。但Linux采用了COW优化fork() 之后 │ ▼ ┌──────────────────────────────────────────┐ │ 物理内存页 │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ P1 │ │ P2 │ │ P3 │ │ P4 │ │ P5 │ │ │ └─┬──┘ └──┬─┘ └──┬─┘ └──┬─┘ └──┬─┘ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ ┌─────────────────────────────┐ │ │ │ 共享的页表只读映射 │ │ │ └──────────────┬──────────────┘ │ │ │ │ │ ┌──────┴──────┐ │ │ │ │ │ │ 父进程页表 子进程页表 │ │ (指向同一 (指向同一 │ │ 物理页) 物理页) │ └──────────────────────────────────────────┘ 当父进程修改某个页时比如修改P3 │ ▼ ┌──────────────────────────────────────────┐ │ 操作系统复制 P3 → P3 │ │ 父进程页表指向 P3可写 │ │ 子进程页表仍指向 P3只读不变 │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ P1 │ │ P2 │ │P3 │ │ P4 │ │ P5 │ ← 父进程用 │ └────┘ └────┘ └────┘ └────┘ └────┘ │ │ ┌────┐ │ │ │ P3 │ ← 子进程用原版 │ │ └────┘ │ └──────────────────────────────────────────┘COW的核心思想fork()后父进程和子进程共享同一块物理内存页表设为只读当父进程或子进程试图修改某个内存页时触发页错误page fault操作系统将该页复制一份修改方使用新副本未修改方继续使用原页这样fork()本身不需要复制所有内存只有在修改时才逐页复制COW对Redis的影响COW机制意味着fork()本身很快只复制页表不复制实际内存子进程看到的是fork瞬间的数据快照因为子进程的页表不会改变父进程的写操作会触发页复制每个被修改的页都需要复制一份内存开销取决于写操作的覆盖范围如果BGSAVE期间只有10%的页被修改额外内存开销约为数据量的10%踩坑提示如果你的Redis实例占用了30GB内存BGSAVE期间写操作修改了50%的页那么额外需要约15GB内存。如果系统没有足够的空闲内存就会触发OOM Killer或者swap导致Redis性能急剧下降。建议Redis服务器预留至少30%-50%的可用内存给COW使用。fork大内存实例的注意事项对于大内存Redis实例比如超过10GBfork本身也可能成为问题fork耗时随内存增长虽然fork不需要复制内存但复制页表本身也需要时间。30GB内存的实例fork可能需要数百毫秒。fork期间主进程短暂阻塞fork是在主线程中执行的虽然时间短但对于延迟敏感的应用P99延迟1ms这可能导致偶尔的延迟毛刺。使用THPTransparent Huge Pages会加剧问题THP将4KB页合并为2MB大页减少了页表条目数但fork时需要处理的每个页更大COW的代价更高。# 检查THP状态cat/sys/kernel/mm/transparent_hugepage/enabled# 建议关闭THP对Redis更友好echonever/sys/kernel/mm/transparent_hugepage/enabled自动触发RDB的条件除了手动执行BGSAVERedis还可以根据配置自动触发RDB持久化。save配置参数在redis.conf中save参数定义了自动触发BGSAVE的条件# 默认配置Redis 7.0save36001# 3600秒1小时内有至少1次修改save300100# 300秒5分钟内有至少100次修改save6010000# 60秒1分钟内有至少10000次修改满足任意一个条件就会触发BGSAVE。Redis内部的dirty计数器和lastsave时间戳 │ ▼ ┌──────────────────────────────────────────┐ │ 每次写操作dirty │ │ BGSAVE完成后dirty 0, lastsave now │ │ │ │ serverCron每100ms检查 │ │ ┌──────────────────────────────────┐ │ │ │ if (now - lastsave 3600 │ │ │ │ dirty 1) │ │ │ │ → BGSAVE │ │ │ │ │ │ │ │ if (now - lastsave 300 │ │ │ │ dirty 100) │ │ │ │ → BGSAVE │ │ │ │ │ │ │ │ if (now - lastsave 60 │ │ │ │ dirty 10000) │ │ │ │ → BGSAVE │ │ │ └──────────────────────────────────┘ │ └──────────────────────────────────────────┘踩坑提示如果不想使用RDB持久化可以注释掉所有save行或者设置save 。但请注意这并不意味着数据不会丢失——只是不会自动保存到磁盘。禁用RDB的配置# 方式一redis.conf中save# 方式二运行时配置CONFIG SET save其他触发RDB的时机除了手动和自动触发还有几种场景也会产生RDB触发场景说明DEBUG RELOAD调试命令重新加载Redis时会先保存RDBFLUSHALL清空所有数据库时如果配置了save会先触发RDBRDB文件为空主从复制主服务器执行全量同步时会生成RDB发送给从服务器SHUTDOWNRedis正常关闭时如果配置了save会执行BGSAVE等子进程完成后再退出主从全量同步中的RDB生成 │ ▼ ┌──────────┐ BGSAVE ┌──────────┐ │ 主服务器 │ ──────────► │ 子进程 │ │ │ │ 生成RDB │ │ │ └────┬─────┘ │ │ │ │ │ ┌───────────────┘ │ │ │ 发送RDB文件 │ │ ▼ │ │ ┌──────────┐ │ │ │ 从服务器 │ │ │ │ 接收RDB │ │ │ │ 载入数据 │ │ │ └──────────┘ └──────────┘踩坑提示FLUSHALL命令会生成一个空的RDB文件覆盖之前的RDB备份如果你不小心执行了FLUSHALL而此时自动save已经触发那之前的数据就再也找不回来了。建议在执行FLUSHALL之前先备份dump.rdb文件。RDB文件的位置和命名RDB文件的存储位置由两个配置参数控制# redis.confdir/var/lib/redis# RDB文件存放目录dbfilename dump.rdb# RDB文件名# 查看当前配置127.0.0.1:6379CONFIG GETdir1)dir2)/var/lib/redis127.0.0.1:6379CONFIG GET dbfilename1)dbfilename2)dump.rdb踩坑提示dir参数也影响AOF文件的存储位置。建议将RDB和AOF文件放在有足够磁盘空间的目录并确保Redis进程有写入权限。生产环境建议放在独立的磁盘或SSD上避免和日志等其他IO密集型操作争抢磁盘带宽。RDB文件校验Redis提供了redis-check-rdb工具来检查RDB文件的完整性# 检查RDB文件redis-check-rdb /var/lib/redis/dump.rdb# 输出示例[offset0]Checking RDBfile/var/lib/redis/dump.rdb[offset26]AUX FIELD redis-ver7.0.12[offset40]AUX FIELD redis-bits64[offset13443]Selecting DB ID0[offset13567]CHECKSUM: OK如果RDB文件损坏你可以尝试修复但可能丢失部分数据# 注意修复操作可能丢失损坏部分之后的所有数据# 修复前请先备份原始RDB文件cpdump.rdb dump.rdb.bak redis-check-rdb--fix/var/lib/redis/dump.rdbLASTSAVE命令LASTSAVE命令返回Redis最后一次成功保存RDB的Unix时间戳127.0.0.1:6379LASTSAVE(integer)1687700123# 转换为可读时间# 在bash中date-d1687700123# Mon Jun 26 14:28:43 UTC 2023这个命令在监控中很有用——如果你设置了自动save可以定期检查LASTSAVE是否在合理时间范围内更新# 监控脚本检查RDB是否正常保存#!/bin/bashLASTSAVE$(redis-cli LASTSAVE)NOW$(date%s)DIFF$((NOW-LASTSAVE))if[$DIFF-gt3600];thenechoWARNING: RDB not saved for${DIFF}sfiRDB的优缺点优点恢复速度快RDB是紧凑的二进制文件恢复时直接载入内存速度远快于AOF重放。对于大数据集RDB恢复可能只需要AOF恢复时间的1/10甚至更快。文件紧凑RDB文件经过压缩体积通常比AOF小很多。对性能影响小BGSAVE由子进程完成父进程正常服务。forkCOW的额外开销可控。适合备份RDB文件是一个完整的时间点快照非常适合定期备份到S3等远程存储。缺点数据丢失风险RDB是定时快照两次快照之间的数据修改在Redis崩溃后会丢失。最坏情况下可能丢失最近一个save周期内的所有数据。时间轴 │ ▼ 12:00 RDB保存 ← 此时数据完整保存 │ ▼ 12:05 写入key A 12:10 写入key B 12:15 写入key C │ ▼ 12:18 Redis崩溃 ← key A/B/C 丢失 │ 不在RDB中 ▼ 重启后从RDB恢复 ← 只能恢复到12:00的状态fork大内存实例的开销如前所述大内存实例的fork可能需要较长时间且COW需要额外内存。不适合实时性要求高的场景如果你的业务要求每秒级的数据安全RDB显然不够。RDB优缺点与AOF的初步对比维度RDBAOF持久化方式快照全量日志追加增量数据安全可能丢失一个周期的数据最多丢失1秒默认配置恢复速度快直接载入慢重放所有写命令文件大小紧凑较大重写后可缩小系统开销fork时的短暂阻塞每次写入都追加AOF适用场景备份、灾备、可容忍少量丢失数据安全要求高生产配置建议# redis.conf 生产环境推荐配置# 1. 开启BGSAVE设置合理的save阈值save9001# 15分钟内有1次修改save30010# 5分钟内有10次修改save6010000# 1分钟内有10000次修改# 2. RDB文件名建议包含端口号避免多实例冲突dbfilename dump-6379.rdb# 3. 存储目录使用独立磁盘dir/data/redis/rdb# 4. 启用压缩对List/Set/Hash等大对象使用LZF压缩rdbcompressionyes# 5. 启用RDB文件校验rdbchecksumyes# 6. 关闭THP# 在系统层面执行# echo never /sys/kernel/mm/transparent_hugepage/enabled# 7. 确保系统有足够内存# 建议可用内存 Redis最大内存 * 1.5踩坑提示如果你的Redis实例同时开启了RDB和AOFRedis重启时会优先使用AOF来恢复数据因为AOF的数据更完整。所以即使配置了RDB如果AOF也是开启的RDB更多是作为备份手段存在。总结RDB持久化是Redis最基础的持久化方案SAVE是阻塞型的危险操作生产禁用BGSAVE通过fork子进程实现后台保存fork COW机制让子进程获得内存快照的同时父进程可以继续服务自动触发条件通过save参数配置满足任意一个即触发BGSAVERDB的优点是恢复速度快、文件紧凑缺点是有数据丢失风险大内存实例需要关注fork耗时和COW内存开销了解了RDB是怎么拍的快照下一篇我们就来拆解这张照片本身——RDB文件的格式里到底藏着什么秘密上一篇【第28篇】数据库通知——Redis的事件订阅机制下一篇【第30篇】RDB文件格式完全解析——一个快照文件里藏着什么秘密