数据同步任务执行到一半失败了,如何保证数据的一致性?
- 标签:后端、场景题
🏛 业务架构师
同步任务执行到一半失败——设计目标是**"任务可重跑"(rerunnable)**:任何一个失败点重启,要么接着跑,要么从头跑,结果都正确。我从设计原则、失败处理、兜底三层答。
核心设计原则
- 幂等写入:任务每一步的写入可重复执行无副作用——数据库用 upsert(
ON DUPLICATE KEY UPDATE)、按业务主键去重,重跑 N 遍结果一致。没有幂等,一切断点恢复都是空谈 - 断点续传(checkpoint):进度持久化(游标/批次号落库),重启后从断点继续——适合长任务、大批量
- 小事务分批提交:每批一个事务(如 1000 条),绝不整任务一个大事务——大事务回滚慢、锁持有久、undo log 爆炸;批粒度让"一半失败"的损失天然只有半批
- 失败隔离:单条数据的问题(格式、约束冲突)不能拖死整批——坏数据进错误表,主流程继续,任务的完成度不被 1% 的脏数据绑架
失败后的处理策略(按任务性质选)
- 可重跑任务(同步、刷新类):自动重试(退避 + 上限),从断点续跑
- 部分成功任务(导入类):已完成部分保留,失败部分导出错误清单人工修复
- 强原子任务(资金类初始化):全量回滚重来,或走人工核对流程
兜底与观测
- 任务状态机:RUNNING / SUCCESS / FAILED / PARTIAL,进度百分比落库可查——运营要看得到任务状态,"黑盒跑批失败三天没人知道"是真实事故
- 告警分级:重试 3 次失败 → 群告警;影响次日业务 → 电话
- 对账闭环:任务级对账(源 vs 目标 count/sum),重跑后核对,差异清零才算恢复
收尾:我的验收标准一句话——任何同步任务,在任意一行之后被 kill -9,重启后数据最终正确。设计时按这个标准写测试用例,就杜绝了"跑一半"的恐惧。
🏗 系统架构师
同步任务执行到一半失败,一致性问题的本质是断点状态的原子性与幂等性。我按设计原则和恢复机制讲。
1. 事务边界设计:任务分片 + 批次原子
- 大任务先切分片(shard),每个分片内按批次(batch)推进,批次是事务与提交的最小单位
- 每批处理完原子地完成两件事:写入目标数据 + 更新进度位点(断点续传的 checkpoint)。进度可以落同步任务表(shard_id、last_cursor、status),与数据写入同事务,保证"数据与进度一致"
- 失败恢复从位点重放,绝不从头重跑全量
2. 幂等性:重放的 safety net 位点回退或消息重复不可避免,所有写操作幂等:
- 数据写入:主键 upsert /
ON DUPLICATE KEY UPDATE - 业务动作:唯一业务单号去重(处理记录表唯一索引)
- 副作用补偿:发过通知的不再发(通知记录表)
3. 失败分类处理(不是所有失败都一样):
- 瞬时失败(网络、锁超时):指数退避重试 N 次,批次内自愈
- 数据失败(格式、约束冲突):该条隔离到错误表,批次跳过继续,不能让一条脏数据卡死整个任务
- 系统失败(依赖宕机):任务挂起,依赖恢复后自动续跑 + 告警
4. 一致性校验与补偿闭环
- 任务完成后对账:源与目标的分片 count/checksum 比对
- 差异自动触发局部重放(按差异区间重跑),仍失败进人工队列
- 关键场景配"最终一致检查点":例如订单同步以订单状态机为准,核对超时未同步的订单主动补拉
5. 调度层保障:任务实例故障由调度平台(如 XXL-Job 分片广播、K8s Job 重启)接管续跑;任务表用分布式锁防双实例并发跑同一分片。
一句话:分片+批次划小事务边界,位点与数据同事务提交,幂等支撑安全重放,对账兜底最终一致。
💻 全栈开发专家
核心思路:把"执行一半"变成不可能出错的状态——分批 + 断点 + 幂等 + 对账,四层防护。
第一层:缩小事务边界
不要把整个任务包成一个巨型事务(长事务锁表、undo log 膨胀、可能超时回滚到怀疑人生)。拆成小批次,每批一个独立事务:
java
while (hasNext()) {
List<Row> batch = fetchNext(1000);
transactionTemplate.execute(st -> {
writeBatch(batch);
saveCheckpoint(batchId, batch.getLastCursor()); // 位点和数据同事务
return null;
});
}关键点:check point 和数据同库同事务落盘——要么都成功要么都回滚,重启后从断点继续,绝不重跑已成功批次。
第二层:幂等设计
重试难免重放,目标端写入必须幂等:主键 upsert、或唯一键 + INSERT IGNORE / 冲突更新。这样即使某批"实际成功但 ack 丢失导致重推",结果依然正确。
第三层:失败隔离
单批失败重试 N 次(指数退避)仍失败,跳过并记录异常表,任务继续跑完,最后汇总异常批人工或自动修复——比整体卡死好得多。不可跳过的场景就暂停任务,保留现场。
第四层:对账修复
任务级最终一致性靠对账:任务完成后比对源和目标的 count/sum/抽样明细,不一致的按主键重新推送。这是兜底,也是面试官最想听的"闭环"。
常见坑
- 位点存 Redis 而不是和业务同事务的 DB:Redis 写成功业务回滚(或反过来)照样不一致,位点必须与数据同事务。
- 半成品中间表方案:写临时表最后原子 rename,适合全量替换型任务。