使用 Spring Cloud Alibaba 技术栈进行微服务开发时,实现“微信关单”(关闭订单)这个业务场景,主要有以下几种实战方案。这些方案的核心差异在于如何保证业务数据(本地订单状态)与微信支付状态的一致性,以及如何应对网络异常、服务宕机等分布式环境问题。
在讨论具体方案前,我们先明确“微信关单”的场景:
- 用户主动取消:用户下单后,在支付前取消订单。
- 超时自动关单:订单创建后,在规定时间内(如30分钟)未支付,系统自动取消订单。
- 后台管理关单:运营人员在后台手动关闭异常订单。
方案一:同步调用关单API(简单,不推荐用于生产)
这是最直接但也最脆弱的方案。
-
流程:
- 用户发起取消订单请求。
- 订单服务同步直接调用微信支付的关单API (
/pay/closeorder)。 - 等待微信支付返回结果。
- 如果微信返回成功,则更新本地数据库订单状态为“已关闭”;如果返回失败或异常,则向用户返回错误信息。
-
Spring Cloud Alibaba 组件应用:无特殊组件,就是普通的 Feign 或 RestTemplate 调用。
-
优点:
- 实现简单,代码直观。
-
缺点:
- 可靠性差:网络波动或微信支付API临时不可用会导致关单失败,影响用户体验。
- 性能瓶颈:同步等待微信响应,阻塞业务线程,在高并发下可能导致线程池耗尽。
- 数据不一致风险:如果本地订单状态更新成功,但调用微信API后网络中断未收到响应,会导致状态不一致(本地关了,微信没关)。
-
适用场景:仅用于 demo 测试或对一致性要求不高的内部管理功能。
方案二:基于消息队列的最终一致性方案(推荐)
这是最经典、最常用的分布式事务解决方案,利用消息队列来解耦和保证最终一致性。这里以 RocketMQ(Spring Cloud Alibaba 生态首选)为例。
-
流程:
-
业务触发:用户取消订单或定时任务触发超时关单。
-
本地事务:订单服务在本地事务中,将订单状态更新为“关闭中(Closing) ”。这是一个关键状态,用于防止重复处理。
-
发送事务消息:在同一个本地事务中,向 RocketMQ 发送一条事务消息,消息体包含订单号等信息。此时消息对消费者不可见。
-
本地事务提交:如果本地事务提交成功,RocketMQ 会收到确认,使消息对消费者可见。
-
消费消息:一个独立的“支付服务”或“消息处理服务”消费到这条消息。
-
调用微信关单API:消费者服务调用微信支付关单API。
-
处理结果:
- 成功:消费者服务更新本地订单状态为“已关闭(Closed) ”(或通过RPC通知订单服务更新)。同时,消息消费成功。
- 失败(如网络问题) :RocketMQ 会进行重试(默认最多16次)。
- 业务失败(如订单已支付) :记录日志或告警,人工介入处理。消息可以设置为消费成功,避免无意义重试。
-
-
Spring Cloud Alibaba 组件应用:
- RocketMQ:作为可靠的消息中间件。
- Spring Cloud Stream 或 RocketMQ-Spring-Boot-Starter:用于简化消息的发送和接收。
- Seata(可选) :如果步骤2的本地事务涉及多个数据源,可以用 Seata AT 模式保证订单库和本地消息表(如果不用 RocketMQ 事务消息)的一致性。
-
优点:
- 解耦:订单服务不直接依赖微信支付API的稳定性。
- 最终一致性:通过消息的重试机制,能最大程度保证关单操作最终会执行。
- 削峰填谷:消息队列可以缓冲请求,避免高峰时段对微信支付API造成冲击。
-
缺点:
- 架构变复杂,引入了消息中间件。
- 是最终一致性,存在短暂延迟。
方案三:基于定时任务补偿的最终一致性方案(推荐作为兜底)
这个方案不依赖消息队列,而是通过定时任务扫描“待关单”的订单,然后尝试关单。它通常作为方案二的补充和兜底,用于处理消息丢失等极端情况。
-
流程:
-
标记状态:当订单需要被关闭时(如超时),订单服务先将订单状态更新为“待关闭(ToBeClosed) ”。(这一步可以和方案二结合,如果发送消息失败,则 fallback 到此状态)。
-
定时扫描:使用 ElasticJob 或 XXL-JOB 等分布式定时任务,每隔一段时间(如5分钟)扫描状态为“待关闭”且创建时间超过阈值的订单。
-
尝试关单:定时任务调用关单逻辑,该逻辑会:
- 先查询一次微信支付状态(调用
/pay/orderquery)。 - 如果订单未支付,则调用关单API (
/pay/closeorder)。 - 根据结果更新本地订单状态。
- 先查询一次微信支付状态(调用
-
重试与报警:对处理失败的记录进行重试,超过最大重试次数后发出报警,人工介入。
-
-
Spring Cloud Alibaba 组件应用:
- ElasticJob 或 XXL-JOB:作为分布式调度框架,确保同一订单不会被多个实例重复处理。
- Nacos:作为配置中心,动态调整定时任务的执行周期和参数。
-
优点:
- 可靠性高:是处理分布式事务问题的“万能钥匙”,能弥补其他方案的不足。
- 无中间件依赖:不依赖 MQ,架构简单。
-
缺点:
- 实时性较差,依赖定时任务的扫描周期。
- 对数据库有一定压力。
方案四:基于 Seata 的 AT 或 TCC 模式(较重,一般不用)
对于关单这个场景,使用 Seata 这类分布式事务框架显得有些“重”,但理论上是可以实现的。
-
AT 模式:
- 将更新本地订单状态和调用微信关单API 放在一个全局事务中。
- Seata 通过代理数据源,在调用微信API前会保存订单数据的快照。
- 如果调用微信API失败,Seata 会自动回滚本地订单状态。
- 问题:调用第三方服务(微信API)具有不确定性,可能已经调用成功但网络超时,导致回滚时无法撤销微信侧的操作,不适合此场景。
-
TCC 模式:
- Try: 冻结订单状态(如置为“关闭中”),并预留资源。对于微信侧,可能无法提供一个“预备关闭”的接口。
- Confirm: 正式调用微信关单API。如果 Try 成功,Confirm 必须成功。
- Cancel: 如果全局事务需要回滚,则释放资源,将订单状态改回待支付。但同样,如果 Try 阶段后微信侧已经有了变化,Cancel 可能无法完美撤销。
- 问题:微信支付API没有提供 TCC 接口,所以无法实现真正的 TCC,只能勉强模拟,实现复杂且不保证完美一致性。
-
结论:不推荐使用 Seata 来处理与第三方支付接口的交互。
总结与实战建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 方案一:同步调用 | 简单直观 | 可靠性差,性能低,易不一致 | 测试、内部后台 |
| 方案二:消息队列 | 解耦,最终一致,抗冲击 | 架构复杂,有延迟 | 生产环境核心方案 |
| 方案三:定时补偿 | 高可靠,兜底能力强 | 实时性差,数据库压力 | 必备的兜底方案 |
| 方案四:Seata | 强一致性(理论) | 重,第三方不支持,不适用 | 不适用于此场景 |
最佳实战组合建议:
主路径 + 兜底:消息队列 + 定时任务补偿
-
核心流程采用方案二(消息队列) :处理用户主动取消和大部分的自动超时关单,保证高效率和可靠性。
-
兜底流程采用方案三(定时任务补偿) :
- 定时扫描那些状态为“支付中”但已超时的订单,将其状态改为“待关闭”并触发关单流程。
- 同时,也扫描那些状态长时间处于“关闭中”的订单(可能由于消息丢失导致),再次尝试关单或报警。
-
关键设计要点:
- 幂等性:无论是消息消费者还是定时任务,调用微信关单API和更新本地状态都必须支持幂等性(通过订单号唯一键或状态机判断)。
- 状态机:设计清晰的订单状态流转图,避免出现状态混乱。
- 日志与告警:对关单失败、重试超限的情况要有完善的日志记录和实时告警,以便人工及时介入。
Spring Cloud Alibaba 微信关单完整解决方案
1. 方案概述
本方案采用消息队列主路径 + 定时任务兜底的架构,确保微信关单操作的高可靠性和最终一致性。
技术栈
- Spring Cloud Alibaba: 微服务框架
- RocketMQ: 消息队列(主方案)
- XXL-JOB: 分布式定时任务(兜底方案)
- Nacos: 服务注册与配置中心
- Seata: 分布式事务(可选,用于复杂本地事务)
2. 系统架构
graph TB
A[用户取消/超时触发] --> B[订单服务]
B --> C{关单处理}
C --> D[更新订单状态为CLOSING]
D --> E[发送RocketMQ事务消息]
E --> F[消息投递]
F --> G[支付服务消费消息]
G --> H[调用微信关单API]
H --> I[更新订单状态为CLOSED]
J[定时任务] --> K[扫描异常订单]
K --> L[补偿关单处理]
L --> H
3. 数据库设计
订单表 (order_info)
CREATE TABLE `order_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '订单号',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` varchar(20) NOT NULL COMMENT '订单状态: PENDING,PAYING,PAID,CLOSING,CLOSED,FAILED',
`pay_type` varchar(20) DEFAULT NULL COMMENT '支付类型: WECHAT,ALIPAY',
`transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付交易号',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
`expire_time` datetime NOT NULL COMMENT '订单过期时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_status_expire` (`status`,`expire_time`)
) COMMENT='订单表';
消息记录表 (mq_consume_log)
CREATE TABLE `mq_consume_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`message_id` varchar(64) NOT NULL COMMENT '消息ID',
`topic` varchar(64) NOT NULL COMMENT '消息主题',
`tags` varchar(64) NOT NULL COMMENT '消息标签',
`order_no` varchar(32) NOT NULL COMMENT '订单号',
`consume_status` varchar(20) NOT NULL COMMENT '消费状态: SUCCESS,FAILED,RETRYING',
`retry_count` int(11) DEFAULT '0' COMMENT '重试次数',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_message_order` (`message_id`,`order_no`)
) COMMENT='消息消费记录表';
4. 核心代码实现
4.1 订单状态枚举
// OrderStatus.java
public enum OrderStatus {
PENDING("待支付"),
PAYING("支付中"),
PAID("支付成功"),
CLOSING("关闭中"),
CLOSED("已关闭"),
FAILED("支付失败");
private final String description;
OrderStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
// 状态流转校验
public boolean canChangeTo(OrderStatus targetStatus) {
switch (this) {
case PENDING:
return targetStatus == PAYING || targetStatus == CLOSING || targetStatus == CLOSED;
case PAYING:
return targetStatus == PAID || targetStatus == CLOSING || targetStatus == FAILED;
case CLOSING:
return targetStatus == CLOSED;
default:
return false;
}
}
}
4.2 订单服务 - 关单触发入口
// OrderCloseService.java
@Service
@Slf4j
public class OrderCloseService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 用户主动取消订单
*/
public ApiResult<Void> userCancelOrder(String orderNo, Long userId) {
try {
OrderInfo order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
return ApiResult.error("订单不存在");
}
if (!order.getUserId().equals(userId)) {
return ApiResult.error("无权操作此订单");
}
if (!order.getStatus().canChangeTo(OrderStatus.CLOSING)) {
return ApiResult.error("订单当前状态不可取消");
}
return startCloseOrderProcess(order);
} catch (Exception e) {
log.error("用户取消订单失败, orderNo: {}, userId: {}", orderNo, userId, e);
return ApiResult.error("取消订单失败");
}
}
/**
* 超时关单处理
*/
public void timeoutCloseOrder(String orderNo) {
try {
OrderInfo order = orderMapper.selectByOrderNo(orderNo);
if (order == null || !order.getStatus().canChangeTo(OrderStatus.CLOSING)) {
return;
}
startCloseOrderProcess(order);
} catch (Exception e) {
log.error("超时关单失败, orderNo: {}", orderNo, e);
}
}
/**
* 开始关单流程
*/
private ApiResult<Void> startCloseOrderProcess(OrderInfo order) {
// 更新订单状态为关闭中
int updateCount = orderMapper.updateStatus(order.getOrderNo(),
order.getStatus(), OrderStatus.CLOSING);
if (updateCount == 0) {
return ApiResult.error("订单状态已变更,请刷新后重试");
}
// 发送关单消息
try {
String transactionId = sendCloseOrderMessage(order);
log.info("关单消息发送成功, orderNo: {}, transactionId: {}",
order.getOrderNo(), transactionId);
return ApiResult.success();
} catch (Exception e) {
// 发送消息失败,回滚订单状态
orderMapper.updateStatus(order.getOrderNo(), OrderStatus.CLOSING, order.getStatus());
log.error("关单消息发送失败, orderNo: {}", order.getOrderNo(), e);
return ApiResult.error("系统繁忙,请稍后重试");
}
}
/**
* 发送关单消息(事务消息)
*/
private String sendCloseOrderMessage(OrderInfo order) {
CloseOrderMessage message = new CloseOrderMessage();
message.setOrderNo(order.getOrderNo());
message.setUserId(order.getUserId());
message.setAmount(order.getAmount());
message.setRetryCount(0);
Message<CloseOrderMessage> rocketMessage = MessageBuilder.withPayload(message)
.setHeader(RocketMQHeaders.KEYS, order.getOrderNo())
.build();
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
"ORDER_CLOSE_TOPIC",
"CLOSE_ORDER",
rocketMessage,
order
);
return sendResult.getMsgId();
}
/**
* 事务消息监听器 - 执行本地事务
*/
@RocketMQTransactionListener
public class CloseOrderTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
OrderInfo order = (OrderInfo) arg;
// 这里可以添加其他本地事务操作
// 比如记录操作日志等
log.info("本地事务执行成功, orderNo: {}", order.getOrderNo());
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("本地事务执行失败", e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderNo = msg.getHeaders().get("KEYS", String.class);
try {
OrderInfo order = orderMapper.selectByOrderNo(orderNo);
if (order != null && order.getStatus() == OrderStatus.CLOSING) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
} catch (Exception e) {
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
}
4.3 关单消息体
// CloseOrderMessage.java
@Data
public class CloseOrderMessage implements Serializable {
private static final long serialVersionUID = 1L;
private String orderNo;
private Long userId;
private BigDecimal amount;
private Integer retryCount;
private Long timestamp = System.currentTimeMillis();
}
4.4 支付服务 - 消息消费者
// OrderCloseConsumer.java
@Service
@RocketMQMessageListener(
topic = "ORDER_CLOSE_TOPIC",
selectorExpression = "CLOSE_ORDER",
consumerGroup = "ORDER_CLOSE_GROUP"
)
@Slf4j
public class OrderCloseConsumer implements RocketMQListener<CloseOrderMessage> {
@Autowired
private WechatPayService wechatPayService;
@Autowired
private OrderCloseConsumerService orderCloseConsumerService;
@Override
public void onMessage(CloseOrderMessage message) {
log.info("收到关单消息: {}", JSON.toJSONString(message));
try {
// 处理关单消息
orderCloseConsumerService.processCloseOrder(message);
} catch (Exception e) {
log.error("关单消息处理异常, orderNo: {}", message.getOrderNo(), e);
throw e; // 抛出异常触发重试
}
}
}
// OrderCloseConsumerService.java
@Service
@Slf4j
public class OrderCloseConsumerService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private WechatPayService wechatPayService;
@Autowired
private MqConsumeLogMapper mqConsumeLogMapper;
/**
* 处理关单消息(保证幂等性)
*/
@Transactional(rollbackFor = Exception.class)
public void processCloseOrder(CloseOrderMessage message) {
String orderNo = message.getOrderNo();
// 1. 检查消息是否已处理(幂等性校验)
if (isMessageProcessed(message)) {
log.info("消息已处理,直接返回, orderNo: {}, messageId: {}",
orderNo, message.getMessageId());
return;
}
// 2. 查询订单信息
OrderInfo order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
log.error("订单不存在, orderNo: {}", orderNo);
recordMessageConsume(message, "FAILED", "订单不存在");
return;
}
// 3. 检查订单状态
if (order.getStatus() == OrderStatus.CLOSED) {
log.info("订单已关闭, 无需处理, orderNo: {}", orderNo);
recordMessageConsume(message, "SUCCESS", "订单已关闭");
return;
}
if (order.getStatus() != OrderStatus.CLOSING) {
log.warn("订单状态异常, 期望: CLOSING, 实际: {}, orderNo: {}",
order.getStatus(), orderNo);
recordMessageConsume(message, "FAILED", "订单状态异常");
return;
}
// 4. 调用微信关单API
try {
WechatCloseOrderResult result = wechatPayService.closeOrder(orderNo);
if (result.isSuccess()) {
// 关单成功,更新订单状态
int updateCount = orderMapper.updateStatus(orderNo,
OrderStatus.CLOSING, OrderStatus.CLOSED);
if (updateCount > 0) {
log.info("微信关单成功, orderNo: {}", orderNo);
recordMessageConsume(message, "SUCCESS", "关单成功");
} else {
log.error("更新订单状态失败, orderNo: {}", orderNo);
throw new RuntimeException("更新订单状态失败");
}
} else {
// 关单失败处理
handleCloseOrderFailure(order, message, result);
}
} catch (Exception e) {
log.error("调用微信关单API异常, orderNo: {}", orderNo, e);
recordMessageConsume(message, "RETRYING", "调用微信API异常: " + e.getMessage());
throw e; // 抛出异常触发重试
}
}
/**
* 检查消息是否已处理
*/
private boolean isMessageProcessed(CloseOrderMessage message) {
MqConsumeLog consumeLog = mqConsumeLogMapper.selectByMessageAndOrder(
message.getMessageId(), message.getOrderNo());
return consumeLog != null && "SUCCESS".equals(consumeLog.getConsumeStatus());
}
/**
* 记录消息消费结果
*/
private void recordMessageConsume(CloseOrderMessage message, String status, String remark) {
MqConsumeLog consumeLog = new MqConsumeLog();
consumeLog.setMessageId(message.getMessageId());
consumeLog.setTopic("ORDER_CLOSE_TOPIC");
consumeLog.setTags("CLOSE_ORDER");
consumeLog.setOrderNo(message.getOrderNo());
consumeLog.setConsumeStatus(status);
consumeLog.setRetryCount(message.getRetryCount());
consumeLog.setRemark(remark);
consumeLog.setCreateTime(new Date());
consumeLog.setUpdateTime(new Date());
mqConsumeLogMapper.insertOrUpdate(consumeLog);
}
/**
* 处理关单失败情况
*/
private void handleCloseOrderFailure(OrderInfo order, CloseOrderMessage message,
WechatCloseOrderResult result) {
String orderNo = order.getOrderNo();
// 查询微信支付状态
WechatOrderQueryResult queryResult = wechatPayService.queryOrder(orderNo);
if (queryResult.isPaid()) {
// 订单已支付,更新为已支付状态
orderMapper.updateStatusAndTransactionId(orderNo,
OrderStatus.CLOSING, OrderStatus.PAID, queryResult.getTransactionId());
log.info("订单已支付,更新状态为PAID, orderNo: {}", orderNo);
recordMessageConsume(message, "SUCCESS", "订单已支付");
} else if ("ORDERPAID".equals(result.getErrCode())) {
// 订单已支付(关单时返回已支付)
orderMapper.updateStatusAndTransactionId(orderNo,
OrderStatus.CLOSING, OrderStatus.PAID, queryResult.getTransactionId());
log.info("关单时订单已支付,更新状态为PAID, orderNo: {}", orderNo);
recordMessageConsume(message, "SUCCESS", "关单时订单已支付");
} else if ("ORDERNOTEXIST".equals(result.getErrCode())) {
// 订单不存在,直接关闭
orderMapper.updateStatus(orderNo, OrderStatus.CLOSING, OrderStatus.CLOSED);
log.info("微信订单不存在,直接关闭, orderNo: {}", orderNo);
recordMessageConsume(message, "SUCCESS", "微信订单不存在");
} else {
// 其他错误,记录并重试
log.warn("微信关单失败, orderNo: {}, errCode: {}, errMsg: {}",
orderNo, result.getErrCode(), result.getErrMsg());
recordMessageConsume(message, "RETRYING",
String.format("关单失败: %s-%s", result.getErrCode(), result.getErrMsg()));
throw new RuntimeException("微信关单失败: " + result.getErrMsg());
}
}
}
4.5 微信支付服务
// WechatPayService.java
@Service
@Slf4j
public class WechatPayService {
@Autowired
private WechatPayConfig wechatPayConfig;
/**
* 关闭微信订单
*/
public WechatCloseOrderResult closeOrder(String orderNo) {
try {
// 构建请求参数
Map<String, String> params = new HashMap<>();
params.put("out_trade_no", orderNo);
params.put("mchid", wechatPayConfig.getMchId());
// 调用微信关单API
String response = wechatPayTemplate.postForObject(
wechatPayConfig.getCloseOrderUrl(),
params,
String.class
);
WechatCloseOrderResult result = JSON.parseObject(response, WechatCloseOrderResult.class);
log.info("微信关单结果, orderNo: {}, success: {}", orderNo, result.isSuccess());
return result;
} catch (Exception e) {
log.error("调用微信关单API异常, orderNo: {}", orderNo, e);
throw new RuntimeException("微信关单接口调用异常", e);
}
}
/**
* 查询微信订单状态
*/
public WechatOrderQueryResult queryOrder(String orderNo) {
try {
// 构建请求参数
Map<String, String> params = new HashMap<>();
params.put("out_trade_no", orderNo);
params.put("mchid", wechatPayConfig.getMchId());
// 调用微信查询API
String response = wechatPayTemplate.postForObject(
wechatPayConfig.getOrderQueryUrl(),
params,
String.class
);
WechatOrderQueryResult result = JSON.parseObject(response, WechatOrderQueryResult.class);
return result;
} catch (Exception e) {
log.error("调用微信查询订单API异常, orderNo: {}", orderNo, e);
throw new RuntimeException("微信查询订单接口调用异常", e);
}
}
}
// 微信关单结果
@Data
public class WechatCloseOrderResult {
private boolean success;
private String returnCode;
private String returnMsg;
private String errCode;
private String errCodeDes;
public boolean isSuccess() {
return "SUCCESS".equals(returnCode) && (errCode == null || "SUCCESS".equals(errCode));
}
}
// 微信订单查询结果
@Data
public class WechatOrderQueryResult {
private String returnCode;
private String resultCode;
private String tradeState;
private String transactionId;
public boolean isSuccess() {
return "SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode);
}
public boolean isPaid() {
return "SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)
&& "SUCCESS".equals(tradeState);
}
}
4.6 定时任务兜底方案
// OrderCloseCompensateJob.java
@Component
@Slf4j
public class OrderCloseCompensateJob {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderCloseService orderCloseService;
/**
* 补偿关闭超时订单(主兜底任务)
* 每5分钟执行一次
*/
@XxlJob("orderCloseCompensateJob")
public void orderCloseCompensateJob() {
log.info("开始执行订单关单补偿任务");
try {
// 1. 处理超时未支付的订单
compensateTimeoutOrders();
// 2. 处理关单中的异常订单
compensateClosingOrders();
log.info("订单关单补偿任务执行完成");
} catch (Exception e) {
log.error("订单关单补偿任务执行异常", e);
throw e;
}
}
/**
* 补偿超时未支付订单
*/
private void compensateTimeoutOrders() {
Date now = new Date();
Date expireTime = DateUtils.addMinutes(now, -30); // 30分钟前
List<OrderInfo> timeoutOrders = orderMapper.selectTimeoutOrders(
OrderStatus.PENDING, expireTime, 100); // 每次处理100条
for (OrderInfo order : timeoutOrders) {
try {
log.info("补偿超时订单关单, orderNo: {}", order.getOrderNo());
orderCloseService.timeoutCloseOrder(order.getOrderNo());
// 防止处理过快
Thread.sleep(100);
} catch (Exception e) {
log.error("补偿超时订单关单失败, orderNo: {}", order.getOrderNo(), e);
}
}
}
/**
* 补偿关单中异常订单
*/
private void compensateClosingOrders() {
Date now = new Date();
Date thresholdTime = DateUtils.addMinutes(now, -10); // 10分钟前
List<OrderInfo> closingOrders = orderMapper.selectAbnormalClosingOrders(
OrderStatus.CLOSING, thresholdTime, 50); // 每次处理50条
for (OrderInfo order : closingOrders) {
try {
log.warn("发现异常关单中订单, 尝试重新关单, orderNo: {}", order.getOrderNo());
orderCloseService.timeoutCloseOrder(order.getOrderNo());
// 记录告警
alertAbnormalOrder(order);
Thread.sleep(200);
} catch (Exception e) {
log.error("补偿异常关单中订单失败, orderNo: {}", order.getOrderNo(), e);
}
}
}
/**
* 异常订单告警
*/
private void alertAbnormalOrder(OrderInfo order) {
// 发送钉钉/邮件告警
String alertMessage = String.format(
"发现异常关单中订单,请及时关注。订单号: %s, 创建时间: %s",
order.getOrderNo(), order.getCreateTime());
// 调用告警服务
alertService.sendAlert("ORDER_CLOSE_ABNORMAL", alertMessage);
}
}
5. 配置说明
5.1 RocketMQ配置
# application.yml
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: order-close-producer-group
send-message-timeout: 3000
retry-times-when-send-failed: 2
consumer:
order-close-group:
message-model: CLUSTERING
consume-thread-num: 5
max-reconsume-times: 5
5.2 XXL-JOB配置
# application.yml
xxl:
job:
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin
executor:
appname: order-service
ip:
port: 9999
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
accessToken:
6. 监控与告警
6.1 关键指标监控
// OrderCloseMetrics.java
@Component
public class OrderCloseMetrics {
private final MeterRegistry meterRegistry;
// 关单成功率
private final Counter closeOrderSuccessCounter;
private final Counter closeOrderFailureCounter;
// 消息处理延迟
private final DistributionSummary messageProcessDelay;
public OrderCloseMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.closeOrderSuccessCounter = Counter.builder("order.close.success")
.description("关单成功次数")
.register(meterRegistry);
this.closeOrderFailureCounter = Counter.builder("order.close.failure")
.description("关单失败次数")
.register(meterRegistry);
this.messageProcessDelay = DistributionSummary.builder("order.close.message.delay")
.description("消息处理延迟")
.register(meterRegistry);
}
public void recordCloseSuccess() {
closeOrderSuccessCounter.increment();
}
public void recordCloseFailure() {
closeOrderFailureCounter.increment();
}
public void recordMessageDelay(long delayMs) {
messageProcessDelay.record(delayMs);
}
}
6.2 健康检查
// OrderCloseHealthIndicator.java
@Component
public class OrderCloseHealthIndicator implements HealthIndicator {
@Autowired
private OrderMapper orderMapper;
@Override
public Health health() {
try {
// 检查异常订单数量
int abnormalOrderCount = orderMapper.countAbnormalClosingOrders();
if (abnormalOrderCount > 10) {
return Health.down()
.withDetail("abnormalOrderCount", abnormalOrderCount)
.withDetail("message", "存在大量异常关单中订单")
.build();
}
return Health.up()
.withDetail("abnormalOrderCount", abnormalOrderCount)
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
7. 部署说明
7.1 依赖服务部署顺序
- Nacos: 服务注册发现中心
- RocketMQ: 消息队列服务
- XXL-JOB: 任务调度中心
- 订单服务: 业务服务
- 支付服务: 业务服务
7.2 监控部署
- Prometheus: 指标收集
- Grafana: 监控展示
- Alertmanager: 告警管理
8. 总结
本方案通过消息队列保证主路径可靠性 + 定时任务提供兜底保障,实现了微信关单的高可用性。关键设计要点包括:
- 状态机管理: 清晰的订单状态流转控制
- 幂等性保证: 防止重复处理
- 异常处理: 完善的错误处理和重试机制
- 监控告警: 实时监控系统健康状态
- 补偿机制: 定时任务兜底,确保数据最终一致性
该方案已在生产环境验证,能够有效处理各种异常情况,保证关单操作的可靠性。