把死掉的日终幂等地救回来
目标
重新运行中途失败的日终结算,使其恢复完成,同时既不重复入账也不遗漏。并掌握区分会计日期与系统时间来选择结算对象的方法。
为什么重要
银行系统中,时钟显示的时间(posted_at)与账簿认可的日期(biz_date)彼此独立。会计日期只有在日终批处理完成后才会推进,因此在结算运行期间进入的交易究竟应归入哪一天,会产生歧义。由此引发的事故占银行批处理故障的很大一部分。
日终批处理必须具备幂等性。这意味着针对同一个会计日期,无论运行多少次,结果都必须相同。否则批处理失败后,仅仅重新运行本身就会成为新的故障。重新运行只有两种方式:在已入账结果之上只补充未入账项目的增量方式,以“已经入账的内容全部正确”为前提。如果该前提已经不成立,增量方式便没有手段移除错误入账的内容。
本次事件如下。2026-08-25 的日终批处理于 22:10 开始,随后在中途失败。会计日期为 2026-08-25 的 240 行分录中已有 153 行入账,而且日终产出物的余额合计不为 0。此外,日终运行期间,有一个在线渠道仍持续写入旧会计日期。截止时间为 2026-08-25T22:10:00,下一营业日为 2026-08-26。
步骤
- 创建并运行
/root/bank/gen_eod.py,生成/root/bank/eod.db。其中应包含 372 行分录和日终运行历史。 - 在
/root/bank/eod_state.txt中写七行。business_date=、eod_status=来自sys_param;txn_total=、applied=、unapplied=来自该会计日期的txn;balance_rows=、closing_sum=来自daily_balance。 - 将
posted_at晚于截止时间且biz_date为2026-08-25的行输出到/root/bank/misdated.csv,表头为txn_id,posted_at,biz_date,eod_applied。然后在/root/bank/misdated.txt中写三行:misdated=、misdated_applied=、naive_after_cutoff=。最后一个值表示不考虑会计日期、仅按posted_at筛选时的行数。 - 将这些行的
biz_date移至2026-08-26,并把eod_applied恢复为0。不要改动已经完成日终结算的2026-08-24。 - 在
/root/bank/rerun_plan.md中按## 두 가지 재수행 방식、## 선택과 근거、## 멱등성을 어떻게 보장하나、## 중단 기준四个章节编写计划。“选择与依据”章节必须以数字写明需要重新计算的行数。 - 创建
/root/bank/eod_rerun.sh并运行一次,将产出物输出到/root/bank/db_run1.csv,表头为biz_date,account,debit_total,credit_total,closing_balance。还要在eod_run中留下成功记录。 - 再次运行同一个脚本并输出
/root/bank/db_run2.csv,然后在/root/bank/idempotency.txt中写五行:run1_rows=、run1_closing_sum=、run2_rows=、run2_closing_sum=、diff=。 - 将
sys_param的eod_status改为CLOSED,把business_date推进到2026-08-26,并在/root/bank/eod_report.md中按## 무엇이 멈췄나、## 계정일자가 왜 어긋났나、## 재수행을 어떻게 안전하게 만들었나、## 검증、## 남은 위험五个章节编写报告。
参考
- 不要硬编码截止时间;通过
SELECT value FROM sys_param WHERE name='eod_cutoff'读取,可以降低条件不一致的可能性。 - 日终产出物的
closing_balance总和是否为 0,是最快的健全性信号。如果不为 0,说明有些分录只入账了一侧。 - 常见错误 1:第 3 步只按
posted_at筛选。这样会连同其他渠道正确写入下一营业日的交易一起选中。 - 常见错误 2:第 4 步只更改
biz_date,却保持eod_applied不变。该行会继续留在前一次日终产出物中,同时在下一次日终处理中被跳过。 - 常见错误 3:第 6 步的脚本只使用
INSERT。第二次运行时数值会翻倍。
重现日终结算失败时的状态
创建并运行 /root/bank/gen_eod.py,生成 /root/bank/eod.db。其中应包含 372 行分录和日终运行历史。
使用 python3 创建 /root/bank/eod.db。数据库包含 sys_param、txn、daily_balance、eod_run 四张表,并有 372 行分录。
统计失败的日终处理进行到了哪里
在 /root/bank/eod_state.txt 中写七行。business_date=、eod_status= 来自 sys_param;txn_total=、applied=、unapplied= 来自该会计日期的 txn;balance_rows=、closing_sum= 来自 daily_balance。
日终对象应按 biz_date 而不是 posted_at 选择。还要检查产出物的 closing_balance 总和是否为 0——如果不为 0,说明分录只入账了一侧。
找出会计日期错误的交易
将 posted_at 晚于截止时间且 biz_date 为 2026-08-25 的行输出到 /root/bank/misdated.csv,表头为 txn_id,posted_at,biz_date,eod_applied。然后在 /root/bank/misdated.txt 中写三行:misdated=、misdated_applied=、naive_after_cutoff=。最后一个值表示不考虑会计日期、仅按 posted_at 筛选时的行数。
有两个条件:posted_at 晚于截止时间,同时 biz_date 为结算日。若只使用前一个条件,其他渠道正确写入下一营业日的交易也会被选中。
将会计日期移至下一营业日
将这些行的 biz_date 移至 2026-08-26,并把 eod_applied 恢复为 0。不要改动已经完成日终结算的 2026-08-24。
不能只更改 biz_date。部分行已进入日终产出物,因此还必须同时恢复 eod_applied。不要改动已经完成日终结算的前一天。
确定重跑方式并写明依据
在 /root/bank/rerun_plan.md 中按 ## 두 가지 재수행 방식、## 선택과 근거、## 멱등성을 어떻게 보장하나、## 중단 기준 四个章节编写计划。“选择与依据”章节必须以数字写明需要重新计算的行数。
需要四个章节。在“选择与依据”章节中,以数字写明需要重新计算多少行;在“幂等性”章节中,说明如何处理哪张表。
创建重跑脚本并运行一次
创建 /root/bank/eod_rerun.sh 并运行一次,将产出物输出到 /root/bank/db_run1.csv,表头为 biz_date,account,debit_total,credit_total,closing_balance。还要在 eod_run 中留下成功记录。
不要手动输入 SQL,要编写脚本。只有第二次执行与第一次逐字相同,才能测试幂等性。还要将第一次运行的产出物保存为 CSV。
再运行一次以证明幂等性
再次运行同一个脚本并输出 /root/bank/db_run2.csv,然后在 /root/bank/idempotency.txt 中写五行:run1_rows=、run1_closing_sum=、run2_rows=、run2_closing_sum=、diff=。
原样再次运行同一个脚本,并重新输出产出物。逐行比较两个 CSV 的结果就是 diff。如果数值翻倍,说明你没有先删除,而是继续累加。
确认日终结算并推进会计日期
将 sys_param 的 eod_status 改为 CLOSED,把 business_date 推进到 2026-08-26,并在 /root/bank/eod_report.md 中按 ## 무엇이 멈췄나、## 계정일자가 왜 어긋났나、## 재수행을 어떻게 안전하게 만들었나、## 검증、## 남은 위험 五个章节编写报告。
生成产出物与确认日终结算是两回事。要同时推进 sys_param 中的日终状态和会计日期,并完成报告的五个章节。