Skip to content
← 返回Redis列表
数据与原子性

Redis 持久化:RDB、AOF 与独立恢复 ​

Redis 持久化入门 ​

持久化把 Redis 的数据状态保存到磁盘,用于重启后的恢复。先理解“能恢复到什么时候”,再考虑配置参数。

RDB 与 AOF ​

方式保存什么恢复时需要注意
RDB某个时刻的数据集快照快照之后的更新不在这份快照里
AOF用于重建状态的写操作记录丢失窗口与同步策略、操作系统和存储故障有关

RDB 适合定期保存和搬迁数据集。AOF 可以降低相邻快照之间的数据丢失风险,但也会增加写入、存储和维护成本。两者可以配合使用;是否启用应结合数据用途和恢复要求决定。

需要认识的命令与配置 ​

BGSAVE 请求后台生成快照,INFO persistence 查看持久化状态。AOF 相关配置包括 appendonly 和 appendfsync;常见的 everysec 策略不能承诺零数据丢失。后台生成文件也可能带来进程创建、内存和磁盘压力。

新手可以先查询状态,再到隔离环境练习。不要为了看结果直接在业务实例运行阻塞式 SAVE,也不要覆盖仅存的数据文件。

持久化、复制与备份各解决什么 ​

  • 持久化:让本实例重启后能够恢复状态。
  • 复制:把数据传播到副本,用于读扩展或故障切换等场景;误删也可能被复制。
  • 备份:保留独立副本,并通过恢复演练确认可用。

数据文件保存在容器内部,并不等于重建容器后仍可访问。需要确认实际数据目录、持久卷、备份位置和恢复流程。

继续阅读:持久化、独立恢复与运行验证。参考:Redis 官方持久化文档。

Redis 持久化、独立恢复与运行验证 ​

本轮仅全文静态审计,未安装依赖、启动服务、连接数据库或执行任何示例。下文实测、RED/GREEN及平台结果均为此前历史证据;本轮修改后的脚本未运行,命令与输出作为学习步骤和预期保留。

RDB 与 AOF 的恢复边界 ​

前置:容器与数据卷。RDB保存某时刻数据集快照,适合备份与搬迁;快照以后写入不在其中。BGSAVE在后台子进程生成,fork与写时复制仍有暂停和内存代价。SAVE在前台阻塞,仅用于本页小型隔离实验;大实例不能照搬。

AOF记录写操作并按配置同步磁盘;appendfsync everysec通常约每秒fsync,故障可能丢最近约一秒或受系统条件影响更多的数据,不承诺零丢失。重写把历史压缩为等效状态,不是恢复到任意历史时刻的审计日志。现代Redis使用多段AOF与manifest,不要仅复制一个旧式appendonly.aof就认为备份完整。RDB与AOF同时启用时通常优先AOF恢复;恢复RDB时应明确关闭目标AOF,避免空AOF掩盖导入结果。

复制、持久化、备份分别应对不同故障:副本也会复制误删;本机volume不能抵御宿主磁盘损毁;备份必须独立存储、限制访问、定期演练恢复。Redis保存过期时间的绝对时间,恢复已到期键不会获得一个全新TTL。限额缓存可因驱逐、恢复时效和故障切换丢失,不能把恢复成功推导为强一致账本。

完整恢复实验与配置 ​

Redis 的 RDB 独立恢复实验在源实例写入唯一 m4:verify:{UUID} 标记、SAVE,然后复制dump.rdb到临时目录;新建 showdocs-m4-redis-restore-<随机后缀> 容器与独立数据卷,将快照导入 /data/dump.rdb,关闭AOF并启动。目标不映射主机端口,使用docker exec查询;断言GET得到restore-proof才算恢复通过。不会向已有服务导入或FLUSHALL。

手动理解流程(后文脚本是完整可复跑实现,随机目标名避免碰撞):

sh
docker exec showdocs-m4-redis-main redis-cli -e SET m4:restore-manual restore-proof
docker exec showdocs-m4-redis-main redis-cli -e SAVE
docker cp showdocs-m4-redis-main:/data/dump.rdb ./dump.rdb
docker create --name showdocs-m4-redis-restore-manual -v showdocs-m4-redis-restore-manual-data:/data redis:8.2.10-alpine redis-server --appendonly no --save ""
docker cp ./dump.rdb showdocs-m4-redis-restore-manual:/data/dump.rdb
docker start showdocs-m4-redis-restore-manual
docker exec showdocs-m4-redis-restore-manual redis-cli -e GET m4:restore-manual

最后预期restore-proof;如果刚启动未ready,先PING再GET。检查目标日志、镜像版本、数据目录权限与dump文件,不要在源实例反复覆盖。手动命令会在当前目录留下dump.rdb,注意其包含整个实验实例数据。本课程verify脚本用系统临时目录并在成功后删除该临时副本。

AOF实验另外确认 CONFIG GET appendonly 是yes,写入aof-proof后 docker restart showdocs-m4-redis-main,再GET相同键。此处验证的是正常重启保留状态,并非模拟突然断电或验证每秒fsync的最坏数据损失。

可复跑验证器与结果 ​

依赖Node.js >=22与已启动的独立Redis容器;无需npm install。在仓库根目录运行:

sh
node examples/redis/verify.mjs
javascript
import {spawnSync} from 'node:child_process';
import {writeFileSync,mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {randomUUID} from 'node:crypto';
import assert from 'node:assert/strict';
import {command} from './quota.mjs';
const results={date:new Date().toISOString(),platform:process.platform,node:process.version,commands:[],checks:[],containers:['showdocs-m4-redis-main']};
function run(program,args){
 const r=spawnSync(program,args,{encoding:'utf8',timeout:120000});
 results.commands.push({program,args,exitCode:r.status,stdout:r.stdout,stderr:r.stderr});
 if(r.status!==0)throw new Error(r.stderr||r.stdout||String(r.error));
 return r.stdout.trim();
}
async function check(name,action){await action();results.checks.push({name,passed:true});}
const main='showdocs-m4-redis-main';
async function waitReady(container){
 for(let i=0;i<40;i++){
  const ping=spawnSync('docker',['exec',container,'redis-cli','-e','--raw','PING'],{encoding:'utf8',timeout:10000});
  if(ping.status===0&&ping.stdout.trim()==='PONG')return;
  await new Promise(r=>setTimeout(r,250));
 }
 throw new Error(`${container} did not become ready`);
}
const k=`m4:verify:{${randomUUID()}}`;
try {
 if(process.env.REDIS_CONTAINER&&process.env.REDIS_CONTAINER!==main)throw new Error('verify requires showdocs-m4-redis-main; custom containers are supported only by quota CLI/tests');
 results.image=JSON.parse(run('docker',['image','inspect','redis:8.2.10-alpine','--format','{{json .RepoDigests}}']));
 results.server=run('docker',['exec',main,'redis-server','--version']);
 results.client=run('docker',['exec',main,'redis-cli','--version']);
 await check('real service PING',async()=>assert.equal(await command('PING'),'PONG'));
 const race=spawnSync(process.execPath,['examples/redis/race-negative.mjs'],{encoding:'utf8'});
 results.commands.push({program:process.execPath,args:['examples/redis/race-negative.mjs'],exitCode:race.status,stdout:race.stdout,stderr:race.stderr,expectedExitCode:1});
 assert.equal(race.status,1);assert.match(race.stdout,/"actual":1/);
 run(process.execPath,['--test','examples/redis/quota.test.mjs']);
 await check('diagnostic commands return real service state',async()=>{
  for(const args of [['INFO','commandstats'],['SLOWLOG','LEN'],['SLOWLOG','GET','5'],['LATENCY','LATEST'],['INFO','memory'],['INFO','stats'],['INFO','persistence']])run('docker',['exec',main,'redis-cli','-e','--json',...args]);
 });
 await check('string TTL replacement and missing key',async()=>{
  await command('SET',k,'x','PX',30000);assert.ok(await command('PTTL',k)>0);
  await command('SET',k,'y');assert.equal(await command('TTL',k),-1);
  await command('DEL',k);assert.equal(await command('TTL',k),-2);
 });
 await check('hash list set sorted set',async()=>{
  await command('HSET',k,'name','Ada');assert.equal(await command('HGET',k,'name'),'Ada');await command('DEL',k);
  await command('RPUSH',k,'a','b');assert.equal(await command('LPOP',k),'a');await command('DEL',k);
  assert.equal(await command('SADD',k,'a','a'),1);await command('DEL',k);
  await command('ZADD',k,20,'b',10,'a');assert.deepEqual(await command('ZRANGE',k,0,-1),['a','b']);await command('DEL',k);
 });
 await check('independent RDB import and query',async()=>{
  await command('SET',k,'restore-proof');await command('SAVE');
  const temp=mkdtempSync(join(tmpdir(),'showdocs-m4-redis-'));
  const restore=`showdocs-m4-redis-restore-${randomUUID().slice(0,8)}`;
  const volume=`${restore}-data`;results.containers.push(restore);results.restoreVolume=volume;
  run('docker',['cp',`${main}:/data/dump.rdb`,join(temp,'dump.rdb')]);
  run('docker',['create','--name',restore,'-v',`${volume}:/data`,'redis:8.2.10-alpine','redis-server','--appendonly','no','--save','']);
  run('docker',['cp',join(temp,'dump.rdb'),`${restore}:/data/dump.rdb`]);
  run('docker',['start',restore]);
  await waitReady(restore);
  assert.equal(run('docker',['exec',restore,'redis-cli','-e','--raw','GET',k]),'restore-proof');
  results.restoredKey=k;rmSync(temp,{recursive:true});
  run('docker',['stop',restore]);
  const down=spawnSync('docker',['exec',restore,'redis-cli','-e','PING'],{encoding:'utf8'});
  results.commands.push({program:'docker',args:['exec',restore,'redis-cli','-e','PING'],exitCode:down.status,stdout:down.stdout,stderr:down.stderr,expectedNonzero:true});
  assert.notEqual(down.status,0,'stopped service must fail, never report cache success');
  run('docker',['start',restore]);
 });
 await check('AOF enabled and restart retains value',async()=>{
  assert.deepEqual(await command('CONFIG','GET','appendonly'),{appendonly:'yes'});
  await command('SET',`${k}:aof`,'aof-proof');
  run('docker',['restart',main]);
  await waitReady(main);
  assert.equal(await command('GET',`${k}:aof`),'aof-proof');
 });
 const failure=spawnSync(process.execPath,['examples/redis/quota.mjs','negative','r','-1'],{encoding:'utf8'});
 results.commands.push({program:process.execPath,args:['examples/redis/quota.mjs','negative','r','-1'],exitCode:failure.status,stdout:failure.stdout,stderr:failure.stderr,expectedExitCode:1});
 assert.equal(failure.status,1);
 results.passed=true;
} catch(error){results.passed=false;results.error=error.stack;process.exitCode=1;}
writeFileSync('examples/redis/verification-results.json',JSON.stringify(results,null,2)+'\n');
console.log(JSON.stringify(results,null,2));

脚本记录Node、server、client版本、镜像摘要、命令参数、退出码、stdout/stderr及断言结果到 examples/redis/verification-results.json(重跑覆盖此文件)。此前历史证据保留于 migration/knowledge-rebuild/m4/redis/results.json,当前验证器不再覆盖它。验证器固定操作showdocs-m4-redis-main,REDIS_CONTAINER若另有所指会在服务操作前拒绝,避免混用两个实例。任何异常使进程非零退出,绝不以skip代替通过。每次运行会新增一个独立恢复容器/volume,名称写在results里;脚本不会自动删除复盘证据。复跑前确认容量足够,完成后只删除自己这次创建的恢复容器和对应volume。主容器数据卷可以保留以继续练习。

Windows PowerShell驱动Node与Docker,实际Redis运行Linux;Node标准库与官方CLI版本分开记录。此前7项测试与恢复记录均来自真实服务操作;这些历史结果不验证本轮脚本修改,构建通过只证明文档可发布,不能替代这些结果。

权限、容量与运行维护 ​

远程服务将访问控制与加密分开考虑:ACL按用户限制命令和键前缀,TLS保护传输,监听地址、防火墙限制可达范围;密码不是流量加密。应用不应拥有CONFIG、DEBUG、FLUSHALL等管理命令。不要把带密码的URI写进命令历史或提交仓库。

容量应同时看used_memory、maxmemory、碎片、连接、复制缓冲与fork额外内存。设置maxmemory并评估淘汰策略:allkeys-lru会从所有键淘汰,volatile策略只选择带过期键,noeviction会拒绝相关写入。限额状态若与可淘汰缓存混用会提前放行,因此根据容忍度隔离实例和内存预算;单靠键前缀不能隔离内存。

变更前保存配置与备份;上线新脚本先校验类型和版本,观察延迟和错误,准备恢复前一版脚本。备份文件同样包含业务数据,应加密、限制访问并安排保留周期。本页未覆盖Cluster/Sentinel选举、跨机灾难恢复或滚动升级,相关历史专题继续保留其原边界。

练习与参考解法 ​

提示:先标出快照时刻、键的到期时刻和实际挂载位置。

题目:备份后再写一个键,把旧dump导入独立目标,哪个键应存在?AOF开启但删掉原容器后数据不见了,先检查什么?

参考解法

快照时已存在且恢复时未过期的键应存在,快照以后新增键不应存在。先核对新旧容器是否挂载同一预期数据卷、实际dir/dbfilename、AOF目录和配置,而不是先怀疑SET失败。对照目标日志与持久化INFO;不要重建覆盖仅存的备份。

资料:官方持久化机制、官方配置样例、Docker官方镜像。

Redis 容器重启、挂载与健康的边界 ​

Redis 容器重启策略为什么不能证明服务健康?restart策略处理进程退出或容器生命周期,不证明客户端可认证、命令可读写、持久化目录可写。no、on-failure、always表达不同重启条件,应结合实际管理方式检查;容器反复重启时先看退出原因和日志,不能用自动重启掩盖配置错误。

Redis作为容器主进程需要保持前台运行;容器以-d脱离终端,与redis.conf中的daemonize后台化是两件事。把主进程再次后台化可能让容器退出,因此不能将旧服务器守护进程习惯直接搬到容器中。启动参数应明确加载哪个配置文件,不能只因宿主文件存在就认定已生效。

配置文件与数据目录的bind mount提供宿主和容器之间的挂载视图,不是把配置复制进镜像;路径类型、权限和读写方向必须核对。数据目录持久保存与容器存活分开,重建容器时挂错目录仍可能得到空数据;只看到AOF开启也不能证明备份完整或恢复成功。

撤回旧redis:latest、示例弱密码以及任意接口端口映射作为可直接复制的部署配方;教学与部署需明确版本、凭据来源和网络范围。旧文“挂载会复制文件”的表述撤回;不能从旧文的重启配置推断服务可用。当前课程的固定版本与隔离绑定保持原范围,本次没有拉镜像、启动容器或验证历史部署。

继续阅读