订单超时取消的四种实现方案
概述
在电商系统中,订单超时自动取消是保证系统健壮性和用户体验的重要功能。当用户下单后未在规定时间内完成支付,系统需要自动取消订单以释放库存和资源。本文详细介绍四种常见的实现方案,分析其优缺点,并提供最佳实践建议。
方案一:基于Redis List + 定时任务轮询
实现原理
通过Redis List作为消息队列,配合Spring的@Scheduled定时任务,定期轮询并处理超时订单。订单创建时加入队列,定时任务每分钟扫描队列并处理。
核心代码
// 1. 订单创建时入队
public class OrderService {
public void createOrder(Order order) {
// ... 订单创建逻辑
// 加入自动未支付自动取消队列
redisUtil.lPush(Constants.ORDER_AUTO_CANCEL_KEY, storeOrder.getOrderId());
}
}
// 2. 定时任务配置
@Component
public class OrderAutoCancelTask {
private static final Logger logger = LoggerFactory.getLogger(OrderAutoCancelTask.class);
@Scheduled(fixedDelay = 1000 * 60L) // 1分钟执行一次
public void init() {
logger.info("---OrderAutoCancelTask task------produce Data with fixed rate task: Execution Time - {}",
DateUtil.nowDateTime());
try {
orderTaskService.autoCancel();
} catch (Exception e) {
e.printStackTrace();
logger.error("OrderAutoCancelTask.task" + " | msg : " + e.getMessage());
}
}
}
// 3. 订单处理服务
@Service
public class OrderTaskServiceImpl implements OrderTaskService {
@Override
public void autoCancel() {
String redisKey = Constants.ORDER_AUTO_CANCEL_KEY;
Long size = redisUtil.getListSize(redisKey);
logger.info("OrderTaskServiceImpl.autoCancel | size:" + size);
if (size < 1) {
return;
}
for (int i = 0; i < size; i++) {
// 如果10秒钟拿不到一个数据,退出循环
Object data = redisUtil.getRightPop(redisKey, 10L);
if (null == data) {
continue;
}
try {
StoreOrder storeOrder = storeOrderService.getByOderId(String.valueOf(data));
if (ObjectUtil.isNull(storeOrder)) {
logger.error("OrderTaskServiceImpl.autoCancel | 订单不存在,orderNo: " + data);
throw new CrmebException("订单不存在,orderNo: " + data);
}
boolean result = storeOrderTaskService.autoCancel(storeOrder);
if (!result) {
// 处理失败,重新入队
redisUtil.lPush(redisKey, data);
}
} catch (Exception e) {
e.printStackTrace();
// 异常情况重新入队
redisUtil.lPush(redisKey, data);
}
}
}
}
优缺点分析
优点:
- 实现简单,技术栈要求低
- 不依赖特殊的数据结构
- 代码逻辑清晰
- 部署和维护成本低
缺点:
- 轮询方式有延迟(最大延迟=轮询间隔)
- Redis压力较大(频繁的list操作)
- 没有真正的延迟时间控制
- 消息可能丢失,可靠性一般
方案二:基于Redisson延迟队列
实现原理
利用Redisson的RDelayedQueue实现精确的延迟消息处理,订单创建时设置固定的延迟时间。基于Redis的zset实现,支持分布式部署。
核心代码
// 1. 订单创建时加入延迟队列
@Service
public class OrderServiceImpl {
@Resource
private RedissonClient redissonClient;
public void createOrder(OrderDTO orderDTO) {
// ... 订单创建逻辑
String orderSn = generateOrderSn();
// 加入延时队列,30分钟自动取消
try {
RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(
ShopConstants.REDIS_ORDER_OUTTIME_UNPAY_QUEUE);
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
delayedQueue.offer(
OrderMsg.builder().orderId(orderSn).build(),
ShopConstants.ORDER_OUTTIME_UNPAY,
TimeUnit.MINUTES
);
String delayTime = TimeUnit.SECONDS.toSeconds(ShopConstants.ORDER_OUTTIME_UNPAY) + "分钟";
log.info("添加延时队列成功,延迟时间:{} 订单编号:{}", delayTime, orderSn);
} catch (Exception e) {
log.error("添加延时队列失败:{}", e.getMessage());
}
}
}
// 2. 延迟队列监听器
@Component
@Slf4j
public class OrderUnPayListener implements DelayedQueueListener<OrderMsg> {
@Resource
private AppStoreOrderService appStoreOrderService;
@Override
public String delayedQueueKey() {
return ShopConstants.REDIS_ORDER_OUTTIME_UNPAY_QUEUE;
}
@Override
public void consume(OrderMsg message) throws Exception {
if (ObjectUtil.isNotNull(message) && StrUtil.isNotEmpty(message.getOrderId())) {
TenantUtils.executeIgnore(() -> {
appStoreOrderService.cancelOrder(message.getOrderId(), null);
log.info("订单编号:{} 自动取消订单成功", message.getOrderId());
});
}
}
}
// 3. 消息实体
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderMsg {
private String orderId;
private Long createTime;
private String orderType;
}
优缺点分析
优点:
- 延迟时间精确
- 性能较好(基于Redis的zset实现)
- 消息可靠性高
- 支持分布式部署
- 内置重试机制
缺点:
- 依赖Redisson框架
- 配置相对复杂
- Redisson的延迟队列是固定延迟,无法动态调整
- 需要额外学习Redisson API
方案三:基于数据库查询 + 定时任务
实现原理
通过定时任务直接查询数据库中超时的订单进行处理,配置灵活,支持动态调整。订单创建时间与当前时间比较,查询未支付且创建时间超过阈值的订单。
核心代码
// 1. 定时任务执行器
@Component
@Slf4j
public class CancelOrderTaskExecute implements EveryMinuteExecute {
@Autowired
private OrderService orderService;
@Autowired
private SettingService settingService;
@Override
public void execute() {
// 从配置获取自动取消时间
Setting setting = settingService.get(SettingEnum.ORDER_SETTING.name());
OrderSetting orderSetting = JSONUtil.toBean(setting.getSettingValue(), OrderSetting.class);
if (orderSetting != null && orderSetting.getAutoCancel() != null) {
// 计算取消时间点
DateTime cancelTime = DateUtil.offsetMinute(DateUtil.date(), -orderSetting.getAutoCancel());
// 构建查询条件
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Order::getOrderStatus, OrderStatusEnum.UNPAID.name());
queryWrapper.le(Order::getCreateTime, cancelTime);
// 查询超时订单
List<Order> list = orderService.list(queryWrapper);
List<String> cancelSnList = list.stream()
.map(Order::getSn)
.collect(Collectors.toList());
// 批量取消订单
for (String sn : cancelSnList) {
orderService.systemCancel(sn, "超时未支付自动取消");
}
}
}
}
// 2. 配置实体
@Data
public class OrderSetting {
/**
* 自动取消时间(分钟)
* 下单后未支付,订单自动取消的时间
*/
private Integer autoCancel;
/**
* 自动收货时间(天)
*/
private Integer autoReceive;
/**
* 售后自动取消时间(天)
*/
private Integer autoCancelAfterSale;
}
// 3. 订单取消服务
@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = Exception.class)
@Override
public void systemCancel(String orderSn, String reason) {
Order order = this.getBySn(orderSn);
if (order == null) {
throw new BusinessException("订单不存在");
}
if (!OrderStatusEnum.UNPAID.name().equals(order.getOrderStatus())) {
log.info("订单[{}]状态为{},不满足自动取消条件", orderSn, order.getOrderStatus());
return;
}
// 更新订单状态
order.setOrderStatus(OrderStatusEnum.CANCELLED.name());
order.setCancelReason(reason);
order.setCancelTime(new Date());
this.updateById(order);
// 释放库存
releaseStock(order);
// 记录操作日志
saveOrderLog(order, "系统自动取消", reason);
log.info("订单[{}]自动取消成功,原因:{}", orderSn, reason);
}
}
优缺点分析
优点:
- 配置灵活,可动态调整取消时间
- 无需额外中间件
- 实现简单直观
- 支持复杂的查询条件
- 天然支持动态配置
缺点:
- 数据库压力大(频繁查询)
- 时间精度依赖于定时任务间隔
- 性能随数据量增长而下降
- 需要处理分布式锁问题
- 需要优化数据库索引
方案四:基于RabbitMQ死信队列
实现原理
利用RabbitMQ的死信队列(Dead Letter Exchange)特性,将超时未支付的订单消息发送到延迟队列,消息过期后自动转发到死信队列进行消费。支持消息持久化和重试机制。
核心代码
// 1. RabbitMQ配置
@Configuration
public class RabbitMQConfig {
// 订单超时取消交换机
@Bean
public DirectExchange orderCancelExchange() {
return new DirectExchange("order.cancel.exchange");
}
// 订单超时取消队列
@Bean
public Queue orderCancelQueue() {
return QueueBuilder.durable("order.cancel.queue")
.withArgument("x-dead-letter-exchange", "order.cancel.dlx")
.withArgument("x-dead-letter-routing-key", "order.cancel.dlx.key")
.withArgument("x-message-ttl", 30 * 60 * 1000) // 30分钟TTL
.build();
}
// 死信交换机
@Bean
public DirectExchange orderCancelDLX() {
return new DirectExchange("order.cancel.dlx");
}
// 死信队列
@Bean
public Queue orderCancelDLQ() {
return QueueBuilder.durable("order.cancel.dlq").build();
}
// 绑定关系
@Bean
public Binding bindingOrderCancel() {
return BindingBuilder.bind(orderCancelQueue())
.to(orderCancelExchange())
.with("order.cancel.key");
}
@Bean
public Binding bindingOrderCancelDLQ() {
return BindingBuilder.bind(orderCancelDLQ())
.to(orderCancelDLX())
.with("order.cancel.dlx.key");
}
}
// 2. 订单创建时发送消息
@Service
public class OrderServiceImpl {
@Resource
private RabbitTemplate rabbitTemplate;
public void createOrder(OrderDTO orderDTO) {
// ... 订单创建逻辑
String orderSn = generateOrderSn();
// 发送延迟消息
OrderCancelMessage message = new OrderCancelMessage();
message.setOrderId(orderSn);
message.setCreateTime(System.currentTimeMillis());
rabbitTemplate.convertAndSend(
"order.cancel.exchange",
"order.cancel.key",
message,
msg -> {
msg.getMessageProperties().setExpiration(String.valueOf(30 * 60 * 1000));
return msg;
}
);
log.info("订单超时取消消息发送成功,订单号:{}", orderSn);
}
}
// 3. 死信队列消费者
@Component
@Slf4j
public class OrderCancelDLQConsumer {
@Resource
private OrderService orderService;
@RabbitListener(queues = "order.cancel.dlq")
public void consumeOrderCancel(OrderCancelMessage message) {
log.info("收到订单超时取消消息:{}", message);
try {
orderService.cancelOrder(message.getOrderId(), "超时未支付自动取消");
log.info("订单[{}]自动取消成功", message.getOrderId());
} catch (Exception e) {
log.error("处理订单超时取消消息失败,订单号:{}", message.getOrderId(), e);
// 可加入重试队列或人工处理
}
}
}
// 4. 动态TTL优化
@Service
public class DynamicOrderCancelService {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private SettingService settingService;
public void sendCancelMessage(String orderId) {
OrderCancelMessage message = new OrderCancelMessage();
message.setOrderId(orderId);
message.setCreateTime(System.currentTimeMillis());
// 动态获取取消时间
Integer cancelMinutes = getCancelMinutes();
long ttl = cancelMinutes * 60 * 1000L;
rabbitTemplate.convertAndSend(
"order.cancel.exchange",
"order.cancel.key",
message,
msg -> {
msg.getMessageProperties().setExpiration(String.valueOf(ttl));
return msg;
}
);
}
public Integer getCancelMinutes() {
Setting setting = settingService.get(SettingEnum.ORDER_SETTING.name());
OrderSetting orderSetting = JSONUtil.toBean(setting.getSettingValue(), OrderSetting.class);
return orderSetting.getAutoCancel();
}
}
优缺点分析
优点:
- 延迟时间精确,基于消息TTL
- 消息可靠性高,支持持久化
- 支持消息重试和死信处理
- 与业务系统解耦
- 支持集群部署和高可用
- 天然支持消息确认机制
缺点:
- 依赖RabbitMQ中间件
- 配置相对复杂
- 需要维护MQ集群
- 消息TTL不可动态调整(需要重新发送消息)
- 系统复杂度增加
方案对比表
| 特性 | 方案一:Redis List轮询 | 方案二:Redisson延迟队列 | 方案三:数据库查询 | 方案四:RabbitMQ死信队列 |
|---|---|---|---|---|
| 实现复杂度 | 简单 | 中等 | 简单 | 中等 |
| 时间精度 | 低(依赖轮询间隔) | 高(精确延迟) | 中(依赖轮询间隔) | 高(精确延迟) |
| 性能 | 中等 | 高 | 低(大数据量时) | 高 |
| 可扩展性 | 好 | 好 | 中等 | 很好 |
| 配置灵活性 | 固定 | 固定 | 高(可动态配置) | 中等 |
| 第三方依赖 | Redis | Redis + Redisson | 无 | RabbitMQ |
| 数据一致性 | 需要额外处理 | 较好 | 需要额外处理 | 好 |
| 消息可靠性 | 中等 | 高 | 低 | 高 |
| 运维成本 | 低 | 中等 | 低 | 中等 |
| 实时性 | 低 | 高 | 中 | 高 |
最佳实践建议
场景推荐
- 中小型系统/初创项目:推荐使用方案三,实现简单,维护成本低,适合快速迭代
- 高并发电商平台:推荐使用方案二或方案四,性能好,延迟精确,可靠性高
- 已有RabbitMQ的系统:推荐使用方案四,与现有架构契合,复用现有基础设施
- 微服务架构:方案四更合适,天然支持服务解耦
- 过渡/临时方案:可使用方案一作为临时解决方案
优化建议
-
混合方案实现:结合多种方案优势
@Component @Slf4j public class HybridOrderCancelService { @Resource private RedissonClient redissonClient; @Resource private OrderService orderService; @Resource private SettingService settingService; /** * 混合方案:延迟队列为主,数据库查询为兜底 */ @Scheduled(cron = "0 */5 * * * ?") // 每5分钟执行一次兜底 public void hybridCancelOrder() { // 1. 处理延迟队列消息(Redisson或RabbitMQ) processDelayQueue(); // 2. 数据库兜底查询 processDbBackup(); } private void processDelayQueue() { // Redisson延迟队列处理逻辑 } private void processDbBackup() { // 数据库兜底查询逻辑 // 查询过去一段时间内应该取消但未取消的订单 } } -
性能优化策略
- 数据库方案:添加复合索引
(order_status, create_time),定期归档历史数据 - Redis方案:使用Redis集群,合理设置过期时间
- 批量处理:合理设置批量处理大小,避免大事务
- 异步处理:取消订单操作可以异步执行,提高响应速度
- 数据库方案:添加复合索引
-
可靠性保障
- 幂等性设计:所有方案都需要考虑幂等性
- 补偿机制:添加手动触发和补偿任务
- 监控告警:监控队列长度、处理延迟、失败率
- 重试机制:失败消息的重试策略
-
可维护性设计
- 配置化:将超时时间、重试次数等参数配置化
- 可观测性:添加完整的日志、指标、追踪
- 版本兼容:消息格式版本化,支持向前兼容
实施建议
- POC验证:在正式使用前,先进行小规模POC验证
- 灰度发布:分批次、分流量逐步上线
- 回滚方案:设计完整的回滚方案
- 压力测试:模拟高并发场景进行压力测试
- 容灾演练:定期进行故障演练
总结
订单超时取消是电商系统的重要功能,选择合适的实现方案需要综合考虑多个因素:
- 技术栈:选择团队熟悉且已有基础的技术
- 业务规模:根据并发量和数据量选择合适的方案
- 维护成本:考虑长期维护的复杂度和成本
- 可靠性要求:根据业务重要性选择不同可靠性方案
- 扩展性需求:考虑未来业务发展和系统演进
对于大部分场景,推荐使用Redisson延迟队列或RabbitMQ死信队列方案,它们在高并发、高可靠性和精确延迟方面表现优异。对于简单场景或快速验证,可以使用数据库查询方案。混合方案能够结合多种方案的优点,是最佳的工程实践。
无论选择哪种方案,都需要注意幂等性、监控、告警、容错等关键点,确保系统稳定可靠运行。
评论区