Java EE 企业应用 34:批处理检查点与重启
处理到一半,重启从哪一行开始
供应商每晚送来一份合成目录文件:有正确的 SKU 和价格,也有重复行、非法金额。逐条以 HTTP 提交会把吞吐和错误恢复转嫁给客户端;单事务包住整个文件,则第 900 行的错误可能让前 899 行全部回滚。批处理的关键不是“在后台跑循环”,而是可持久化的输入标识、完成边界和拒绝清单。
本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 1b08ada,SHA-256 见源码清单。
线上采购用的价格目录实现是固定值,采购领域金额约束属于在线订单规则;初始数据库迁移没有供应商目录导入表,服务器配置没有 Batch 作业资源或 job XML。不能拿现有采购场景脚本当批量导入证据。
检查点必须能重现输入
拟议作业以 (supplier_id, file_sha256, import_version) 标识不可变输入,每行有 (file_id, line_no) 唯一键;先将待处理文件复制到隔离、不可变位置,核对行数与摘要后才启动导入。Job 参数记录文件标识和格式版本,而不是客户端任意路径;文件若被替换,旧 checkpoint 的偏移不能安全套到新内容上。
Jakarta Batch 2.1 的 chunk 模型由 reader、processor、writer 和检查点协作:reader 读取下一段,processor 校验或规范化,writer 在事务中写入行及账本。教学实验可设 item-count=20,但具体提交边界、异常时回滚与 reader 位置恢复须按实现实际记录。为两条非法输入设置有限 skip 上限时,单靠配置跳过异常不会自动生成带原因码的 import_rejection:需要设计拒绝记录如何与检查点及重试保持一致,并以数据库结果检验。那条合法但重复的业务行应按业务键核对并单独记账,不占用两条非法输入的 skip 预算。数据库不可用不应被当作坏数据无限跳过;暂时性错误的 retry 也需有上限,并确保 writer 使用 (file_id, line_no) 去重。Job repository 的保留、持久化及崩溃恢复能力需要在已冻结运行时验证,不能仅凭 restart(executionId, properties) API 就断言任何断电点都无损。
1 | |
如果 writer 还发送邮件或写外部文件,数据库回滚不会自动撤销它。把外部副作用另记任务或采用第 23、30 篇的 outbox 思路;本章验收仅覆盖本地目录表和拒绝记录。作业状态 FAILED 不等于所有先前 chunk 均回滚,COMPLETED 也不等于所有输入均成功:需分别报成功数、拒绝数、重复数与未处理数。故障后不得直接重新以新参数创建全新作业并宣称“重启成功”。
文件位置不是输入身份
假设 suppliers.csv 上一版有 55 行,后一版被覆盖在同一路径下,只剩 54 行。如果 reader 只保存了“已读取到第 40 行”,重启时可能把新版本的第 41 行当成旧版未处理记录,也可能重复写入旧版的第 40 行。导入前先复制到不可修改的隔离路径,记录 SHA-256、字节数、编码、换行方式和格式版本;每次作业启动只接受这个固定输入标识。读到的行号要从文件头约定清楚:是否包含表头、空白行和末尾空行,必须在数据合同中定义,不能让 CSV 解析器版本变化后导致账本总数漂移。
任务参数包含 supplier_id、输入摘要、格式版本与导入批次标识,机密凭证不能作为 job 参数随作业记录保存。服务端从不可变目录解析文件名,拒绝 ../ 和外部 URL,不能让调用者指定任意系统文件。对每行计算稳定行键 (file_id, line_no),并另按业务键 (supplier_id, sku, import_version) 判定重复价格版本;两种键语义不同,同一行在 checkpoint 重放时应被安全忽略或核对,文件中两条相同 SKU 应按业务规则拒绝或覆写,但不能悄悄多入一条。这些规则须在最初 55 行样例里明确,不能等运行结果出来再更改“重复”的含义。
当前FixedPriceCatalog给采购用例提供固定目录;若批导入结果要影响在线下单,未来还需一个从持久目录读取、带版本与生效时间的价格端口。不能只写 supplier_price 表就宣布线上订单已经使用导入价格。更新价格时应定义正在创建的订单读取旧版本还是新版本,保证采购单中保存的单价不会被后续导入悄悄重算;现有purchase_line已保存订单行单价,这是复核历史金额的基础。
chunk 提交后哪些数据仍可见
chunk 的 reader 决定读哪些条目,processor 区分合法、重复与可跳过坏记录,writer 写入业务表。item-count=20 表示目标 chunk 大小,不等同于任何时刻“必定恰好 20 行入库”:跳过错误、处理重试和最后不足一组的输入都会影响入库数量。给可跳过的解析/校验异常设置白名单与上限,而不是把 SQLException 全部归为坏数据。比如价格 -1 是行级输入错误,可记录原因并允许其它行继续;数据库连接中断是系统故障,应导致当前 chunk 回滚并停止或有限重试,绝不能把断库期间的所有价格都记作供应商坏文件。
检查点保存的是处理器将来继续工作的必要状态,不能只存一条文本“已读 40”。如果 reader 在事务提交前读过 20 行,但 writer 提交失败,读取位置也应回到正确的边界。读指针、业务写入和 checkpoint 的原子性要依靠实际 Batch 实现及所用资源事务配置核实;规范描述恢复契约,不能替本地部署的 repository 提供存储保障。writer 每行执行重复安全检查:相同 (file_id,line_no) 再出现只核对已保存内容;不同内容占据旧键则报数据完整性错误。若写失败后用新文件 ID 重跑以绕过唯一键,会让重复保护完全失效。
对于一次完整的 55 行样例,正常结果应分别记录:文件读入 55、52 行通过并新增、2 行按明确原因拒绝、1 行命中已定义的业务重复、0 行未处理。为了让统计可复查,拒绝账本记录 file_id、line_no、错误码与内容摘要,避免包含供应商私密字段;重复账本记录对应的既有行 ID。仅查看作业状态 COMPLETED 不足以证明这四个计数成立。若现有库里已有同 SKU,实验应使用独立测试供应商或清晰的初始快照,不能偷偷清空共享目录来凑 52 个新行。
STOP、FAILED 与服务器被杀不是同一种恢复
Batch 的 JobOperator 可以查询 job execution 状态,也提供 restart 操作,但重启要有可被识别的上一轮执行及可用的 repository。如果用受控异常让作业标记 FAILED,可在记录 execution ID、摘要和检查点后尝试对同一作业实例重启;正常执行结束后应仍指向同一不可变输入。直接杀掉服务器时,repository 可能留下 STARTED 状态,是否以及何时能重启,取决于冻结实现的恢复机制和管理员操作。先记录它的实际状态、错误信息与官方处置方法;不允许自动创建一个新的 job instance,改名叫“断点续跑”。
实验选择明确的屏障:当第二个 chunk 的业务写入及对应 checkpoint 已持久化,测试进程发出可观察信号,随后才停止服务器。重启时先查询已提交行、拒绝和重复账本;若第二个 chunk 还没提交就被杀,应报告实际断点并按真正检查点验证重放,不应该硬写“前 40 行已经导入”。最终与一次未注入故障的运行作差:新增业务行相同、错误原因相同、重复行数相同,且在线目录价格没有被半成品覆盖。若人手补数据,需单独记补偿过程,不算自动恢复通过。
本地数据库事务只覆盖它参加的持久资源。如果 writer 逐行发邮件或写导出文件,第二 chunk 回滚后文件可能还留下行,不能用数据库唯一键消除文件副作用。把这类出站动作转为与业务结果同事务的任务记录,再由第 30、31 篇链路处理,或独立做文件临时写入与原子发布的验收。第 34 篇只把目录导入和拒绝账本当成批作业的完成判据,不把供应商通知和文件投递混入 COMPLETED 的含义。
当前配置与以后要保存的命令
按工程 README 准备 JDK 21、Maven wrapper、隔离 Open Liberty 和 PostgreSQL 16。db/migrations/004-price-import.sql 增加导入与拒绝账本,webapp/src/main/resources/META-INF/batch-jobs/price-import.xml 配置每 chunk 两条;PriceReader 将文件 SHA-256 与下一条序号作为 checkpoint,PriceWriter 按 (file_id,line_no) 幂等写入,非法价格进入拒绝账本。batch-input/sample.csv 是五条不含引号内逗号/换行的合成记录,不能与下方待构造的 55 条合同混同。scenarios/34-price-import.sh 提供启动、状态和重启入口,专用口令只从安全环境注入:
1 | |
JAVAEE_BATCH_DIR 要在容器启动前设置;004 只在空隔离库执行一次。第一次的 execution_id 必须从实际回包提取。第 5 条写入前注入异常,前两个 chunk 的状态需通过独立 SQL 确认;重启后核对 repository 状态、有效四条中的三条与一条拒绝、输入摘要及最终第五条。独立 Liberty 实例读到 Batch 2.1 并激活 JPA 持久仓库,却以 CWWKY0304W 拒绝已通过 HTTP Basic 的实验用户启动作业;需要配置并核验 Batch 自身的提交角色映射后重跑,web.xml 的 batchLab 角色映射不足以替代引擎授权。当前入口通过编译,但导入、chunk 提交和崩溃恢复均无成功运行记录,保持 NOT_RUN。
用一份可重放文件验收
现有可执行的构建基线(JDK 21、Maven wrapper;运行时使用 Open Liberty 26.0.0.5、独立 PostgreSQL 16、隔离环境,详见工程说明):
1 | |
现已有 job XML、REST 启动/重启入口、004 迁移和五条合成记录;Liberty 已激活持久 repository,但启动被 Batch 内部角色授权错误 CWWKY0304W 拒绝,没有成功执行导入或重启,因此实验仍为 NOT_RUN,原始记录见 verification/20261005T-revision/。后续实验需准备 55 条解析后的数据记录、固定摘要的合成文件:52 个唯一有效行、2 个独立错误行、1 个重复行;配置 chunk 20 和非法输入的 skip 上限 2,另外为业务重复记独立结果。正常路径必须逐项核对 52 + 2 + 1 = 55 与账本唯一键、作业 COMPLETED;重复的定义若是数据库已有行,另记录既有数据,不把它算作 52 个新增。失败路径在第二个 chunk 提交后的受控断点杀进程,保存 job execution ID、已提交行数、检查点,再按同一输入重启;最终计数与一次完整运行一致,重复行不增加,拒绝原因仍保留。断点若无法稳定落在提交之后,必须保留日志说明实际位置,不推断“恰好第二 chunk”。详细合同见 批处理实验合同。
对 55 行结果做可解释的对账
用同一份固定摘要输入运行一次没有故障的作业作为基准,保留 job execution ID、每个 chunk 前后的 row-count、成功行键列表、错误行号与拒绝原因。再在隔离批次里,用相同输入和作业定义注入第二 chunk 后的失败。两次批次要有各自的 file_id,或先把数据库恢复到相同初态;不能在已有导入结果的库里重做第二次,把历史成功行算成“重复”。比较的是两个同样初态的最终业务行集合与拒绝集合,不能只比两个日志上的“52”。独立库或新供应商 ID 都可以提供隔离,但哪种方式应写进实验记录。
正常路径的 52 行新增并非“最后批次写入 52 行”的隐含结果。假设第二个 chunk 中包含两条坏行,依据 skip 配置该 chunk 可能写入 18 行;下一轮 reader 从上一个成功 checkpoint 开始继续读,最终集合要与一次性处理整文件的结果一致。若 skip 限额是 2,却有第 3 条非法价格,作业应按实际规范和实现报告失败,保留前面已提交行且未处理输入可见;不能因为业务方想拿到 55 行总账,就在 processor 里把第 3 条坏行偷偷替换成零价。
文件副作用的判据也要在验收表里独立列出:数据库已提交的 52 行价格记录不能自动推出供应商收到了 52 个回执;中间生成的部分导出文件不能直接发布给采购页面。若导入作业还要刷新价格查询缓存,应在每个可见版本完成后切换生效标记,并能回退到上一版本,而不是让 chunk 1 提交后前台先读到混合价格。该在线切换属于后续集成,不在当前静态 FixedPriceCatalog 内假装已经实现。
一旦实际 Batch 仓库记录与业务表的提交状态不一致,应先停止新导入、保存证据,并按 (file_id,line_no) 及内容摘要对账。贸然删掉 repository 的 STARTED 行或清空业务表重跑,会丢失第一次执行保留的有效数据。人工修复属于有审计记录的处置,而非“可自动恢复”实验的成功结果。只有在真实实现的 restart 流程、真实输入摘要和最终行集合都可追溯时,才能将重启案例标记为通过。迁移 reader 或 CSV 解析器版本时,先用旧摘要输入在隔离库重放并比较行键,再决定是否允许使用旧 checkpoint;否则解析规则改变也会让同一个文件产生不同的 55 行统计。
两道练习
练习一:输入文件路径相同但内容被供应商改了一行,能否用旧 execution ID 重启?解:不应直接重启。检查文件摘要和版本;不同内容对应新的不可变输入,旧 checkpoint 的行位置已不能描述新文件。先隔离旧任务并完成差异核对,再决定新导入或补偿。
练习二:第二个 chunk 失败后,数据库已有 20 行;是不是 Batch 实现吞了异常?解:不是。chunk 的提交单位小于整份文件,前面已提交的行可保留。核对 job execution 状态、checkpoint、writer 唯一键,再以相同输入重启,不能用“全文件原子”标准误判 chunk 处理。
CSV 的引号内换行会让“物理文件行号”与业务记录序号不一致。固定解析器与方言,把 (file_id,line_no) 中的 line_no 定义为解析后的记录序号,并记录原始字节偏移或摘要用于定位错误;仅依赖文本编辑器显示的第 37 行,重启后可能指向另一笔供应商价格。读入编码异常也要形成拒绝或作业失败的明确结果,不允许静默跳过而仍报告 55 条已处理。
适用范围与版本资料
数值、输入数据与故障注入均是计划,不代表作业已部署;批处理结果 NOT_RUN。规范:Jakarta Batch 2.1:checkpoint、skip/retry 与 restart、Jakarta EE 11 Platform;本系列实际运行边界见版本表(证据路径:writing-plans/javaee-enterprise/VERSIONS.md,需仓库权限)。






