qinghuan
← 让长任务能暂停和恢复 / 全部笔记

单元 25 / 让长任务能暂停和恢复

P0 · 必会主线L2 · 进阶

让执行能重启、重试与等待确认

保存长任务进度,防止恢复时重复操作,并让用户确认对应到具体内容和版本。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能在模拟崩溃后恢复任务、拒绝过期确认,并核对实际写入结果,确认测试场景中没有重复操作。

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

为什么学它

长任务会等待确认,也可能中途崩溃。恢复时必须知道哪一步会重新运行,哪些外部写入可能已经完成。

先读这些内容

知识点清单

  • 持久检查点
  • 内存状态与进程重启
  • 节点重放
  • 本地事务与远程副作用
  • 确认绑定操作版本
  • 暂停状态落库
  • 稳定幂等键
  • 超时结果未知

运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。

01先弄懂它是什么

保存进度,不代表操作只做一次

假设一个 Agent 要取消测试订单 A。远端已经取消成功,但回复在网络中丢失,本地还没来得及记录结果。

这时重启应用,只能恢复“之前准备取消订单 A”这条记录,不能凭这条记录断定取消成功;没有核对结果,也没有可靠防重依据时,不应直接重试。

正确的中间状态是“结果待核对”。先查实际结果,再决定是否继续;查不清时,明确保留未知状态。

三件事分别解决三个问题

机制它回答的问题它不能单独保证什么
持久化任务状态任务退出前进行到哪一步?远端动作一定成功,或只执行一次
批准绑定参数用户究竟同意了什么操作?旧批准仍适用于变更后的参数
业务防重与结果核对同一动作是否已经生效?任意外部系统都支持安全重试

批准的是具体内容

实现时,可以按下面的设计核对批准:

  1. 展示并保存操作类型、资源、参数摘要和版本,例如“取消订单 A,版本 v1”。
  2. 保存批准者、批准时间和对应版本。
  3. 执行前再次核对身份、权限、参数和版本。
  4. 如果目标由订单 A 改成订单 B,重新确认,不能沿用旧批准。

暂停位置、待确认内容和批准记录都需要存到持久层。页面曾经点过一个按钮,不足以证明重启后这次操作仍获许可。

恢复可能从一个节点的开头重跑

框架不一定能从退出时的那一行 Java 代码接着执行。若节点在暂停前已经创建远程记录,重跑整个节点可能再次创建。

设计恢复点前,先分清:

  • 纯计算:通常可以重新计算。
  • 本地状态修改:按自己的事务和日志规则恢复。
  • 远端写操作:使用稳定业务键,并由执行方按约定实际去重;结果查询用来确认状态。仅生成一个键,或仅查询过一次,都不能保证操作不会重复。不清楚时进入待核对。

这里说的副作用,就是计算结果以外已经发生的状态变化,例如订单被取消、远程记录被创建。

后面的 Java 程序只演示内存日志的连续性检查,并没有实现这套完整业务流程。

文中这几个词,先认一认

检查点
把任务进度保存为可恢复的快照或日志位置,并注明它对应的状态版本。
重放
根据保存下来的状态重新执行部分流程,因此同一段代码可能再次运行,外部操作需另行防重。
确认绑定
用户的批准对应当时展示的具体操作和版本,关键内容改变后要重新判断原批准是否仍适用。

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

02看看它怎么工作

恢复时先检查记录能不能连起来。本题给每条日志一个递增顺序号,再根据内容计算 CRC 校验值,用于发现意外损坏。读取时从第一条开始逐条核对,遇到缺号或校验失败就停下。例如第一条好、第二条坏、第三条看似正常,也只接受第一条:第三步可能依赖第二步的结果,跳过中间记录就失去了完整的前后依据。这段从开头起连续通过检查的记录,就是“连续有效前缀”。

CRC用于发现意外损坏,不防恶意篡改。校验包含顺序号与载荷,防止只校验内容而忽略位置;真实日志还需要长度边界、格式版本和提交标记。算法只处理内存记录,不执行磁盘写入,也不声称复现数据库预写日志。

恢复任务后,执行器还要核对这个动作以前做过没有,以及当前内容是否仍对应获批版本。“先保存准备做什么、再调用远端、最后保存结果”这三步之间,都可能发生故障。如果远端支持按同一业务键查询结果或防重重试,就能进一步查清真实状态。单靠本地数据库事务,无法把任意远程系统的操作也一起变成不可分割的一步。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart TD
  P["操作计划与版本"] --> C["持久待确认状态"]
  C --> A["批准绑定具体内容"]
  A --> I["保存操作意图和幂等键"]
  I --> E["外部执行"]
  E --> R["记录已知结果"]
  E --> U["未知结果待核对"]
  U --> R

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

  1. 把纯计算、状态提交和外部副作用分开。
  2. 保存带版本的任务状态、操作意图与待确认内容。
  3. 确认绑定具体参数与操作版本后再执行。
  4. 重启时验证检查点或日志连续性,恢复有效状态。
  5. 对未决副作用查询或幂等核对,明确完成、失败或结果未知。

03对照实现代码

OpenJDK 17 FileChannel 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · FileChannel.force

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。

核心机制代码 · java
while (buffer.hasRemaining()) channel.write(buffer); // write可能没有一次写完全部字节。
channel.force(true); // 请求将文件内容及相关元数据更新写向存储设备。
// force不把另一个远程服务纳入事务,也不自动定义日志提交与恢复格式。
// 原子重命名、目录持久性和设备行为仍需按文件系统与平台核验。

FileChannel.force提供将通道文件更新写向存储设备的接口,metaData参数涉及元数据要求;实际保证受本地或非本地存储与平台约束,不能概括成所有故障下绝不丢失。

write允许只处理部分缓冲内容,因此完整写入需要检查剩余量。完整字节写出、请求落盘与业务提交是不同步骤,日志协议必须规定何时记录可恢复。

本篇Java算法用CRC32检查内存日志前缀,展示恢复验证机制,不调用FileChannel,也不实现操作系统缓存或数据库WAL。

框架检查点与人类中断应按固定文档核对重放范围。持久存储只保存可恢复信息,外部写操作是否重复仍取决于业务幂等与结果核对。

代码先调用谁,再调用谁

  1. 序列化带版本日志记录
  2. 循环写出全部字节
  3. 按策略force与提交记录
  4. 重启后验证连续有效前缀
  5. 核对外部操作状态再继续

04动手写一个例子

连续有效检查点日志恢复

每条记录包含从1递增的seq、字符串载荷和CRC32;返回首次顺序或校验错误之前的载荷前缀。CRC不是安全签名。Java17 Main.java只使用内存样本自测。

输入 1
seq1正确、seq2损坏、seq3正确
输出 1
只恢复seq1
输入 2
seq1、seq3均校验正确
输出 2
只恢复seq1

预期顺序号从一开始,逐条核对记录位置。即使CRC正确,缺号也说明恢复链不连续,后续记录不能被当成完整历史。

CRC输入采用顺序号、换行分隔和载荷的UTF-8字节。顺序号格式固定,因此相同记录产生相同校验;这只检测意外差错,攻击者可以重新计算CRC。

遇到第一条无效记录停止,不尝试猜测后续是否独立。真实系统可以有更复杂的事务提交与截断规则,但必须由日志格式明确定义,不能静默跳坏行。

为什么这样写是对的每次接受记录后,恢复列表恰好对应从1到当前序号的连续且校验正确前缀;任何无效记录之后的状态均未被采纳。

时间 O(n+B)额外空间 O(n+M)

n为检查记录数,B为载荷总UTF-8字节量,M为最大单条编码长度;顺序号是固定宽度int,CRC逐字节处理。输出保存载荷引用,临时字节只保留一条;空载荷仍有每记录检查成本。

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

Main.java · 完整可运行程序 · Java
import java.util.*;
import java.util.zip.CRC32;
import java.nio.charset.StandardCharsets;
public class Main {
    record Entry(int seq,String payload,long checksum){}
    static long crc(int seq,String payload){CRC32 c=new CRC32();c.update((Integer.toString(seq)+(char)10+payload).getBytes(StandardCharsets.UTF_8));return c.getValue();}
    static Entry entry(int seq,String text){return new Entry(seq,text,crc(seq,text));}
    static List<String> recover(List<Entry> log){
        List<String> out=new ArrayList<>();int expected=1;
        for(Entry e:log){
            if(e==null||e.payload()==null||e.seq()!=expected)break; // 缺号或坏结构不能跳过。
            if(e.checksum()!=crc(e.seq(),e.payload()))break; // 校验包含序号和载荷。
            out.add(e.payload());expected++; // 只接受连续有效前缀。
        }
        return out;
    }
    static void eq(List<String> e,List<String> a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        eq(List.of("draft","approved"),recover(List.of(entry(1,"draft"),entry(2,"approved"))));
        eq(List.of(),recover(List.of()));
        eq(List.of("draft"),recover(List.of(entry(1,"draft"),new Entry(2,"changed",0),entry(3,"done"))));
        eq(List.of("draft"),recover(List.of(entry(1,"draft"),entry(3,"done"))));
        eq(List.of(),recover(List.of(entry(2,"wrong start"))));
        eq(List.of("中文状态"),recover(List.of(entry(1,"中文状态"))));
        System.out.println("PASS 6 tests");
    }
}

这些特殊情况也要试一试

  • CRC正确但序号跳跃仍停止。
  • 空日志恢复空状态,不自动编造已完成检查点。
  • CRC可被重新计算,不防恶意修改。
进阶选读 · 后续再读:事务日志与跨系统副作用

场景本地状态已提交而外部动作未完成,或外部已完成但本地未记录。

做法使用Outbox、业务幂等键、回执查询和补偿策略,明确每个故障窗口;需要强原子性时评估参与方是否支持相应协议。

复杂度恢复扫描成本与日志量相关,索引和快照可缩短;跨系统一致性成本受网络和参与方语义限制,没有通用常数时间恰好一次方案。

好处与代价更强协调降低不确定性但增加延迟与可用性成本;补偿也不是物理回滚,需定义业务可接受结果。

05项目里怎么用

批准前展示具体计划和影响范围,保存操作摘要与版本。恢复时检查批准仍对应当前内容,目标、参数或授权范围变化时重新核验。批准记录不能仅存在浏览器内存,否则页面刷新或进程重启后无法证明何时批准了什么。

每个可能重放的节点明确副作用位置。读取与纯计算通常可重算,写操作必须有稳定业务键或结果查询;超时后的“未知”不应被自动归类为失败。把未知状态暴露为待核对任务,避免用户和系统同时重复执行。

做退出恢复演练时,不要只随机关一次进程。分别选在“保存准备做的操作”前后、“远端实际执行”前后、“本地保存结果”前后让它退出;还要测试确认之后参数又改变的情况。每次重启,都同时核对存下来的任务状态和远端的真实结果。程序能重新启动,只能说明入口恢复了,不能证明操作没有漏做或重复。

容易踩的坑

  • 检查点存在就承诺外部动作恰好一次。
  • 把节点恢复理解成任意代码行继续执行。
  • 确认不绑定参数与版本,后续内容变化仍沿用。
  • 跳过损坏日志继续恢复后面的状态。

几种做法,怎么选

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

方案优势代价与限制适用条件
内存状态实现简单,延迟低进程退出后无法恢复短任务纯计算
持久检查点与日志可追溯并恢复已知进度需格式、迁移和损坏处理长任务及人工暂停
业务幂等与核对减少重复外部副作用依赖远端能力与操作账本写操作和结果未知场景

06检验是否读懂了

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

先用自己的话解释

01持久执行与幂等是什么关系?

展开参考答案与推导

持久执行保存恢复信息,幂等处理重复执行;前者不能自动提供后者,二者要共同设计。

02CRC能证明记录未被恶意改动吗?

展开参考答案与推导

不能,CRC用于意外损坏检测,攻击者可计算新CRC;完整性安全需要认证机制和受控存储。

03为什么确认要绑定版本?

展开参考答案与推导

批准的是具体操作,后续参数变化可能扩大影响;版本绑定让执行器核对当前动作是否仍在批准范围。

换个情况,还会处理吗

01远端创建成功但本地超时,如何恢复?

展开参考答案与推导

保留未知状态,使用稳定业务键查询或幂等重放,再记录确认结果;不能仅凭超时重复创建。

02重启后再次进入确认节点,副作用重复了怎么办?

展开参考答案与推导

检查中断前是否执行非幂等动作,调整节点边界或持久操作账本,明确框架的节点重放语义。

03日志第二条损坏但第三条看似正常,能用第三条吗?

展开参考答案与推导

本题不能,连续因果链已断;实际恢复只能按明确日志提交和快照规则处理,不能凭外观跳过。

写代码试一试

01序号缺失但CRC正确如何处理?

输入 / 条件
记录1和3
输出 / 预期
仅恢复1

提示 连续性独立于校验。

展开参考答案与推导

第二条不等于预期2,立即停止;CRC只说明该条字节与校验匹配,不能补回缺失状态。

02怎样测试中文载荷?

输入 / 条件
payload=中文状态
输出 / 预期
恢复原字符串

提示 编码必须固定。

展开参考答案与推导

CRC统一使用UTF-8,构造与恢复使用相同编码;不要依赖操作系统默认字符集。

03为什么不能用本测试证明落盘?

输入 / 条件
所有记录在内存List中
输出 / 预期
只证明前缀验证算法

提示 区分算法与存储。

展开参考答案与推导

程序没有写磁盘、崩溃或模拟设备,所以落盘和文件系统持久性需要独立集成证据。

读完之后,独立完成

课后作业:自己做一遍

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

这次练什么

能在模拟崩溃后恢复任务、拒绝过期确认,并核对实际写入结果,确认测试场景中没有重复操作。

本篇重点加练

节点重放必须区分已知结果与未知副作用,否则恢复流程容易重复执行或过早终止。

  • 节点重放
  • 超时结果未知

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

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

基础必做

  1. 作业 01原理问答

    列出一条三节点任务在进程退出后恢复所需的数据,区分内存对象与持久检查点;说明缺少每类数据时会丢失什么判断能力。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点持久检查点 · 内存状态与进程重启

  2. 作业 02代码推演

    构造“本地提交、远程写入、收到响应、保存结果”四个事件的不同崩溃时点,逐个画恢复时序并标记尚未确认的事实。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点节点重放 · 本地事务与远程副作用

  3. 作业 03原理问答

    给一项远程动作列出已确认成功、已确认失败、结果未知三种证据集合,说明恢复节点可依据哪些事实推进状态。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点节点重放 · 超时结果未知

综合应用

  1. 作业 04面试场景

    用户确认操作v3后,参数变成v4且进程重启。设计暂停记录、确认范围和继续执行条件,禁止只用一个布尔值代表所有确认。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点确认绑定操作版本 · 暂停状态落库

  2. 作业 05编程实践

    编写带稳定操作ID的本地假远程服务,模拟请求已执行但响应丢失;恢复任务并记录重发、核对、最终结果与副作用次数。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点稳定幂等键 · 超时结果未知

  3. 作业 06排错实验

    保存检查点后故意重放同一节点两次,检查恢复逻辑是否推进到错误状态;提交重放日志及状态不变量检查。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点持久检查点 · 节点重放 · 暂停状态落库

  4. 作业 07编程实践

    编写事件回放器,预制远程结果先于本地超时记录到达、晚于恢复请求到达和永不到达三种序列,检验最终状态与动作次数的合同。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点节点重放 · 超时结果未知

  5. 作业 08排错实验

    模拟旧进程迟到的成功响应与新进程的恢复查询交错,记录哪个运行版本接收结果;通过自写测试检测过期响应对当前状态的影响。

    提交内容
    最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
    怎样算完成
    • 故障可以稳定重现,现象与已验证原因分开。
    • 观测记录足以判断声明的合同是否被违反。

    练到的知识点节点重放 · 超时结果未知

  6. 作业 09面试场景

    一项未知结果持续数小时,用户要求取消任务。定义取消的是等待、未来动作还是已发生的副作用,并设计可以实际核对的状态说明。

    提交内容
    场景评审记录,包含约束、备选方案、未决信息和验证计划。
    怎样算完成
    • 方案回应题目约束,取舍能对应到可核验的事实。
    • 给出失败或条件变化时的检查方式。

    练到的知识点节点重放 · 超时结果未知

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

  1. 作业 10设计评审

    设计一次确认涉及多个远程动作的恢复协议,说明部分完成、长期未知与用户撤回时可以承诺的行为和无法保证的范围。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点本地事务与远程副作用 · 确认绑定操作版本 · 超时结果未知

思维拓展

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

  1. 作业 11思维拓展 · 反例构造

    构造一个“检查点记录足够完整但仍无法单凭本地数据确定远程动作是否发生”的故障时序,论证所需外部证据。

    提交内容
    具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
    怎样算完成
    • 反例满足原主张所声称的前提,确实针对其结论。
    • 说明一个反例能推翻什么、不能进一步证明什么。

    练到的知识点节点重放 · 超时结果未知

  2. 作业 12思维拓展 · 跨域迁移

    把节点重放与未知结果处理迁移到支付对账的纸面案例,只设计状态和证据链,说明学习助手中的哪些保证在新场景下必须重新验证。

    提交内容
    原场景与目标场景的对应表、可迁移的机制、不能迁移的前提及验证设计。
    怎样算完成
    • 逐项核对两种场景的前提,不只比较相似术语。
    • 至少一个不能直接迁移的边界有具体检验方法。

    练到的知识点节点重放 · 超时结果未知

完成后,再检验一次

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

继续阅读

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

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