返回架构笔记
Arch Day 39

Arch Day 39: 日终批处理架构 — 金融系统的"心脏搏动"

日终批处理(End-of-Day Batch Processing)是银行核心系统的心脏搏动——每天营业结束后,系统需要执行计息、对账、报表生成、风险计算、监管数据报送等一系列有序、可靠、高性能的批量处理任务,通常需要在4-6小时窗口内完成数亿笔数据的处理。

2026-05-08
第二阶段 - 金融域深度
核心银行批处理日终清算计息SpringBatchDAG编排容错设计

日期: 2026-05-08 (Day 39) 阶段: 第二阶段 - 金融域深度 标签: #核心银行 #批处理 #日终清算 #计息 #SpringBatch #DAG编排 #容错设计


核心概念

一句话定义

日终批处理(End-of-Day Batch Processing)是银行核心系统的心脏搏动——每天营业结束后,系统需要执行计息、对账、报表生成、风险计算、监管数据报送等一系列有序、可靠、高性能的批量处理任务,通常需要在4-6小时窗口内完成数亿笔数据的处理。

为什么资深架构师必须关注

维度关注理由
业务关键性计息错误=直接资金损失;对账错误=监管处罚;报表延迟=违规
性能挑战1亿账户的日终处理需要在4-6小时完成,对架构设计要求极高
可靠性要求批处理失败必须能从断点恢复,不能全部重跑(可能来不及)
与在线冲突批处理和在线交易共享数据库,设计不当会导致在线系统受影响
面试高频"如何设计不影响在线交易的日终批处理"是金融系统架构面试必考题

常见误区与反模式

误区真相
"现代系统不需要批处理"即使实时系统也需要批处理——合规报表、监管数据、会计日切都无法完全实时化
"批处理就是跑SQL"金融批处理涉及复杂的业务逻辑(计息公式/费率计算/多币种),不是简单SQL
"并行度越高越快"盲目并行会导致数据库锁争用、内存溢出、网络瓶颈,需要精心设计分片策略
"批处理和在线系统可以共用同一个库"共用主库的批处理会严重影响在线交易响应时间,必须做隔离
"失败了全部重跑就行"全量重跑可能需要8小时以上,超出批处理窗口,必须支持断点续跑

知识点详解

知识点1:银行为什么需要日终批处理

银行一天的生命周期
━━━━━━━━━━━━━━━━━

06:00-09:00  早间准备
├── 检查隔夜批处理结果
├── 异常处理/手工调整
└── 开门前系统健康检查

09:00-17:00  营业时间(在线交易)
├── 柜面交易(存取款/转账/开户)
├── 电子渠道(网银/手机银行/ATM)
├── 大额支付系统(CNAPS)
├── 小额支付系统
└── 跨境汇款(SWIFT)

17:00-18:00  日切准备
├── 停止接收新交易(Cutoff)
├── 等待在途交易完成
├── 数据库快照(一致性点)
└── 启动日终标志

18:00-24:00  日终批处理(EOD Batch)
├── Phase 1: 计息(Interest Calculation)
│   ├── 活期存款计息(1亿+账户)
│   ├── 定期存款到期处理
│   ├── 贷款计息+罚息
│   └── 结构性存款收益计算
├── Phase 2: 记账(Posting)
│   ├── 利息入账
│   ├── 手续费扣收
│   ├── 自动扣款(定期还款)
│   └── 代收代付处理
├── Phase 3: 对账(Reconciliation)
│   ├── 内部对账(总分核对)
│   ├── 跨系统对账(核心vs支付vs渠道)
│   ├── 银联/网联对账
│   └── 央行清算对账
├── Phase 4: 风险计算
│   ├── 拨备计算(Expected Credit Loss)
│   ├── 资本充足率计算
│   ├── 流动性覆盖率(LCR)
│   └── 大额风险暴露
├── Phase 5: 报表生成
│   ├── 1104监管报表
│   ├── 反洗钱报告
│   ├── 内部管理报表
│   └── 会计分录/日记账
└── Phase 6: 日切(Day Change)
    ├── 更新系统日期
    ├── 初始化次日参数
    └── 发送日终完成信号

00:00-06:00  隔夜处理
├── 数据备份
├── 数据仓库ETL
├── 监管数据报送
└── 灾备数据同步

关键业务逻辑举例——活期存款计息

活期存款计息逻辑(积数计息法)
━━━━━━━━━━━━━━━━━━━━━━━━━━

公式: 利息 = 积数 × 日利率
积数 = Σ(每日余额 × 天数)

示例:
日期       余额        天数    积数
3/1-3/10   10,000元    10      100,000
3/11-3/20  15,000元    10      150,000
3/21-3/31  8,000元     11      88,000
                       ──      ───────
                       31      338,000

日利率 = 年利率 / 360 = 0.35% / 360 = 0.0009722%
利息 = 338,000 × 0.0009722% = 3.29元

复杂性:
├── 多层利率(阶梯利率: 5万以下0.35%, 5万以上0.40%)
├── 靠档计息(已取消,但存量客户仍按旧规则)
├── 协议存款(大额客户自定义利率)
├── 多币种(不同币种计息基础不同: 360/365/实际天数)
├── 精度要求(分以下四舍五入,误差需要入差错账)
└── 税务处理(利息税自动扣缴)

知识点2:批处理架构模式

模式1: 传统ETL Pipeline
━━━━━━━━━━━━━━━━━━━━━━

  Source DB → Extract → Transform → Load → Target DB
             (抽取)    (转换)      (加载)

工具: Informatica / DataStage / Talend / Kettle

特点:
├── 简单直观,适合数据搬运型任务
├── 主要用于报表/数据仓库场景
├── 不适合复杂业务逻辑处理
└── 扩展性有限(通常单机运行)

适用场景: 数据仓库ETL、监管报表数据准备


模式2: Spring Batch (Java生态标准)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Job
  ├── Step 1: 计息
  │   ├── ItemReader (读取账户数据)
  │   ├── ItemProcessor (计算利息)
  │   └── ItemWriter (写入利息记录)
  ├── Step 2: 记账
  │   ├── ItemReader
  │   ├── ItemProcessor
  │   └── ItemWriter
  └── Step 3: 对账
      ├── ItemReader
      ├── ItemProcessor
      └── ItemWriter

核心概念:
├── Job: 一个完整的批处理任务
├── Step: Job中的一个步骤
├── Chunk: 读取-处理-写入的原子单位(如1000条)
├── Tasklet: 简单的单步任务(非读写模式)
├── JobRepository: 存储执行元数据(支持重启)
└── JobLauncher: 启动和调度Job

特点:
├── 成熟的容错机制(Skip/Retry/Restart)
├── 事务管理(每个Chunk一个事务)
├── 可扩展(多线程Step/分区/远程分块)
├── 与Spring生态深度集成
└── 金融行业广泛使用

适用场景: 单机或少量节点的批处理


模式3: 分布式批处理 (MapReduce思想)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

                    ┌─ Worker 1: 处理账户分片1
  Coordinator ──────┼─ Worker 2: 处理账户分片2
  (调度+分片)       ├─ Worker 3: 处理账户分片3
                    └─ Worker N: 处理账户分片N
                              │
                              ▼
                    Aggregator: 汇总结果

实现方式:
├── Spring Batch Partitioning (远程分区)
├── 自建分布式调度框架
├── Spark Batch (大数据场景)
├── Flink Batch Mode
└── 自研框架(蚂蚁/微众等)

特点:
├── 水平扩展能力强
├── 适合海量数据(亿级账户)
├── 架构复杂度高
├── 需要分片策略设计
└── 需要分布式事务或补偿机制

适用场景: 大型银行(千万级以上账户)

知识点3:批处理编排——DAG依赖管理

日终批处理DAG (有向无环图)
━━━━━━━━━━━━━━━━━━━━━━━━━

                    ┌─── [活期计息] ─── [利息入账] ───┐
[日切检查] ──┬──────┤                                  ├─── [总分对账]
             │      └─── [定期到期] ─── [到期处理] ───┘      │
             │                                               │
             ├─── [贷款计息] ─── [罚息处理] ─── [还款处理] ──┤
             │                                               │
             ├─── [手续费计算] ─── [费用扣收] ────────────────┤
             │                                               │
             └─── [代收代付] ─────────────────────────────────┘
                                                              │
                                                              ▼
                                         ┌─── [1104报表] ──────┐
                              [记账汇总] ┼─── [反洗钱报告] ────┤
                                         └─── [管理报表] ──────┘
                                                              │
                                                              ▼
                                                         [日切完成]

DAG编排的关键要素:
├── 依赖关系: 利息入账必须在计息完成后
├── 并行机会: 活期计息和贷款计息可以并行
├── 检查点: 每个节点完成后记录状态
├── 超时控制: 每个节点有最大执行时间
├── 失败策略: 节点失败后是阻塞下游还是跳过
└── 优先级: 关键路径上的节点优先执行

编排框架选型

框架适用场景优点缺点
Spring Batch + Quartz中小银行成熟稳定,Java生态分布式能力弱
Apache Airflow数据处理流DAG可视化,Python生态非金融级,事务支持弱
XXL-Job分布式调度轻量级,中文社区功能相对基础
Apache DolphinScheduler大数据批处理国产开源,DAG编排强金融场景需要增强
自研调度框架大型银行完全可控,贴合业务开发维护成本高
Control-M (BMC)传统大型银行企业级功能最全License费极高

知识点4:容错设计

批处理容错五层防线
━━━━━━━━━━━━━━━━━

Layer 1: Chunk级别事务控制
├── 每个Chunk(如1000条)一个事务
├── Chunk失败只回滚本Chunk,不影响已完成的Chunk
├── 好处: 100万条数据,处理到第50万条失败,只需从第50万条继续
└── 实现: Spring Batch的chunk-oriented processing

Layer 2: Skip策略(跳过异常记录)
├── 遇到特定异常(如数据格式错误)跳过该记录
├── 跳过的记录写入异常表,后续人工处理
├── 设置最大跳过数(如100条),超过则终止Job
└── 注意: 金融场景对Skip需谨慎——资金类操作通常不能跳过

Layer 3: Retry策略(重试)
├── 遇到临时性故障(数据库连接超时/锁等待)自动重试
├── 重试策略: 固定间隔 / 指数退避 / 自定义
├── 最大重试次数(通常3次)
└── 重试需幂等: 同一操作重试N次结果必须一样

Layer 4: Checkpoint/Restart(断点续跑)
├── 每个Step完成后记录执行位置到JobRepository
├── Job失败后可以从上次失败的位置继续
├── 不需要重新处理已完成的数据
└── 这是批处理最核心的容错机制

Layer 5: 补偿与对账
├── 批处理完成后运行对账Job
├── 发现差异立即告警
├── 差错处理流程(冲正/补录/调账)
└── T+1早间必须在开门前完成差错处理

Spring Batch容错配置示例

@Configuration
public class InterestBatchConfig {

    @Bean
    public Job interestCalculationJob(
            JobRepository jobRepository,
            Step calculateInterestStep,
            Step postInterestStep,
            Step reconcileStep) {

        return new JobBuilder("interestCalculationJob", jobRepository)
            .start(calculateInterestStep)
            .next(postInterestStep)
            .next(reconcileStep)
            .listener(new JobCompletionNotificationListener())
            .build();
    }

    @Bean
    public Step calculateInterestStep(
            JobRepository jobRepository,
            PlatformTransactionManager txManager) {

        return new StepBuilder("calculateInterest", jobRepository)
            .<Account, InterestRecord>chunk(1000, txManager)  // 每1000条一个事务
            .reader(accountReader())
            .processor(interestCalculator())
            .writer(interestWriter())
            .faultTolerant()
            .skipLimit(100)                          // 最多跳过100条
            .skip(DataFormatException.class)          // 只跳过格式异常
            .noSkip(InsufficientFundsException.class) // 资金异常不能跳过
            .retryLimit(3)                            // 最多重试3次
            .retry(DeadlockLoserDataAccessException.class) // 死锁重试
            .retry(QueryTimeoutException.class)       // 超时重试
            .listener(new StepExecutionListener() {
                @Override
                public void afterStep(StepExecution stepExecution) {
                    // 记录处理统计
                    log.info("处理: {}条, 成功: {}条, 跳过: {}条",
                        stepExecution.getReadCount(),
                        stepExecution.getWriteCount(),
                        stepExecution.getSkipCount());
                }
            })
            .build();
    }

    @Bean
    @StepScope
    public JdbcPagingItemReader<Account> accountReader() {
        return new JdbcPagingItemReaderBuilder<Account>()
            .name("accountReader")
            .dataSource(replicaDataSource)  // 从只读副本读取
            .queryProvider(accountQueryProvider())
            .pageSize(1000)
            .rowMapper(new AccountRowMapper())
            .build();
    }
}

知识点5:批处理与在线交易的隔离

隔离策略
━━━━━━━━

策略1: 时间窗口隔离(最传统)
├── 银行营业时间: 09:00-17:00 (在线交易)
├── 批处理窗口: 18:00-06:00
├── 问题: 随着7×24服务普及,时间窗口越来越短
└── 现状: 仍然是大多数银行的主要策略

策略2: 读写分离
├── 在线交易: 读写主库
├── 批处理读取: 从只读副本读
├── 批处理写入: 写入影子表 → 批处理完成后swap
├── 挑战: 主从延迟导致计息数据不准
└── 缓解: 批处理启动前等待副本追上主库

策略3: 影子库(Shadow Database)
├── 日切时将主库数据快照到影子库
├── 批处理在影子库上运行
├── 批处理结果通过消息/API写回主库
├── 在线交易完全不受影响
└── 代价: 双倍存储成本 + 数据同步复杂度

策略4: CQRS分离
├── Command(写): 在线交易通过命令写入
├── Query(读): 批处理从读模型读取
├── 批处理结果: 生成新的Command写回
└── 好处: 天然隔离,但架构复杂度最高

策略5: 分片隔离
├── 按账户分片: 分片1-100在线, 分片101-200批处理
├── 轮流处理: 每个分片轮流进入批处理模式
├── 该分片在批处理期间只读
├── 好处: 不需要停机,渐进式处理
└── 挑战: 需要精心设计分片粒度和切换机制

知识点6:批处理性能优化

优化策略具体方法效果预估适用场景
分片并行按账户号段/按分行/按产品分片,多线程并行处理线性提升(N核≈N倍)账户级计算(计息)
预计算白天实时维护积数,日终只做最后计算减少80%计算量计息场景
增量处理只处理当天有变动的账户(而非全量)减少60-90%处理量对账/报表
内存计算将账户数据加载到内存(Redis/本地缓存)计算10-100倍提升热点数据计算
批量写入攒够1000条一次性写入(而非逐条写)10-50倍提升所有写入场景
索引优化批处理专用索引(处理前创建,处理后删除)2-5倍提升大范围扫描
压缩事务合并同一账户的多笔操作为一笔减少事务数多笔入账
异步写入计算和写入解耦,计算结果先入队列再写库提升吞吐量I/O密集场景

分片策略详解

分片策略选择
━━━━━━━━━━━

方案A: 按账户号段分片
├── 分片1: 账户 0000000001 - 0010000000
├── 分片2: 账户 0010000001 - 0020000000
├── ...
├── 优点: 简单,数据分布均匀
├── 缺点: 新账户集中在最后的分片(数据倾斜)
└── 优化: 定期重新平衡分片边界

方案B: 按Hash分片
├── 分片 = hash(账户号) % N
├── 优点: 数据分布均匀,不会倾斜
├── 缺点: 同一分行的账户分散到不同分片,跨分片对账困难
└── 适用: 不需要按机构汇总的场景

方案C: 按分行/机构分片
├── 分片1: 北京分行所有账户
├── 分片2: 上海分行所有账户
├── ...
├── 优点: 符合业务组织结构,便于按机构对账
├── 缺点: 大分行和小分行数据量差异大
└── 优化: 大分行再按号段二次分片

方案D: 混合分片
├── 第一层: 按产品类型(活期/定期/贷款)
├── 第二层: 按账户号段或Hash
├── 优点: 不同产品的处理逻辑可以完全独立
├── 缺点: 跨产品的聚合操作需要额外处理
└── 推荐: 大型银行首选

知识点7:从日终到准实时的演进

演进路线图
━━━━━━━━━━

阶段1: 传统日终批处理 (大多数银行现状)
├── 特征: 所有计算集中在夜间窗口
├── 时间: 4-8小时
├── 问题: 窗口越来越紧张
└── 适合: 中小银行、业务量不大

阶段2: 缩短批处理窗口
├── 方法: 预计算+增量+并行
├── 时间: 1-3小时
├── 里程碑: 大多数银行的优化目标
└── 适合: 需要7×24服务的银行

阶段3: Mini-Batch (微批处理)
├── 方法: 每小时/每30分钟跑一次小批
├── 好处: 计息更及时、报表更新更频繁
├── 挑战: 需要重新设计会计日切逻辑
└── 适合: 互联网银行(微众/网商)

阶段4: Event-Driven + 实时计算
├── 方法: 每笔交易触发实时计息更新
├── 架构: Event Sourcing + Stream Processing
├── 仍需要批处理: 对账/报表/监管
├── 挑战: 实时计算的一致性保证
└── 适合: 下一代核心银行(Thought Machine方式)

阶段5: 无批处理(理想状态)
├── 所有计算实时完成
├── 对账通过区块链/分布式账本实现
├── 报表从实时数据湖生成
├── 监管报送API化(Open Regulation)
└── 现实: 短期内不可能完全实现,合规要求仍有T+1逻辑

对比分析

批处理架构模式对比

维度单机Spring Batch分布式Spring BatchSpark BatchStream+Mini-Batch
适用规模<1000万账户1000万-5亿账户>1亿账户(数据分析)不限(逐笔处理)
技术栈Java/SpringJava/Spring + 消息队列Scala/Python/SparkKafka/Flink/自研
容错机制Chunk+Skip+Retry分区+重试+补偿RDD LineageCheckpoint+Exactly-Once
事务支持强(ACID)最终一致性无事务At-least-once+幂等
开发复杂度
运维复杂度
窗口要求4-8小时1-4小时1-3小时接近实时
金融系统适用性中小银行核心大型银行核心风险计算/报表下一代核心银行
成本中高

日终批处理 vs DeFi实时结算

维度银行日终批处理DeFi实时处理
结算时效T+0(日终)/T+1(跨行)秒级(区块确认后)
计息方式日终统一计算每个区块实时累积(如Aave利息)
对账需求必须(总分核对/跨系统)不需要(区块链=唯一真相源)
报表生成批处理生成The Graph/Dune实时查询
容错机制Checkpoint/Restart区块链回滚(链重组)
资源消耗集中在夜间均匀分布(每个区块)

架构设计实操

设计目标

为一家5000万零售账户的股份制银行设计日终清算+计息批处理方案。

当前痛点:

  • 日终批处理需要7.5小时,超出6小时窗口
  • 在线交易在批处理期间响应变慢
  • 批处理失败后需要全量重跑,经常来不及

架构方案

整体架构
━━━━━━━━

           ┌──────────────────────────────────────┐
           │          批处理调度中心(Coordinator)    │
           │  ┌─────────────────────────────────┐  │
           │  │    DAG引擎(依赖管理+并行调度)    │  │
           │  └─────────────────────────────────┘  │
           │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
           │  │监控  │ │告警  │ │日志  │ │审计  │ │
           │  └──────┘ └──────┘ └──────┘ └──────┘ │
           └──────┬────────┬────────┬────────┬─────┘
                  │        │        │        │
        ┌─────────┤ ┌──────┤ ┌──────┤ ┌──────┤
        ▼         ▼ ▼      ▼ ▼      ▼ ▼      ▼
   ┌────────┐┌────────┐┌────────┐┌────────┐
   │Worker 1││Worker 2││Worker 3││Worker N│
   │分片1   ││分片2   ││分片3   ││分片N   │
   │1000万  ││1000万  ││1000万  ││1000万  │
   └───┬────┘└───┬────┘└───┬────┘└───┬────┘
       │         │         │         │
       ▼         ▼         ▼         ▼
   ┌────────────────────────────────────┐
   │     批处理专用数据库(影子库)        │
   │  (日切时从主库快照,批处理在此运行) │
   └────────────────────────────────────┘
       │ 结果写回
       ▼
   ┌────────────────────────────────────┐
   │           主库(在线交易)            │
   │  (批处理期间在线交易不受影响)       │
   └────────────────────────────────────┘

DAG编排设计

# 日终批处理DAG定义
dag:
  name: "eod_batch_20260508"
  schedule: "0 18 * * *"  # 每天18:00启动
  timeout: "6h"
  alert_threshold: "4h"   # 超过4小时告警

  phases:
    - name: "phase1_preparation"
      timeout: "30m"
      steps:
        - id: "cutoff_check"
          desc: "检查在途交易是否全部完成"
          type: "check"
          retry: 3
          retry_interval: "5m"

        - id: "snapshot_db"
          desc: "创建影子库快照"
          depends_on: ["cutoff_check"]
          type: "tasklet"
          timeout: "20m"

    - name: "phase2_calculation"
      timeout: "2h"
      parallel: true  # 以下步骤可并行
      steps:
        - id: "demand_deposit_interest"
          desc: "活期存款计息"
          depends_on: ["snapshot_db"]
          type: "partitioned_chunk"
          partition_key: "account_id"
          partition_count: 50  # 50个分片并行
          chunk_size: 1000
          skip_limit: 100
          retry_limit: 3

        - id: "time_deposit_maturity"
          desc: "定期存款到期处理"
          depends_on: ["snapshot_db"]
          type: "partitioned_chunk"
          partition_key: "account_id"
          partition_count: 20
          chunk_size: 500

        - id: "loan_interest"
          desc: "贷款计息"
          depends_on: ["snapshot_db"]
          type: "partitioned_chunk"
          partition_key: "loan_id"
          partition_count: 30
          chunk_size: 500

    - name: "phase3_posting"
      timeout: "1h"
      steps:
        - id: "interest_posting"
          desc: "利息入账"
          depends_on: ["demand_deposit_interest", "time_deposit_maturity", "loan_interest"]
          type: "partitioned_chunk"
          partition_count: 50

        - id: "fee_collection"
          desc: "手续费扣收"
          depends_on: ["interest_posting"]
          type: "partitioned_chunk"
          partition_count: 20

    - name: "phase4_reconciliation"
      timeout: "1h"
      steps:
        - id: "internal_recon"
          desc: "内部总分对账"
          depends_on: ["interest_posting", "fee_collection"]
          type: "tasklet"

        - id: "cross_system_recon"
          desc: "跨系统对账"
          depends_on: ["internal_recon"]
          type: "chunk"

    - name: "phase5_reporting"
      timeout: "1h"
      parallel: true
      steps:
        - id: "regulatory_report"
          desc: "监管报表"
          depends_on: ["internal_recon"]
        - id: "management_report"
          desc: "管理报表"
          depends_on: ["internal_recon"]

    - name: "phase6_day_change"
      timeout: "15m"
      steps:
        - id: "day_change"
          desc: "日切"
          depends_on: ["regulatory_report", "management_report"]

ADR: 日终批处理架构决策

# ADR-039: 日终批处理架构选型

## 状态
ACCEPTED

## 上下文
当前日终批处理耗时7.5小时,超出6小时窗口。需要重新设计批处理架构,
目标是将处理时间缩短到3小时以内,且支持断点续跑。

## 决策
采用"分布式Spring Batch + DAG编排 + 影子库隔离"的架构。

1. 计算引擎: Spring Batch Remote Partitioning
   - 50个Worker并行处理5000万账户(每Worker 100万)
   - 每个Worker内部按Chunk(1000条)处理

2. 编排引擎: 自研DAG调度器(基于DolphinScheduler改造)
   - 支持任务依赖、并行、超时、告警
   - 支持Checkpoint和断点续跑

3. 数据隔离: 影子库方案
   - 日切时对主库做逻辑快照
   - 批处理在影子库上运行,不影响在线交易
   - 结果通过消息队列异步写回主库

4. 预计算优化: 白天实时维护积数
   - 每笔交易时实时更新账户积数
   - 日终只需做最后的利息计算(积数×利率)
   - 预计减少80%的计息计算量

## 后果
- 正面: 批处理窗口从7.5h缩短到约2.5h
- 正面: 在线交易不受批处理影响
- 正面: 支持断点续跑,故障恢复时间<30min
- 负面: 影子库增加存储成本(约30%)
- 负面: 预计算增加在线交易的复杂度
- 负面: 分布式架构运维复杂度增加

AI增强实践

AI在批处理中的应用

AI增强的批处理系统
━━━━━━━━━━━━━━━━━

1. 智能调度优化
   ├── AI预测每个Job的执行时间(基于历史数据)
   ├── 动态调整并行度(根据当前系统负载)
   ├── 自动优化DAG执行顺序(关键路径优先)
   └── 预测批处理窗口是否足够(提前告警)

2. 异常检测
   ├── 监控批处理各指标(处理速度/错误率/资源使用)
   ├── AI检测异常模式(突然变慢/错误率飙升)
   ├── 自动根因分析(数据库慢查询/锁等待/内存不足)
   └── 智能建议修复方案

3. 计息逻辑验证
   ├── AI对比新旧算法的计息结果
   ├── 自动发现计算偏差
   ├── 异常利息金额检测(偏离正常分布)
   └── 减少人工对账工作量

4. 对账自动化
   ├── AI匹配跨系统的对账差异
   ├── 自动分类差异原因(时间差/汇率差/舍入差)
   ├── 自动生成差错处理建议
   └── 历史差异模式学习
# AI驱动的批处理调度优化器
class AIBatchScheduler:
    """基于ML的批处理调度优化"""

    def predict_execution_time(self, job_id, params):
        """预测Job执行时间"""
        features = {
            'account_count': params['account_count'],
            'day_of_week': datetime.now().weekday(),
            'is_month_end': self._is_month_end(),
            'is_quarter_end': self._is_quarter_end(),
            'db_load': self._get_current_db_load(),
            'historical_avg': self._get_historical_avg(job_id),
        }
        return self.model.predict(features)

    def optimize_dag(self, dag, total_window_hours=6):
        """优化DAG执行计划"""
        # 预测每个节点的执行时间
        for node in dag.nodes:
            node.predicted_time = self.predict_execution_time(
                node.job_id, node.params
            )

        # 计算关键路径
        critical_path = self._find_critical_path(dag)
        critical_time = sum(n.predicted_time for n in critical_path)

        if critical_time > total_window_hours * 3600:
            # 窗口不够,需要优化
            suggestions = self._generate_optimization_suggestions(
                dag, critical_path, total_window_hours
            )
            self._alert("批处理窗口预测不足", suggestions)

        # 动态调整并行度
        available_resources = self._get_available_resources()
        optimized_plan = self._schedule_with_resources(
            dag, available_resources
        )
        return optimized_plan

与Web3/DeFi的关联

批处理概念在DeFi中的对应

传统银行批处理 → DeFi处理方式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━

日终计息 → Aave/Compound利息累积
├── 传统: 日终统一计算,T+1入账
├── Aave: 每个区块实时累积利息(aToken余额实时增长)
├── Compound: 每次交互时重新计算(lazy evaluation)
└── 差异: DeFi用区块时间戳而非营业日概念

日终对账 → 链上实时对账
├── 传统: 隔夜跑对账Job,发现差异人工处理
├── DeFi: 不需要对账——区块链本身就是唯一真相源
└── 但是: DeFi协议仍需要链下对账(预言机价格/跨链状态)

监管报表 → 链上数据查询
├── 传统: 批处理生成报表 → 人工审核 → 报送监管
├── DeFi: 通过The Graph/Dune实时查询链上数据
└── 趋势: 监管机构直接接入区块链节点(Open Regulation)

日切 → 区块(Block)
├── 传统: 每天一次日切,更新系统日期
├── 区块链: 每12秒一个区块(以太坊),每个区块就是一次"结算"
└── 本质: 区块链把"一天一次的批处理"变成了"每个区块一次的微批处理"

清结算 → 原子结算
├── 传统: T+1或T+2清结算(涉及多方对账)
├── DeFi: 交易即结算(Atomic Settlement)
└── 意义: 消除了对账、清算、结算三个独立步骤

DeFi的"批处理"场景

即使DeFi是实时的,仍有类似批处理的需求:

1. Keeper/Liquidation Bot
   ├── 定期扫描所有借贷头寸
   ├── 检查健康因子 < 1的头寸
   ├── 触发清算
   └── 类似: 银行日终贷款风险扫描

2. Rebase Token (如OHM/AMPL)
   ├── 定期(如每8小时)调整所有持有者余额
   └── 类似: 银行日终计息入账

3. 链下计算 + 链上验证
   ├── Merkle Air Drop: 链下计算空投资格 → 链上Claim
   ├── L2 Batch: 链下执行交易 → 链上提交证明
   └── 类似: 批处理结果写回主系统

4. 预言机价格更新
   ├── Chainlink定期推送价格(Heartbeat)
   └── 类似: 银行日终更新汇率/利率

今日思考

深度问题1

"如果银行的所有业务都实时化了,还需要日终批处理吗?"

短期内仍然需要。即使所有交易实时处理,仍有三类需求需要批处理:(1)监管报表——监管机构的数据采集标准是按日/按月/按季,不支持实时;(2)会计日切——财务会计需要明确的"日"的边界来做记账和报表;(3)大规模数据分析——风险计算(如VaR/ECL)需要全量数据。但这些批处理会从"必须在夜间完成"演进到"可以在任何时间完成"。

深度问题2

"预计算(白天维护积数)和延迟计算(交互时再算,如Compound)哪种更好?"

各有权衡。预计算的优点是日终处理快、结果即时可用,缺点是增加了在线交易的复杂度和存储需求。延迟计算(Lazy Evaluation)的优点是在线处理简单,缺点是查询余额时需要实时计算。DeFi的Compound选择了延迟计算是因为链上存储昂贵、用户交互频率低。银行选择预计算是因为用户频繁查询余额且对性能要求高。本质上是写时计算 vs 读时计算的权衡。

深度问题3

"区块链的'每个区块结算一次'模型能否应用到传统银行?"

理论上可以——将银行的日终批处理改为每分钟/每秒的微批处理,本质上就是在模拟区块链的模型。实际上,Thought Machine的Vault就在这样做:每笔交易触发合约执行,利息实时累积。但完全采用这种模型需要解决:(1)计算资源成本大幅增加;(2)与现有监管框架(按日计息/按日报表)的兼容;(3)与其他银行/清算机构(仍然是批处理模式)的对接。


面试题准备

面试题1:如何设计不影响在线交易的日终批处理?

30秒版本: 核心策略是隔离。我推荐三层隔离:第一层,使用影子库——日切时将数据快照到独立数据库,批处理在影子库运行;第二层,时间窗口管理——利用交易低峰期(凌晨)做高负载处理;第三层,读写分离——批处理读取只读副本,结果通过消息队列异步写回主库。

2分钟版本: 这个问题我会从隔离策略、性能优化、容错设计三个角度回答。

隔离策略方面,最有效的方案是影子库。在日切时对主库做逻辑快照(或物理复制)到一个独立的数据库实例,所有批处理计算在影子库上运行。这样在线交易完全不受影响。批处理的结果(利息记录、对账结果)通过消息队列异步写回主库。

性能优化方面,关键是减少处理量和提高并行度。减少处理量的最有效方法是预计算——白天每笔交易时实时维护积数,日终只需做最后的乘法运算。提高并行度方面,按账户号段分片,50个Worker并行处理5000万账户。

容错设计方面,使用Spring Batch的Chunk机制,每1000条一个事务。配合Checkpoint记录处理进度,失败后从断点续跑。对于不同类型的异常,分别设置Skip(数据异常)和Retry(临时性故障)策略。

实际效果方面,在我了解的一个案例中,通过这三个策略将批处理时间从7.5小时缩短到2.5小时,且在线交易的P99延迟没有增加。

可能的追问

  • Q:影子库的数据一致性如何保证?
  • A:使用数据库的逻辑复制(如PostgreSQL的pglogical)或MVCC快照,确保影子库的数据是日切时刻的一致性视图。批处理结果写回主库时,使用幂等性设计防止重复写入。

面试题2:批处理失败如何恢复?

30秒版本: 关键是断点续跑。每个Chunk处理完成后将进度记录到JobRepository(持久化元数据表)。Job失败时,重启会自动从上次失败的位置继续处理,不需要重跑已完成的数据。对于跳过的异常记录,写入异常表后续人工处理。对于严重故障(如数据库不可用),启动灾备方案——切换到灾备数据库继续。

2分钟版本: (展开断点续跑的实现细节,加上不同失败场景的处理策略)


学习资源

资源类型推荐理由
Spring Batch官方文档文档批处理框架最权威的参考
《Pro Spring Batch》书籍Spring Batch深度实践
DolphinScheduler文档文档国产调度框架,适合学习DAG编排
Aave利息计算文档DeFi文档对比理解"实时计息"
银行核心系统日终处理流程(知乎)文章中文实战经验分享

明日预告

Day 40: 金融系统数据库设计 — 分库分表策略(按账户/按时间/按业务线)、路由规则设计、读写分离的一致性挑战、分布式数据库选型(OceanBase/TiDB/CockroachDB)、金融数据归档与审计。数据库是核心银行的基石,设计失误代价极高。