内部服务与第三方外部服务交互,如何保证数据一致性

方案

1,先创建外部数据,再创建内部数据

2,先创建内部数据,再创建外部数据,外部数据失败或异常,抛出异常回滚,本地服务

3,先创建内部数据,再创建外部数据,外部数据失败或异常,不回滚,保留本地数据,同时将同步第三方服务请求写入中间表,通过job执行中间表补偿

这是一个非常经典的在分布式系统中保证数据一致性的问题。简单的本地事务无法跨网络和系统边界,因此需要权衡。

针对你给出的三个方案,我的分析和结论如下:

最终推荐:方案3

先创建内部数据,外部调用失败不回滚,而是记录失败任务并通过Job补偿。

详细分析与理由

方案1:先创建外部数据,再创建内部数据
  • 逻辑:调第三方接口成功 -> 保存本地数据。

  • 风险极高

  • 致命缺陷:如果第三方接口调用成功,但本地数据库写入失败(比如网络抖动、磁盘满、字段超长等),会导致外部系统有数据,本地系统没有

    • 外部系统已经为客户创建了订单/资源,但本公司内部完全不知道这件事。

    • 补偿非常困难:需要人工去外部系统查询“孤立”数据,再补录到本地,极易产生资损和客诉。

  • 结论此方案不可行,因为它把一致性的主动权完全交给了不可控的本地持久化环节。

方案2:先创建内部数据,再创建外部数据,失败即回滚
  • 逻辑:开启本地事务 -> 保存本地数据 -> 调第三方接口 -> 若第三方失败,抛出异常导致本地事务回滚

  • 优点:保证了本地数据和外部数据的强一致性(要么都成功,要么都失败)。

  • 致命缺陷外部调用不能放在本地事务内

    • 外部调用是网络IO,耗时不可控,会长时间占用数据库连接,高并发下瞬间打爆连接池。

    • 外部调用失败时回滚,但外部调用可能已经部分生效(例如对方扣款成功,但返回超时)。此时本地回滚了,外部却已扣款,导致资金损失

    • 外部调用成功但本地提交瞬间断电?依然不一致。

  • 结论:仅适合低并发、外部响应极快、且外部接口支持完美幂等和事务补偿的极少数场景,通常不推荐。

方案3:先创建内部数据,再调用外部,失败异步补偿(推荐)
  • 逻辑

    1. 本地事务:保存本地数据,同时保存一条“待同步外部”的任务记录(状态为:待处理)。

    2. 事务提交后,再尝试调用第三方接口。

    3. 若调用成功:更新任务状态为“成功”,结束。

    4. 若调用失败(网络异常、超时、业务失败):不做回滚。本地数据保留(状态可能为“处理中”),任务状态保持“待处理”。

    5. 定时Job扫描“待处理”的任务,进行重试补偿,直到成功或人工介入。

为什么方案3是最佳实践?(核心优势)

  1. 保护内部数据库:本地事务非常快(只写本地表和任务表),不会因为外部接口的延迟或超时而阻塞数据库连接。这是高并发系统的底线。

  2. 最终一致性:通过Job重试,保证本地成功的数据最终一定会同步到外部。虽然短时间内可能不一致(例如本地订单状态为“同步中”,外部还没有),但最终会一致

  3. 幂等保障:Job重试要求外部接口必须支持幂等(同一个请求多次调用效果等于一次)。通过唯一的业务流水号(如本地订单号)实现。

  4. 可追溯、可恢复:所有的失败记录都留存在中间表中,不会丢失。Job可以记录重试次数、失败原因,失败多次后还能发出告警,由人工介入处理。方案2一旦失败,连痕迹都没有,排查问题如同大海捞针。

  5. 适用于绝大多数分布式场景:这是事务消息本地消息表模式的核心思想,是分布式系统中处理跨系统数据一致性的标准解决方案。阿里、腾讯等大厂内部大量使用。

方案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 生成,内容仅供参考,请仔细甄别。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐