外部系统回调的异步处理架构:接收、落库、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 场景描述
支付平台在用户支付成功后,回调你的系统通知支付结果。你需要:
- 接收回调,快速返回
- 更新订单状态为"已支付"
- 通知仓库系统发货
- 通知积分系统加积分
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. 可追溯:日志表记录完整链路,支持问题排查和数据恢复