Inner Reg Sync Redesign

architect review — current vs. proposed — 2026-03-09

1 — 问题诊断: 为什么能停27天无人知
根因不是某行代码 — 而是架构缺失

同步停滞27天却没人发现,暴露的不是"哪个接口挂了",而是整套同步机制在可观测性、容错性、状态管理三个维度上的系统性缺失。任何单点异常都会让整条链路静默死亡,且无任何告警机制触发人工干预。

0
告警通知
0
健康检查
0
重试机制
27d
发现延迟
2 — 现有架构七大缺陷
#缺陷现状描述风险等级
F1 静默失败
错误黑洞
catch 块仅 log.error,不抛异常、不告警、不重试。Stage 1 整体被 try-catch 包裹,外部接口返回任何异常都被吞掉。QueryZdRegsBiz:214-218 / SyncRegsDataJob:28-29 致命
F2 sync_status
死锁陷阱
Stage 2 处理前先将 sync_status 设为 1(同步中),如果服务重启/OOM/异常中断,状态永远停在1。查询条件 IN(0,3) 不包含1,这批数据永远不会被重新处理。UpdateZdRegsBiz:142 / 86 致命
F3 零可观测性
零监控
没有 Metrics、没有 HealthCheck、没有告警。唯一的可观测手段是翻日志。停了27天靠人肉发现。 致命
F4 递归调用
无保护
queryRegsFromZd() 使用递归拉取下一批数据,如果制度系统返回大量数据,递归深度不可控,可能 StackOverflow。虽然有 zd.limit 但默认可能未配。QueryZdRegsBiz:139
F5 三阶段
无编排
三个 Stage 是完全独立的 Quartz Job,各跑各的 Cron。Stage 1 00:00 拉数据 → Stage 2 每2h转换 → Stage 3 18:00 同步。没有事件驱动、没有依赖关系,完全靠时间差来"碰运气"。
F6 筛选条件
过于严格
Stage 2 要求 5 个条件同时满足才处理。任何一个字段异常(如制度系统返回了 status=2 而非 3),记录就被永久跳过,且无任何提示。这不是"过滤",是"静默丢数据"。
F7 无幂等性
保证
Stage 2 的 lawRegulationProcess 先删后插,没有事务保护覆盖全流程。中途失败可能导致部分删除+部分插入的脏数据状态。
3 — 改进方案总览
设计原则: 不大改架构,聚焦补齐三个关键能力

考虑到这是银行生产系统,不建议推倒重来(引入消息队列、重写为事件驱动等)。风险高、周期长、审批困难。改进方案的原则是:在现有 Spring Boot + Quartz 技术栈内,用最小改动补齐三个关键能力

A. 状态机 + 容错
让每条同步记录的生命周期可管理、可恢复、不会卡死
B. 可观测 + 告警
主动发现问题,而不是等用户反馈"数据怎么没更新"
C. 可恢复 + 幂等
任何中断都能自动恢复或手动补偿,不丢数据
4 — 核心改进 A: 状态机 + 容错
Current: 脆弱的二值状态
Proposed: 完整状态机
  • sync_status 只有 0/1/2/3 四种值
  • 处理前直接改为1,中断即卡死
  • 查询条件 IN(0,3) 永久跳过状态1
  • 没有超时检测机制
  • 没有最大重试次数
  • 增加 retry_countlast_attempt_time 字段
  • 状态1(同步中) 超过30分钟自动回退到0
  • 重试次数超过3次 → 状态4(人工处理)
  • 每次处理记录 last_attempt_time
  • 定时扫描"卡死"记录并自动修复
实现代码示例: 超时自动回退
// 在 UpdateZdRegsBiz.updateLawRegulation() 开头增加:

// 1. 自动修复卡在"同步中"超过30分钟的记录
LambdaUpdateWrapper<LawRegulationSyncRecord> fixStuck = Wrappers
    .lambdaUpdate(LawRegulationSyncRecord.class)
    .set(LawRegulationSyncRecord::getSyncStatus, "0")
    .eq(LawRegulationSyncRecord::getSyncStatus, "1")
    .lt(LawRegulationSyncRecord::getUpdateTime,
        DateUtil.offsetMinute(new Date(), -30));
lawRegulationSyncRecordService.update(fixStuck);

// 2. 超过3次重试的移入人工队列
LambdaUpdateWrapper<LawRegulationSyncRecord> toDead = Wrappers
    .lambdaUpdate(LawRegulationSyncRecord.class)
    .set(LawRegulationSyncRecord::getSyncStatus, "4")
    .in(LawRegulationSyncRecord::getSyncStatus, "0", "3")
    .ge(LawRegulationSyncRecord::getRetryCount, 3);
lawRegulationSyncRecordService.update(toDead);
改动量评估: 表加2个字段 (retry_count INT DEFAULT 0, last_attempt_time DATETIME),UpdateZdRegsBiz 开头加10行修复逻辑,updateSyncStatus 方法中增加 retry_count 自增。总改动 < 30行代码。
5 — 核心改进 B: 可观测 + 告警
新增: 同步健康检查定时任务

新建一个轻量级 Quartz Job,每天10:00检查同步健康状态,发现异常时通过现有的消息服务推送告警。

检查项 1: 同步新鲜度
sync_record 表最新记录距今超过 48小时 → 告警
说明 Stage 1 拉取已停止
检查项 2: 积压检测
sync_status IN (0,3) 的记录超过 50条 → 告警
说明 Stage 2 转换能力不足或已停止
检查项 3: 卡死检测
sync_status = 1update_time 超过 1小时 的记录 → 告警
说明有数据处于僵死状态
检查项 4: 错误率检测
最近24h内 result_code = E 的比率超过 50% → 告警
说明外部接口大面积异常
Current: 纯日志, 需人工翻查
Proposed: 主动发现 + 推送
  • 所有异常只写 log.error
  • 日志量大,淹没在正常日志中
  • 没有结构化 Metrics
  • 依赖人工发现问题
  • 发现时已滞后数周
  • 新增 SyncHealthCheckJob (每天10:00)
  • 复用现有 MessageService 发站内消息/邮件
  • 后台管理页增加同步状态看板 (复用现有前端组件)
  • 关键操作增加结构化日志 (JSON格式便于检索)
  • 问题发现时间: 数周 → 24小时内
改动量评估: 新增 SyncHealthCheckJob + SyncHealthCheckJobScheduler 两个类 (约100行),复用现有 MessageService 推送告警。前端可选增加一个简单的同步状态页面。
6 — 核心改进 C: 可恢复 + 幂等
三项关键改造
C1. 递归改循环
queryRegsFromZd() 从递归改为 while 循环 + 计数器。避免 StackOverflow,同时方便记录进度和中断恢复。

改动: ~15行
C2. Stage 1 增加超时
callZd() 的 HTTP 请求没有设置超时。增加 connectTimeout=10s + readTimeout=60s。防止请求永久挂起阻塞 Quartz 调度。

改动: ~5行
C3. 手动补偿接口
RegMaintainController 增加管理端接口:指定 traceId 重新拉取、批量重置 sync_status、手动触发 Stage 2。提供运维自救能力。

改动: ~40行
C1 示例: 递归改循环
// Before: 递归 (有 StackOverflow 风险)
public QueryZdRegsXml queryRegsFromZd(req, counter) {
    QueryZdRegsXml result = regsProcess(req, params, counter);
    String nextTraceId = result.getNextTraceId();
    if (isNotEmpty(nextTraceId)) {
        req.setTraceId(nextTraceId);
        result = queryRegsFromZd(req, counter);  // 递归!
    }
    return result;
}

// After: 循环 (安全、可控、可记录进度)
public void syncRegsFromZd(String traceId) {
    int count = 0;
    int limit = zdConfig.getLimit() != null ? zdConfig.getLimit() : 100;
    String currentTraceId = resolveTraceId(traceId);

    while (currentTraceId != null && count < limit) {
        try {
            QueryZdRegsXml result = regsProcess(currentTraceId);
            currentTraceId = result.getNextTraceId();
            count++;
        } catch (Exception e) {
            log.error("[SYNC] 第{}批处理失败, traceId={}",
                count, currentTraceId, e);
            break;  // 失败时中断, 下次从断点继续
        }
    }
    log.info("[SYNC] 本次共处理{}批", count);
}
C2 示例: HTTP 超时设置
// Before: 无超时 (可能永久阻塞)
String body = HttpRequest.post(url)
    .addHeaders(headers).form(params)
    .execute().body();

// After: 明确超时
String body = HttpRequest.post(url)
    .addHeaders(headers).form(params)
    .timeout(60000)       // 总超时 60 秒
    .execute().body();
7 — 逐项对比: 现有 vs. 改进
维度现有方案改进方案改动量
异常感知 catch 后仅 log.error,异常被吞掉 健康检查 Job 每日巡检 + 消息告警推送 新增1个Job
问题发现
时间
27天(人工偶然发现) < 24小时(定时巡检主动发现) --
状态卡死 sync_status=1 后服务重启 → 永久卡死 超时自动回退 + 重试次数上限 + 死信队列 加2字段+10行
递归风险 queryRegsFromZd 递归调用,理论可 StackOverflow 改为 while 循环,异常时 break 保存进度 改15行
HTTP 超时 callZd() 无超时,可能永久挂起阻塞 Quartz 设置 60s 超时 改1行
数据静默
丢失
5个条件全不满足的记录被永久跳过,无提示 不满足条件的记录标记为"待人工审核(status=5)"并告警 加5行判断
运维手段 无补偿接口,出问题只能直接改数据库 管理端补偿接口: 重拉/重置/重试 加3个接口
阶段协调 三阶段靠 Cron 时间差碰运气 Stage 1 完成后主动触发 Stage 2(可选增强) 可选改进
8 — 分阶段实施路径
三个阶段, 按优先级递减
P0 紧急 止血修复
目标: 防止再次静默死亡

  • HTTP 超时设置 (1行)
  • sync_status 超时自动回退 (10行)
  • 递归改循环 (15行)
  • Stage 1 catch 块增加告警 (5行)

总改动: ~30行
耗时: 0.5天
P1 重要 可观测性
目标: 主动发现问题

  • 新增 SyncHealthCheckJob (100行)
  • 复用 MessageService 推送告警
  • 管理端补偿接口 (40行)
  • DB 增加 retry_count 等字段

总改动: ~150行
耗时: 1-2天
P2 增强 长期演进
目标: 架构韧性

  • 前端同步状态看板
  • Stage 1 完成后事件驱动 Stage 2
  • 异常记录人工审核流程
  • 同步数据一致性校验

总改动: 按需
耗时: 按需迭代
核心理念: 好的同步系统不是"永不出错"的系统,而是"出错时能快速发现、自动恢复、不丢数据"的系统。P0 阶段 30 行代码就能解决最致命的三个问题(卡死、超时、静默失败),投入产出比极高,建议立即实施。