外部系统回调的异步处理架构:接收、落库、MQ消费、推送的完整设计

外部系统回调的异步处理架构:接收、落库、MQ消费、推送的完整设计

一、架构概述

在微服务系统中,经常需要接收外部系统的回调通知(如物流节点推送、支付结果回调、第三方状态变更),然后将处理结果同步给其他下游系统。

这类场景的典型架构是:

复制代码
外部系统回调
    │
    ▼
① API 接口接收(快速响应,不做重逻辑)
    │
    ▼
② 落库保存原始数据(日志表,用于追溯和重试)
    │
    ▼
③ 发 MQ 消息(异步解耦)
    │
    ▼
④ MQ 消费者处理业务逻辑(核心处理,可重试)
    │
    ▼
⑤ 事务提交后通知下游系统(再发 MQ 或调接口)

二、为什么要这样设计

2.1 为什么不在 API 接口中直接处理业务?

直接处理 异步处理
外部系统等待时间长 快速返回成功,外部系统不超时
处理失败时外部系统会重推 失败后内部重试,不依赖外部重推
无法控制并发 MQ 天然限流,可控制消费速度
一次失败全部丢失 数据已落库,随时可重试

2.2 为什么要先落库再发 MQ?

复制代码
场景:如果不落库直接发MQ

外部回调 → 发MQ → MQ丢失(网络抖动)→ 数据永久丢失!
                    ↑ 无法恢复

场景:先落库再发MQ

外部回调 → 落库(数据安全了)→ 发MQ → MQ丢失
                                        ↑ 没关系,定时任务扫描日志表重发

2.3 为什么消费者要用新事务?

MQ 消费者通常使用 @Transactional(propagation = REQUIRES_NEW)

  • 消费失败时事务回滚,MQ 可以重新投递
  • 不影响其他消息的消费
  • 每条消息独立事务,互不干扰

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

三、完整示例:支付结果回调处理

3.1 场景描述

支付平台在用户支付成功后,回调你的系统通知支付结果。你需要:

  1. 接收回调,快速返回
  2. 更新订单状态为"已支付"
  3. 通知仓库系统发货
  4. 通知积分系统加积分

3.2 整体架构图

复制代码
支付平台
    │
    ▼
PaymentCallbackController(API层)
    ├── 保存回调日志到 payment_callback_log 表
    ├── 返回"接收成功"给支付平台
    └── 发 MQ 消息(logId)
            │
            ▼
PaymentCallbackConsumer(MQ消费者)
    ├── 根据 logId 查询日志表获取回调数据
    ├── 更新订单状态(数据库事务)
    ├── 注册事务提交后的操作:
    │   ├── 发 MQ 通知仓库发货
    │   └── 发 MQ 通知积分系统
    └── 更新日志表状态为"处理成功"

3.3 代码实现

第一层:API 接口(快速接收)
java 复制代码
/**
 * 支付回调接口.
 * 职责:接收数据、落库、发MQ,快速返回.
 */
@RestController
@RequestMapping("/api/callback")
public class PaymentCallbackController {

    @Resource
    private PaymentCallbackLogRepository callbackLogRepository;

    @Resource
    private PaymentCallbackMqSender callbackMqSender;

    /**
     * 接收支付平台回调.
     */
    @PostMapping("/payment-notify")
    public CallbackResponse receivePaymentNotify(
            @RequestBody PaymentNotifyDto notifyDto) {
        try {
            // 1. 保存原始回调数据到日志表
            PaymentCallbackLog callbackLog = new PaymentCallbackLog();
            callbackLog.setOrderCode(notifyDto.getOrderCode());
            callbackLog.setPaymentNo(notifyDto.getPaymentNo());
            callbackLog.setInputParams(JSON.toJSONString(notifyDto));
            callbackLog.setStatus("PENDING"); // 待处理
            callbackLog.setReceiveTime(new Date());
            callbackLogRepository.save(callbackLog);

            // 2. 发 MQ 异步处理
            callbackMqSender.send(callbackLog.getId());

            // 3. 快速返回成功(支付平台不会重推)
            return CallbackResponse.success();

        } catch (Exception e) {
            log.error("接收支付回调异常, orderCode:{}",
                notifyDto.getOrderCode(), e);
            // 返回失败,支付平台会重推
            return CallbackResponse.fail("处理异常");
        }
    }
}
第二层:MQ 发送者
java 复制代码
/**
 * 支付回调MQ发送者.
 */
@Component
public class PaymentCallbackMqSender {

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送回调处理消息.
     */
    public void send(Integer logId) {
        rabbitTemplate.convertAndSend(
            "payment-callback-exchange",
            "payment.callback.process",
            logId);
        log.info("支付回调MQ发送, logId:{}", logId);
    }
}
第三层:MQ 消费者(核心业务处理)
java 复制代码
/**
 * 支付回调MQ消费者.
 * 职责:处理核心业务逻辑,更新订单状态,通知下游.
 */
@Component
public class PaymentCallbackConsumer {

    @Resource
    private PaymentCallbackService paymentCallbackService;

    /**
     * 消费支付回调消息.
     */
    @RabbitListener(queues = "payment-callback-queue")
    public void consume(Integer logId) {
        log.info("开始处理支付回调, logId:{}", logId);
        try {
            paymentCallbackService.processCallback(logId);
        } catch (Exception e) {
            log.error("处理支付回调异常, logId:{}", logId, e);
            throw e; // 抛异常让MQ重试
        }
    }
}

/**
 * 支付回调处理服务.
 */
@Service
public class PaymentCallbackServiceImpl
        implements PaymentCallbackService {

    @Resource
    private PaymentCallbackLogRepository callbackLogRepository;

    @Resource
    private OrderRepository orderRepository;

    @Resource
    private WarehouseMqSender warehouseMqSender;

    @Resource
    private PointsMqSender pointsMqSender;

    /**
     * 处理支付回调(新事务,独立于MQ消费框架的事务).
     */
    @Transactional(
        propagation = Propagation.REQUIRES_NEW,
        rollbackFor = Exception.class)
    public void processCallback(Integer logId) {
        // 1. 查询回调日志
        PaymentCallbackLog callbackLog = callbackLogRepository
            .findById(logId).orElse(null);
        if (callbackLog == null) {
            log.warn("回调日志不存在, logId:{}", logId);
            return;
        }

        // 2. 幂等检查:已处理的不重复处理
        if ("SUCCESS".equals(callbackLog.getStatus())) {
            log.info("回调已处理过, logId:{}", logId);
            return;
        }

        // 3. 解析回调数据
        PaymentNotifyDto notifyDto = JSON.parseObject(
            callbackLog.getInputParams(), PaymentNotifyDto.class);

        // 4. 更新订单状态
        Order order = orderRepository.findByOrderCode(
            notifyDto.getOrderCode());
        if (order == null) {
            callbackLog.setStatus("FAILED");
            callbackLog.setErrorMsg("订单不存在");
            callbackLogRepository.save(callbackLog);
            return;
        }

        order.setPaymentStatus("PAID");
        order.setPaymentNo(notifyDto.getPaymentNo());
        order.setPaymentTime(notifyDto.getPaymentTime());
        orderRepository.save(order);

        // 5. 更新日志状态
        callbackLog.setStatus("SUCCESS");
        callbackLogRepository.save(callbackLog);

        // 6. 注册事务提交后通知下游系统
        Integer orderId = order.getId();
        AfterTransactionActionCollector collector =
            new AfterTransactionActionCollector();

        // 通知仓库发货
        collector.addCommitSyncAction(() -> {
            try {
                warehouseMqSender.sendShipOrder(orderId);
            } catch (Exception e) {
                log.warn("通知仓库发货失败, orderId:{}", orderId, e);
            }
        });

        // 通知积分系统加积分
        collector.addCommitSyncAction(() -> {
            try {
                pointsMqSender.sendAddPoints(
                    order.getUserId(), order.getAmount());
            } catch (Exception e) {
                log.warn("通知积分系统失败, orderId:{}", orderId, e);
            }
        });

        TransactionSynchronizationManager
            .registerSynchronization(collector);

        log.info("支付回调处理完成, logId:{}, orderCode:{}",
            logId, notifyDto.getOrderCode());
    }
}
日志表实体
java 复制代码
/**
 * 支付回调日志表.
 */
@Entity
@Table(name = "payment_callback_log")
public class PaymentCallbackLog {
    private Integer id;
    private String orderCode;      // 订单号
    private String paymentNo;      // 支付流水号
    private String inputParams;    // 原始回调数据JSON
    private String status;         // PENDING/SUCCESS/FAILED
    private String errorMsg;       // 失败原因
    private Date receiveTime;      // 接收时间
    private Integer retryCount;    // 重试次数
}

四、关键技术点

4.1 接口幂等性

外部系统可能重复回调(网络超时重试),必须保证多次调用结果一致:

java 复制代码
// API 层幂等:相同数据不重复落库
PaymentCallbackLog existingLog = callbackLogRepository
    .findByPaymentNo(notifyDto.getPaymentNo());
if (existingLog != null) {
    return CallbackResponse.success(); // 已接收过,直接返回成功
}

// 消费者层幂等:已处理的不重复处理
if ("SUCCESS".equals(callbackLog.getStatus())) {
    return; // 已处理过,跳过
}

4.2 MQ 消费失败重试

java 复制代码
// 消费者抛异常 → MQ 框架自动重试
@RabbitListener(queues = "payment-callback-queue")
public void consume(Integer logId) {
    try {
        paymentCallbackService.processCallback(logId);
    } catch (Exception e) {
        log.error("处理失败, logId:{}", logId, e);
        throw e; // 抛出异常,MQ 会重新投递这条消息
    }
}

RabbitMQ 重试策略配置:

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true
          max-attempts: 3        # 最多重试3次
          initial-interval: 5000 # 首次重试间隔5秒
          multiplier: 2          # 每次间隔翻倍

4.3 定时任务兜底补偿

MQ 可能丢消息,需要定时任务扫描"卡住"的数据:

java 复制代码
/**
 * 定时任务:补偿处理卡住的回调.
 * 每5分钟扫描一次状态为 PENDING 且超过10分钟未处理的记录.
 */
@Scheduled(fixedRate = 300000)
public void compensatePendingCallbacks() {
    Date threshold = DateUtils.addMinutes(new Date(), -10);
    List<PaymentCallbackLog> pendingLogs = callbackLogRepository
        .findByStatusAndReceiveTimeBefore("PENDING", threshold);

    for (PaymentCallbackLog callbackLog : pendingLogs) {
        if (callbackLog.getRetryCount() >= 5) {
            // 超过最大重试次数,标记为失败,人工介入
            callbackLog.setStatus("FAILED");
            callbackLog.setErrorMsg("超过最大重试次数");
            callbackLogRepository.save(callbackLog);
            continue;
        }

        // 重新发MQ处理
        callbackLog.setRetryCount(callbackLog.getRetryCount() + 1);
        callbackLogRepository.save(callbackLog);
        callbackMqSender.send(callbackLog.getId());
    }
}

4.4 事务传播行为选择

java 复制代码
// MQ 消费者使用 REQUIRES_NEW:每条消息独立事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processCallback(Integer logId) {
    // 如果处理失败,只回滚这条消息的事务
    // 不影响其他消息的消费
}
传播行为 含义 适用场景
REQUIRED(默认) 加入当前事务,没有则新建 普通 Service 方法
REQUIRES_NEW 总是新建事务,挂起当前事务 MQ 消费者、独立操作
NESTED 嵌套事务(保存点) 部分失败不影响外层

4.5 日志表的状态机

复制代码
PENDING(待处理)
    │
    ├── 处理成功 → SUCCESS
    │
    ├── 处理失败(可重试)→ PENDING(retryCount+1)
    │
    └── 超过最大重试次数 → FAILED(人工介入)

五、异常场景处理

5.1 API 接收成功但 MQ 发送失败

复制代码
解决方案:先落库,定时任务兜底
即使MQ发送失败,定时任务会扫描 PENDING 状态的记录重新发送

5.2 MQ 消费成功但下游通知失败

java 复制代码
// 解决方案:事务提交后通知,失败只记日志
collector.addCommitSyncAction(() -> {
    try {
        warehouseMqSender.sendShipOrder(orderId);
    } catch (Exception e) {
        // 不抛异常,不影响主流程
        // 由下游系统的定时任务或人工补偿
        log.warn("通知仓库失败, orderId:{}", orderId, e);
    }
});

5.3 消费者处理到一半崩溃

复制代码
消费者开始处理 → 更新订单状态 → JVM崩溃!
    │
    ▼
事务未提交 → 自动回滚 → 订单状态未变
    │
    ▼
MQ 消息未 ACK → MQ 重新投递 → 消费者重新处理
    │
    ▼
幂等检查 → 订单状态仍是"未支付" → 正常处理

六、与直接同步调用的对比

6.1 同步方式(不推荐)

java 复制代码
// 外部回调 → 直接处理所有逻辑 → 返回结果
@PostMapping("/payment-notify")
public CallbackResponse receivePaymentNotify(PaymentNotifyDto dto) {
    // 更新订单(可能慢)
    orderService.updatePaymentStatus(dto);
    // 通知仓库(可能超时)
    warehouseFeign.shipOrder(dto.getOrderCode());
    // 通知积分(可能失败)
    pointsFeign.addPoints(dto.getUserId(), dto.getAmount());
    return CallbackResponse.success();
}

问题

  • 外部系统等待时间长(所有操作串行)
  • 任何一步失败,外部系统收到失败响应会重推
  • 无法控制并发
  • 没有重试机制

6.2 异步方式(推荐)

复制代码
外部回调 → 落库 + 发MQ → 立即返回成功(50ms内)
                │
                ▼
        MQ消费者异步处理(不影响外部系统)
                │
                ├── 失败 → MQ重试
                ├── 崩溃 → MQ重新投递
                └── 成功 → 事务后通知下游

七、技术栈总结

技术 在架构中的角色 解决的问题
REST API 接收外部回调 提供标准化的接入点
JPA/MyBatis 数据持久化 保存日志、更新业务数据
RabbitMQ/Kafka 异步解耦 接收和处理解耦,削峰填谷
@Transactional 事务管理 保证数据一致性
AfterTransactionActionCollector 事务后操作 确保数据持久化后再通知下游
定时任务 补偿机制 兜底处理MQ丢失或消费失败
幂等设计 防重复处理 应对外部重推和MQ重试
日志表 数据追溯 记录完整处理链路,支持排查和重试

八、设计原则总结

复制代码
1. 快速响应:API 层只做接收和落库,不做重逻辑
2. 数据先行:先保存数据再发MQ,保证数据不丢
3. 异步解耦:通过MQ解耦接收和处理,互不阻塞
4. 幂等设计:每一层都要能处理重复请求
5. 事务隔离:MQ消费者用独立事务,失败不影响其他消息
6. 事务后通知:下游通知在事务提交后执行,保证数据可见性
7. 兜底补偿:定时任务扫描异常数据,保证最终一致性
8. 可追溯:日志表记录完整链路,支持问题排查和数据恢复
相关推荐
我是一颗柠檬14 小时前
【MySQL全面教学】MySQL存储过程与函数Day11(2026年)
数据库·后端·mysql
ting945200014 小时前
ModelHub 深度技术解析:macOS 原生菜单栏 LLM 模型管理工具,补齐 Ollama/MLX/LM Studio 生态短板
人工智能·macos·架构·策略模式
“码”力全开14 小时前
【架构深析】基于 Docker 与边缘计算的 AI 视频管理平台:从 GB28181/RTSP 统一接入到源码交付的闭环演进
人工智能·docker·架构
程序猿乐锅14 小时前
【MySQL | 第三篇】MySQL存储引擎详解
数据库·mysql
TDengine (老段)14 小时前
TDengine 数据文件格式 — TSDB 文件集的物理结构与块编码
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
热爱Liunx的丘丘人14 小时前
搭建一个 Web + 数据库系统(Nginx+PHP+MySQL)
数据库·nginx·php
战族狼魂14 小时前
Powabase 新手快速入门与实战指南
数据库
whn197714 小时前
达梦数据文件的移动或改名
数据库
cfm_291414 小时前
了解Redis
数据库·redis·缓存