关于内部外系统数据一致行方案
内部服务与第三方外部服务交互,如何保证数据一致性
方案
1,先创建外部数据,再创建内部数据
2,先创建内部数据,再创建外部数据,外部数据失败或异常,抛出异常回滚,本地服务
3,先创建内部数据,再创建外部数据,外部数据失败或异常,不回滚,保留本地数据,同时将同步第三方服务请求写入中间表,通过job执行中间表补偿

这是一个非常经典的在分布式系统中保证数据一致性的问题。简单的本地事务无法跨网络和系统边界,因此需要权衡。
针对你给出的三个方案,我的分析和结论如下:
最终推荐:方案3
先创建内部数据,外部调用失败不回滚,而是记录失败任务并通过Job补偿。
详细分析与理由
方案1:先创建外部数据,再创建内部数据
-
逻辑:调第三方接口成功 -> 保存本地数据。
-
风险:极高。
-
致命缺陷:如果第三方接口调用成功,但本地数据库写入失败(比如网络抖动、磁盘满、字段超长等),会导致外部系统有数据,本地系统没有。
-
外部系统已经为客户创建了订单/资源,但本公司内部完全不知道这件事。
-
补偿非常困难:需要人工去外部系统查询“孤立”数据,再补录到本地,极易产生资损和客诉。
-
-
结论:此方案不可行,因为它把一致性的主动权完全交给了不可控的本地持久化环节。
方案2:先创建内部数据,再创建外部数据,失败即回滚
-
逻辑:开启本地事务 -> 保存本地数据 -> 调第三方接口 -> 若第三方失败,抛出异常导致本地事务回滚。
-
优点:保证了本地数据和外部数据的强一致性(要么都成功,要么都失败)。
-
致命缺陷:外部调用不能放在本地事务内。
-
外部调用是网络IO,耗时不可控,会长时间占用数据库连接,高并发下瞬间打爆连接池。
-
外部调用失败时回滚,但外部调用可能已经部分生效(例如对方扣款成功,但返回超时)。此时本地回滚了,外部却已扣款,导致资金损失。
-
外部调用成功但本地提交瞬间断电?依然不一致。
-
-
结论:仅适合低并发、外部响应极快、且外部接口支持完美幂等和事务补偿的极少数场景,通常不推荐。
方案3:先创建内部数据,再调用外部,失败异步补偿(推荐)
-
逻辑:
-
本地事务:保存本地数据,同时保存一条“待同步外部”的任务记录(状态为:待处理)。
-
事务提交后,再尝试调用第三方接口。
-
若调用成功:更新任务状态为“成功”,结束。
-
若调用失败(网络异常、超时、业务失败):不做回滚。本地数据保留(状态可能为“处理中”),任务状态保持“待处理”。
-
定时Job扫描“待处理”的任务,进行重试补偿,直到成功或人工介入。
-
为什么方案3是最佳实践?(核心优势)
-
保护内部数据库:本地事务非常快(只写本地表和任务表),不会因为外部接口的延迟或超时而阻塞数据库连接。这是高并发系统的底线。
-
最终一致性:通过Job重试,保证本地成功的数据最终一定会同步到外部。虽然短时间内可能不一致(例如本地订单状态为“同步中”,外部还没有),但最终会一致。
-
幂等保障:Job重试要求外部接口必须支持幂等(同一个请求多次调用效果等于一次)。通过唯一的业务流水号(如本地订单号)实现。
-
可追溯、可恢复:所有的失败记录都留存在中间表中,不会丢失。Job可以记录重试次数、失败原因,失败多次后还能发出告警,由人工介入处理。方案2一旦失败,连痕迹都没有,排查问题如同大海捞针。
-
适用于绝大多数分布式场景:这是事务消息和本地消息表模式的核心思想,是分布式系统中处理跨系统数据一致性的标准解决方案。阿里、腾讯等大厂内部大量使用。
方案3的具体落地架构(本地消息表 + Job)
text
1. 【本地事务】
insert into 本地订单表 (order_id, status, ...) values ('ORD123', 'PENDING', ...);
insert into 外发任务表 (task_id, biz_id, status, retry_count) values (1, 'ORD123', 'INIT', 0);
commit;
2. 【同步调用】(事务外)
try {
调用第三方创建订单接口(orderId='ORD123');
更新任务表 status = 'SUCCESS';
} catch (Exception e) {
更新任务表 retry_count++, next_retry_time = now + 5分钟;
// 本地数据不回滚,保持PENDING状态
}
3. 【Job补偿】
@Scheduled(cron = "0/30 * * * * ?") // 每30秒
public void compensate() {
List<Task> tasks = taskMapper.selectList(
status='INIT' AND retry_count < 10 AND next_retry_time <= now
);
for (Task task : tasks) {
// 重试调用第三方接口
// 必须基于业务ID(ORD123)做幂等,防止重复创建
}
}
改进建议:引入消息队列(MQ)
如果项目中有MQ(如RocketMQ、RabbitMQ),可以将Job扫描升级为MQ异步,体验更好:
-
本地事务提交时,同时发送一条MQ消息。
-
MQ消费者负责调用外部接口。
-
消费失败时,利用MQ的重试和死信队列机制进行补偿。
-
优点:更实时,减少Job扫描的延迟和数据库压力。
总结
| 方案 | 数据一致性 | 数据库性能 | 可补偿性 | 推荐度 |
|---|---|---|---|---|
| 方案1 (先外部) | 极差 | 一般 | 极难 | ❌ 不可用 |
| 方案2 (事务内) | 强(假象) | 极差(长事务) | 无 | ❌ 高风险 |
| 方案3 (本地消息表) | 最终一致 | 优秀 | 完备 | ✅ 强烈推荐 |
最终建议:选择方案3,并确保第三方接口支持幂等处理。这是分布式环境下兼顾性能、可靠性和一致性的标准解法。
本回答由 AI 生成,内容仅供参考,请仔细甄别。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)