qinghuan
← 数据系统 / 全部笔记

单元 76 / 数据系统

P1 · 重点提升L2 · 进阶

Redis 持久化与恢复:备份能不能真的用起来

按允许丢多少数据、多久恢复来比较 RDB、AOF 和副本,再在隔离环境核对恢复的值与有效期。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能说明不同故障下可能丢失哪些数据,并制定包含 TTL 的恢复核对步骤。

学习理由、准备要求与知识点

为什么学它

主从与持久化都不是零丢失承诺;恢复演练决定备份是否真的能支撑业务。

先读这些内容

知识点清单

  • RDB 快照
  • AOF 追加
  • fsync 策略
  • 恢复顺序
  • 异步复制
  • 复制延迟
  • 故障切换
  • 隔离恢复校验

运行前先确认Redis 7.4;Java 客户端源码固定 Jedis 5.2.0。完整 Java 17 程序模拟快照与日志的恢复语义,不读取或生成 Redis RDB/AOF 格式。最新文档中的新增备份命令不自动适用于 7.4。

01先弄懂它是什么

讨论 Redis 恢复前,先回答业务问题:最多允许丢多少已接受的数据,多久要恢复服务,哪些内容能从数据库重建?商户缓存可以重新加载,登录态可以让用户重新登录,但尚未保存在别处的订单事件可能再也补不回。不能把所有 Redis 数据都当成随时可以丢弃的缓存。

RDB 保存某个时间点的数据状态,AOF 记录可用于重建数据的写入历史,复制则把主节点变化发给副本。它们应对的问题不同:副本能接管服务,但误删也可能被同步过去;同一主机上的备份挡不住整台主机或磁盘丢失。看到文件存在、复制连接正常,都还不能证明恢复后的数据正确,需要实际恢复检查。

恢复数据时,还要记住它原本什么时候到期。例如登录态原定一小时后失效,停机两小时再恢复,就不应重新给它一小时。TTL 只是“查询那一刻还剩多久”;恢复快照和日志时,需要考虑原来定下的到期时刻。验收时也不能只数键的数量,还要抽样检查值、类型和有效期,并核对业务上不能重复的记录与重要计数。

文中这几个词,先认一认

RPO
允许丢失多长时间内的数据变更,例如最多接受恢复到故障前某个时间点。
RTO
发生故障后,允许花多长时间恢复服务,包括发现、处理和验证的时间。
复制确认
副本报告已处理相应写入,但这不自动保证任何故障下都不会丢数据。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

先在独立练习实例读取 INFO persistence、INFO replication,以及有权限时的 CONFIG GET appendonly、CONFIG GET appendfsync 和 CONFIG GET save。

记录实际角色、持久化策略及最近错误,再按对应版本方法保存完整备份。Redis 7 的 AOF 可以由基文件、增量文件及清单组成,不能只凭旧教程复制一个同名文件就推断备份完整;目录和文件集合应以当前配置与版本说明为准。

主节点通常不会等副本全部跟上才完成写入,这就是异步复制。WAIT 可以等待“当前连接之前的写入”获得指定数量的副本确认;等到超时,也会返回实际确认了多少个,应用要检查这个数。确认不够,不表示原写入已撤销。Redis 7.2 起提供的 WAITAOF 还可等待 AOF 刷盘确认,但仍要核对由哪些节点确认、配置怎样设置,以及故障切换会选谁。若通信中断导致结果未知,不能直接再做一次扣减。

一份可信的恢复记录至少区分备份时间点、恢复启动时间与验证完成时间。恢复到隔离实例后,先禁用真实消费者入口,核对关键数据与过期状态,再讨论流量切换。下面的教学程序将快照序号、连续日志和绝对过期时间组合起来;它故意拒绝缺口与未达清单终点的输入,让恢复缺失成为可见失败,而不是悄悄宣布成功。

正在加载流程图…

备份完成只是中间状态;可恢复性需要读取与业务验证。
查看流程图源码(Mermaid)
flowchart TD
A["定义数据重要性 RPO RTO"] --> B["记录持久化与复制状态"]
B --> C["保存完整备份及校验清单"]
C --> D["恢复到隔离实例"]
D --> E["验证值 类型 TTL 与业务不变量"]
E --> F["评估切换和后续对账"]

按顺序看,每一步做了什么

  1. 列出可以重建、可以失效和必须恢复的数据。
  2. 记录当前版本、角色、持久化参数和备份文件集合。
  3. 在不同故障域保存备份与必要元信息,验证完整性。
  4. 用隔离恢复测量 RPO/RTO,并核对过期与业务不变量。

03对照实现代码

Jedis 的 WAIT 只发送命令并读取实际确认数量 (新标签页)↗

Jedis v5.2.0 · Jedis#waitReplicas(int,long);Jedis#waitAOF(long,long,long)

以下是原创客户端调用契约模型,强调同一连接与确认不足的结果;不是 Jedis 源码节选,也不是 Redis 持久化引擎实现。Wire 接口没有网络实现。

核心机制代码 · java
class SourceModel {
  interface Wire {
    void set(String key, String value);
    long waitReplicas(int replicas, long timeoutMillis);
  }
  enum Result { CONFIRMED_THRESHOLD, WRITTEN_BUT_THRESHOLD_NOT_MET }
  static Result write(Wire sameConnection, String key, String value) {
    sameConnection.set(key, value); // 写入和等待必须属于同一连接的上下文
    long acknowledged = sameConnection.waitReplicas(1, 500);
    if (acknowledged >= 1) return Result.CONFIRMED_THRESHOLD; // 检查返回值而非只看无异常
    return Result.WRITTEN_BUT_THRESHOLD_NOT_MET; // 不足不会撤销已发送写入
  }
}

真实 waitReplicas 先检查当前调用模式,再通过持有的 connection 发送 WAIT,随后读取整数响应。Java 客户端没有在此替服务端完成磁盘刷写。

waitAOF 发送 WAITAOF 并把响应转换为两个计数,不能将其误读为一个无需检查的成功布尔值。

相关确认针对同一连接上的写入,因此写入和确认要使用正确关联的连接。若每一步都各自向连接池借用,可能已经换成不同连接,应采用客户端提供的同一连接使用方式。

模型仅返回已知响应的两种情况;连接异常应由调用者保留为结果未知,并使用稳定业务标识查询或对账,不能转成确定失败后盲重试。

代码先调用谁,再调用谁

  1. 业务写入使用固定连接
  2. Jedis.waitReplicas
  3. connection.sendCommand(WAIT)
  4. connection.getIntegerReply
  5. 业务检查确认数量

04动手写一个例子

带清单终点与绝对有效期的快照日志恢复

从序号 base 的快照开始,严格按连续序号重放 SET/DEL 记录,要求最终到达备份清单 last。恢复时刻 now 已过期的键应消失,不能重新开始 TTL。此格式为自定义教学事件,不是 Redis AOF。

输入 1
快照k=old;日志SET k=new,到期90;恢复时刻100
输出 1
k不存在,旧值也不能复活
输入 2
base=5,last=7;只提供序号6
输出 2
拒绝:日志尾部缺失

先复制快照到临时状态,确保任何验证失败都不会修改调用者提供的快照。

逐条验证下一序号恰好加一,再覆盖值或删除键;已经在恢复时刻过期的 SET 也要参与覆盖,最后统一过滤。

重放终点必须匹配清单。清单只声明备份自身覆盖范围,不能证明故障前未进入备份的所有写入都已保存。

为什么这样写是对的处理到序号 s 时,临时状态精确表示快照与 base+1 到 s 的有序变更;对外返回前,所有有限到期点都严格晚于 now。

时间 平均 O(k+n)额外空间 O(k+n)

复制 k 个快照键,处理 n 条记录并遍历最终键集;哈希表平均单次更新为常数,字符串内容存储成本另计。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 46 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;

public class Main {
  record Value(String text, long expiresAt) {
    Value {
      Objects.requireNonNull(text);
      if (expiresAt < -1) throw new IllegalArgumentException("expiry");
    }
  }
  record Event(long sequence, String key, Value value) {} // null 值明确表示删除事件
  static Map<String, Value> restore(long base, long last, Map<String, Value> snapshot,
      List<Event> events, long now) {
    if (base < 0 || last < base || now < 0) throw new IllegalArgumentException("range");
    Map<String, Value> state = new HashMap<>(snapshot); // 验证失败不污染输入快照
    long current = base;
    for (Event event : events) {
      Objects.requireNonNull(event.key());
      if (event.sequence() != Math.addExact(current, 1))
        throw new IllegalArgumentException("gap or duplicate");
      current = event.sequence();
      if (event.value() == null) state.remove(event.key());
      else state.put(event.key(), event.value()); // 先覆盖旧值,不能跳过已到期的新值
    }
    if (current != last) throw new IllegalArgumentException("manifest mismatch");
    state.values().removeIf(v -> v.expiresAt() != -1 && v.expiresAt() <= now);
    return Map.copyOf(state); // 返回恢复时仍有效的独立状态
  }
  static Value forever(String s) { return new Value(s, -1); }
  static void check(boolean ok) { if (!ok) throw new AssertionError(); }
  static void bad(Runnable action) {
    try { action.run(); } catch (IllegalArgumentException expected) { return; }
    throw new AssertionError("expected rejection");
  }
  public static void main(String[] args) {
    Map<String, Value> base = Map.of("k", forever("old"));
    check(restore(5,5,base,List.of(),100).get("k").text().equals("old"));
    check(restore(5,6,base,List.of(new Event(6,"k",new Value("new",90))),100).isEmpty());
    check(restore(5,6,base,List.of(new Event(6,"k",null)),100).isEmpty());
    check(restore(5,6,base,List.of(new Event(6,"x",new Value("live",101))),100).size()==2);
    check(restore(5,6,base,List.of(new Event(6,"k",new Value("edge",100))),100).isEmpty());
    bad(() -> restore(5,7,base,List.of(new Event(7,"x",forever("v"))),100));
    bad(() -> restore(5,7,base,List.of(new Event(6,"x",forever("v"))),100));
    check(base.get("k").text().equals("old")); // 重放成功或失败都不改变原始快照
    System.out.println("8 recovery checks passed");
  }
}

这些特殊情况也要试一试

  • expiresAt=-1 表示永久键,到期点等于恢复时刻则视为过期。
  • 日志重复、缺口与尾部缺失都拒绝,实际格式还需校验损坏与事务边界。
  • 恢复时钟不准确会改变到期判断;真实环境需要可靠时间来源。
进阶选读 · 生产拓展:大规模恢复的校验与断点

场景数据量大,完整恢复需要数小时,希望提前发现损坏并在中断后继续。

做法用带格式版本的清单写明备份分成哪些块、每块覆盖哪些序号,以及用于检查内容的摘要。每块校验通过后,记录一个只读的已完成位置,便于中断后继续。全部恢复后,再按同一个目标时间点核对业务数据。哈希摘要相符,只能说明内容与清单一致;清单本身也必须可靠保存。

复杂度顺序读写与摘要计算为 O(B),B 为备份字节数;额外内存可按分块大小限制。

好处与代价断点降低重做成本,但格式升级、过期语义和跨分片业务一致性必须纳入协议。

05项目里怎么用

备份除了数据,还要保留配置、版本和完整文件集合,并放在不会随原实例一起失效的位置。everysec 描述刷盘策略,不是对所有硬件和系统故障都保证最多丢一秒;操作系统缓存、存储损坏和副本切换都可能影响结果。不能接受丢失的业务事实,还应评估更合适的存储与确认方式。

恢复演练先创建永久键、Hash 和短 TTL 键,保存备份后再更改其中一部分,恢复到另一个实例核对时间点差异。明确恢复端口、网络隔离和消费者配置,再记录加载时间、键值摘要、剩余 TTL 及业务查询结果。只验证 PING 成功说明服务可访问,不能证明数据符合预期。

AOF 重写、生成快照和复制全量同步会竞争 CPU、内存与 I/O,应预留容量并监控失败状态。发现文件异常时先保留副本和证据,再按版本工具与恢复策略处理;直接修复唯一文件会减少回退选择。恢复完成后仍需对账补偿备份窗口内未覆盖的业务变更。

容易踩的坑

  • 将副本当成能自动回到误删前的历史备份。
  • WAIT 返回不足就认为写入已回滚。
  • 恢复时把原剩余 TTL 重新完整设置,延长旧会话寿命。
  • 只拷贝一个 AOF 文件,未核对版本对应的清单与文件集合。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
RDB 时间点备份便于保留历史状态和搬运快照之间的变更可能不在恢复范围可接受明确数据窗口的备份
AOF 与合适刷盘策略可缩小部分故障的数据损失窗口增加写入和恢复管理成本需要较细恢复粒度的状态
复制加独立备份同时考虑可用性与历史恢复需要多个故障域和定期演练承担重要业务状态的部署

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01复制为什么不能替代备份?

展开参考答案与推导

复制通常传播当前变更,误删也会传播;独立历史备份保留先前状态。

02WAIT 确认的是哪一批写入?

展开参考答案与推导

当前连接此前的写入;需要检查实际副本确认数,不能把连接池中别的连接写入混为同一上下文。

03RPO 与 RTO 有什么区别?

展开参考答案与推导

前者描述可接受的数据损失时间窗口,后者描述可接受的服务恢复时长。

换个情况,还会处理吗

01恢复后的短期登录态重新获得完整 TTL,有什么问题?

展开参考答案与推导

恢复延长了原凭证有效期,可能重新启用本应失效的会话;应按绝对到期语义验证。

02WAIT 超时返回零,能再做一次 INCR 吗?

展开参考答案与推导

不能据此判断原 INCR 未执行。应通过业务幂等标识或对账确定结果,避免重复增加。

03备份可以加载但订单计数不一致,算恢复成功吗?

展开参考答案与推导

只能说明格式可读,业务不变量尚未满足;需定位恢复窗口与缺失数据后再决定切换。

写代码试一试

01旧永久值之后被一个已过期的新值覆盖,恢复保留哪个?

输入 / 条件
old永久;SET new到期90;now100
输出 / 预期
键不存在

提示 不能先跳过过期事件。

展开参考答案与推导

先应用覆盖再过滤过期,否则错误保留 old,形成数据复活。

02base为8,日志首条为10,如何处理?

输入 / 条件
8→10
输出 / 预期
拒绝缺口

提示 预期下一序号为9。

展开参考答案与推导

程序验证连续性,缺少9时不能知道遗漏操作对状态的影响。

03清单last为9而日志只到8,是否返回部分状态?

输入 / 条件
预期9,实际8
输出 / 预期
拒绝清单不匹配

提示 区分部分恢复与完整恢复。

展开参考答案与推导

本题契约只返回完整结果,部分恢复应有独立状态和显式审批流程,不能默认为成功。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能说明不同故障下可能丢失哪些数据,并制定包含 TTL 的恢复核对步骤。

本篇重点加练

持久化策略只能形成恢复假设,只有隔离恢复和业务校验才能证明材料实际可用。

  • fsync 策略
  • 隔离恢复校验

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    本地实例先保存一批商品缓存,再连续修改少量键。比较 RDB 和 AOF 中恢复材料表达的内容,并列出判断材料是否完整所需信息。

    提交内容
    恢复材料清单
    怎样算完成
    • 区分快照时点与操作序列
    • 包含配置及版本

    练到的知识点RDB 快照 · AOF 追加

  2. 作业 02代码推演

    AOF 有两种 fsync 配置,实例在接收写入、写入文件缓冲和持久化后分别退出。画出可能丢失窗口,并分析同时存在 RDB/AOF 时要核查的启动配置。

    提交内容
    故障时间线与核查表
    怎样算完成
    • 不把返回成功等同于落盘
    • 注明实际恢复使用材料

    练到的知识点fsync 策略 · 恢复顺序

  3. 作业 03原理问答

    主机掉电、进程退出和客户端断连对写入证据的影响不同。建立 fsync 时点与这三类故障的关系,列出需要注明的操作系统假设。

    提交内容
    故障与证据矩阵
    怎样算完成
    • 区分内存、缓冲与稳定存储
    • 不混淆传输确认和持久化

    练到的知识点fsync 策略

综合应用

  1. 作业 04面试场景

    副本落后主节点一段时间,运维仅看到连接仍在线。定义复制延迟的观测口径及不同业务读副本时的容忍条件。

    提交内容
    指标字典与业务分级
    怎样算完成
    • 区分连接状态和数据进度
    • 包含写后立即读场景

    练到的知识点异步复制 · 复制延迟

  2. 作业 05设计评审

    需要验证一次故障切换后的业务数据,手头有备份和最近操作日志。制定隔离恢复过程,说明如何证明恢复结果达到目标。

    提交内容
    恢复检查单与验收口径
    怎样算完成
    • 不覆盖现有实例
    • 检查关键数据和 TTL 而非只看启动成功

    练到的知识点故障切换 · 隔离恢复校验

  3. 作业 06排错实验

    用独立本地 Redis 实例生成小备份,复制到新建恢复目录启动另一实例;对预先记录的键、类型、值和有效期进行校验。

    提交内容
    命令、清单与恢复报告
    怎样算完成
    • 源实例和目标端口清楚
    • 注明实验未验证的真实故障与复制场景

    练到的知识点RDB 快照 · AOF 追加 · fsync 策略 · 恢复顺序 · 异步复制 · 复制延迟 · 故障切换 · 隔离恢复校验

  4. 作业 07设计评审

    备份文件可加载但来自不同时刻的配置、应用版本与数据。制定恢复材料的配套清单,说明业务校验应覆盖哪些格式假设。

    提交内容
    恢复清单与校验项目
    怎样算完成
    • 包含 TTL 时间口径
    • 材料版本能够相互对应

    练到的知识点隔离恢复校验

  5. 作业 08排错实验

    在本地生成两个小型备份副本,模拟清单缺项或文件截断,只在新建临时恢复实例验证;记录失败阶段与可用诊断信息。

    提交内容
    实验记录与材料校验器
    怎样算完成
    • 不损坏原始备份
    • 无法模拟掉电的范围明确标注

    练到的知识点fsync 策略 · 隔离恢复校验

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    业务要求最大丢失一分钟数据、十分钟恢复服务。估算备份大小、传输、加载和校验耗时,并制定可重复演练计划。

    提交内容
    预算表与演练记录模板
    怎样算完成
    • 用测量支持时间预算
    • 区分目标承诺和已验证能力

    练到的知识点RDB 快照 · fsync 策略 · 异步复制 · 故障切换 · 隔离恢复校验

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    如果恢复目标是“恢复到指定分钟之前的业务状态”,评审现有 RDB/AOF 材料是否包含足够时间与业务边界信息。

    提交内容
    可恢复性研究提案
    怎样算完成
    • 区分加载某状态和时间点恢复
    • 列出缺失信息而不编造能力

    练到的知识点fsync 策略 · 隔离恢复校验

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗