工作中对接了招商银行模块,但是回调过程中需要考虑很多问题,这里小计一下
网络不可靠!可能出现:
你的服务器临时过载(GC、Full GC)
数据库连接池满
防火墙拦截
代码 bug 导致 500
机房网络抖动
系统必须支持 幂等处理!
1.首先回调不保证,重复发送回调,需要超过3min后定时主动查询状态,需要幂等处理,改成支付成功/退款关闭订单
首先回调默认是含有重试的5s → 10s → 30s → 1m → 5m → 10m → 30m,招商平台发起回调,如果没有相应是会重复发起,所以考虑网络抖动等情况
需要保证幂等性,我使用的是,out_trade_order+状态,如果非待支付状态,直接return
if (order.getStatus() == PAID || order.getStatus() == CLOSED) {return "success"; // 已处理,直接 ACK
}
// 如果是其他异常状态(如 PROCESSING),应记录告警!
if (order.getStatus() != WAITING_PAY) {log.warn("订单状态异常,orderId: {}, status: {}", orderId, order.getStatus());return "success"; // 仍返回 success 避免重试
}
每次创建订单的时候,塞入当前时间+30min=end_time是最大支付时间,只要超过最大支付时间 使用job扫描 待支付订单查询状态是关闭订单还是支付成功状态
2.向下对接下游系统,比如支付系统收到回调,需要向下调用物业系统发送订单成功,怎么保证事务?
首先下游也要考虑幂等,下游宕机,超时怎么办?考虑首先是告警机制+退款补偿
考虑性能,可以使用线程池 异步发送http/rpc请求下游订单 或者 mq消息发送(可靠消息最终一致性)
如果使用mq消息,建议将更改订单状态+mq本地消息表放在同一个事务内,保证向下传递链路完整性, 并且发MQ消息,由消费者重试
// 阶段1:事务内写 DB + 消息表
@Transactional
public void prepareMessage(String orderId) {updateOrderToPaid(orderId);mqTask.insert(new Message(orderId, "CREATE"));try{sendMQ()mqTask.update("SEND")}catch{mqTask.update("FAIL")//可增加告警mq是否出现问题//不抛出异常}
}//这块可以重构
// 阶段2:独立线程/Job 扫描消息表并发 MQ(成功后改消息状态)
@Schedule(3000)private Map<String, Integer> execNotifyJob(List<NotifyTaskEntity> notifyTaskEntityList) throws Exception {int successCount = 0, errorCount = 0, retryCount = 0;for (NotifyTaskEntity notifyTask : notifyTaskEntityList) {// 回调处理 success 成功,error 失败String response = port.groupBuyNotify(notifyTask);// 更新状态判断&变更数据库表回调任务状态if (NotifyTaskHTTPEnumVO.SUCCESS.getCode().equals(response)) {int updateCount = repository.updateNotifyTaskStatusSuccess(notifyTask);if (1 == updateCount) {successCount += 1;}} else if (NotifyTaskHTTPEnumVO.ERROR.getCode().equals(response)) {if (notifyTask.getNotifyCount() > 4) {int updateCount = repository.updateNotifyTaskStatusError(notifyTask);if (1 == updateCount) {errorCount += 1;}} else {int updateCount = repository.updateNotifyTaskStatusRetry(notifyTask);if (1 == updateCount) {retryCount += 1;}}}}Map<String, Integer> resultMap = new HashMap<>();resultMap.put("waitCount", notifyTaskEntityList.size());resultMap.put("successCount", successCount);resultMap.put("errorCount", errorCount);resultMap.put("retryCount", retryCount);return resultMap;}
本地消息任务表可以由Job重试发送,但是注意Rabbitmq本地重试消息会在内存内,而且会丢失
建议开始死信队列,并且告警机制,外加补偿机制!
@Configuration
public class RabbitMQConfig {// 死信交换机@Beanpublic DirectExchange dlxExchange() {return new DirectExchange("dlx.exchange");}// 死信队列@Beanpublic Queue dlqQueue() {return QueueBuilder.durable("dlq.queue").build();}// 主队列:绑定死信@Beanpublic Queue mainQueue() {return QueueBuilder.durable("order.queue").withArgument("x-dead-letter-exchange", "dlx.exchange").withArgument("x-dead-letter-routing-key", "dlq").build();}@Beanpublic Binding dlqBinding() {return BindingBuilder.bind(dlqQueue()).to(dlxExchange()).with("dlq");}
}@RabbitListener(queues = "order.queue")
public void handleOrder(String message, Channel channel, Message msg) throws IOException {try {processOrder(message);channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);} catch (Exception e) {// 拒绝消息,不重新入队 → 进入死信队列channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, false);}
}@RabbitListener(queues = "dlq.queue")
public void handleDlqMessage(String message) {log.error("死信消息,请人工处理: {}", message);// 1. 发企业微信/钉钉告警// 2. 写入监控数据库// 3. 可选:尝试自动重试 N 次(谨慎!)
}@XxlJob("compensateUnsettledGroupOrders")
public void compensateUnsettledGroupOrders() {// 查询:已支付但未回调订单(超过5分钟)List<OrderEntity> orders = orderRepository.findPaidButUnsettledGroupOrders(System.currentTimeMillis() - 5 * 60 * 1000);for (OrderEntity order : orders) {try {settlementService.doSettlement(order.getOrderId(), order.getPayTime());log.info("补偿结算成功: {}", order.getOrderId());} catch (Exception e) {log.error("补偿结算失败: {}", order.getOrderId(), e);// 可记录到告警系统}}
}
对账系统 每日跑批:比对银行账单 vs 本地订单,自动修复不一致
全链路追踪 在回调入口打 TraceID,贯穿 MQ、RPC、DB
熔断降级 下游物业系统不可用时,自动跳过并告警(避免阻塞主流程)
第 2 步:配置熔断策略(application.yml)
yaml
编辑
resilience4j:circuitbreaker:instances:propertyService: # 熔断器名称(对应 @CircuitBreaker(name="propertyService"))failure-rate-threshold: 50 # 错误率 >50% 触发熔断minimum-number-of-calls: 5 # 至少5次调用才计算错误率wait-duration-in-open-state: 30s # 熔断后30秒进入半开状态permitted-number-of-calls-in-half-open-state: 3 # 半开时允许3次试探automatic-transition-from-open-to-half-open-enabled: truetimelimiter:instances:propertyService:timeout-duration: 2s # 超过2秒视为失败
第 3 步:封装物业系统调用(带熔断)
java
编辑
@Service
public class PropertyServiceClient {@Autowiredprivate RestTemplate restTemplate;// 🔥 核心:使用 @CircuitBreaker 注解@CircuitBreaker(name = "propertyService", fallbackMethod = "notifyPropertyFallback")@TimeLimiter(name = "propertyService") // 超时控制public CompletableFuture<Void> notifyProperty(String orderId) {return CompletableFuture.runAsync(() -> {// 模拟 HTTP 调用物业系统restTemplate.postForObject("http://property-system/api/order-paid",Map.of("orderId", orderId),Void.class);});}// ⚠️ fallback 方法:熔断时执行public CompletableFuture<Void> notifyPropertyFallback(String orderId, Exception ex) {// 1. 记录告警日志log.error("【熔断触发】物业系统不可用,跳过通知!orderId: {}, 原因: {}", orderId, ex.getMessage());// 2. 发送告警(钉钉/企业微信/邮件)alertService.sendAlert("物业系统熔断", "订单 " + orderId + " 通知失败,请检查!");// 3. 可选:写入补偿任务表(后续 Job 重试)compensationTaskService.addTask(orderId, TaskType.NOTIFY_PROPERTY);// 4. 返回成功(不抛异常,避免影响主流程)return CompletableFuture.completedFuture(null);}
}
或者使用
@Service
public class PropertyServiceClient {@Autowiredprivate RestTemplate restTemplate;// 🔥 核心:定义 Sentinel 资源@SentinelResource(value = "notifyProperty", // 资源名(必须唯一)blockHandler = "notifyPropertyBlock", // 触发熔断/限流时的 fallbackexceptionsToIgnore = { BusinessException.class } // 业务异常不计入熔断)public void notifyProperty(String orderId) {// 模拟调用物业系统(可能超时或抛异常)restTemplate.postForObject("http://property-system/api/order-paid",Map.of("orderId", orderId),Void.class);}// ⚠️ BlockHandler:必须和原方法签名一致(多一个 BlockException 参数)public void notifyPropertyBlock(String orderId, BlockException ex) {log.warn("【Sentinel 熔断】物业系统不可用,跳过通知!orderId: {}", orderId, ex);// 发告警alertService.sendAlert("物业通知熔断", "订单 " + orderId + " 被 Sentinel 熔断");// 可选:加入补偿任务compensationTaskService.addTask(orderId, TaskType.NOTIFY_PROPERTY);}
}
消息轨迹 记录每条消息的发送/消费时间、重试次数(便于排查)