侧边栏壁纸
博主头像
峰峰火火博主等级

一条咸鱼罢了

  • 累计撰写 130 篇文章
  • 累计创建 97 个标签
  • 累计收到 59 条评论

目 录CONTENT

文章目录

订单超时取消的四种实现方案

峰峰火火
2026-02-05 / 0 评论 / 0 点赞 / 9 阅读 / 3,255 字 / 正在检测是否收录...
温馨提示:
若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

订单超时取消的四种实现方案

概述

在电商系统中,订单超时自动取消是保证系统健壮性和用户体验的重要功能。当用户下单后未在规定时间内完成支付,系统需要自动取消订单以释放库存和资源。本文详细介绍四种常见的实现方案,分析其优缺点,并提供最佳实践建议。

方案一:基于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
数据一致性 需要额外处理 较好 需要额外处理
消息可靠性 中等
运维成本 中等 中等
实时性

最佳实践建议

场景推荐

  1. 中小型系统/初创项目:推荐使用方案三,实现简单,维护成本低,适合快速迭代
  2. 高并发电商平台:推荐使用方案二或方案四,性能好,延迟精确,可靠性高
  3. 已有RabbitMQ的系统:推荐使用方案四,与现有架构契合,复用现有基础设施
  4. 微服务架构:方案四更合适,天然支持服务解耦
  5. 过渡/临时方案:可使用方案一作为临时解决方案

优化建议

  1. 混合方案实现:结合多种方案优势

    @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() {
            // 数据库兜底查询逻辑
            // 查询过去一段时间内应该取消但未取消的订单
        }
    }
    
  2. 性能优化策略

    • 数据库方案:添加复合索引(order_status, create_time),定期归档历史数据
    • Redis方案:使用Redis集群,合理设置过期时间
    • 批量处理:合理设置批量处理大小,避免大事务
    • 异步处理:取消订单操作可以异步执行,提高响应速度
  3. 可靠性保障

    • 幂等性设计:所有方案都需要考虑幂等性
    • 补偿机制:添加手动触发和补偿任务
    • 监控告警:监控队列长度、处理延迟、失败率
    • 重试机制:失败消息的重试策略
  4. 可维护性设计

    • 配置化:将超时时间、重试次数等参数配置化
    • 可观测性:添加完整的日志、指标、追踪
    • 版本兼容:消息格式版本化,支持向前兼容

实施建议

  1. POC验证:在正式使用前,先进行小规模POC验证
  2. 灰度发布:分批次、分流量逐步上线
  3. 回滚方案:设计完整的回滚方案
  4. 压力测试:模拟高并发场景进行压力测试
  5. 容灾演练:定期进行故障演练

总结

订单超时取消是电商系统的重要功能,选择合适的实现方案需要综合考虑多个因素:

  1. 技术栈:选择团队熟悉且已有基础的技术
  2. 业务规模:根据并发量和数据量选择合适的方案
  3. 维护成本:考虑长期维护的复杂度和成本
  4. 可靠性要求:根据业务重要性选择不同可靠性方案
  5. 扩展性需求:考虑未来业务发展和系统演进

对于大部分场景,推荐使用Redisson延迟队列RabbitMQ死信队列方案,它们在高并发、高可靠性和精确延迟方面表现优异。对于简单场景或快速验证,可以使用数据库查询方案。混合方案能够结合多种方案的优点,是最佳的工程实践。

无论选择哪种方案,都需要注意幂等性、监控、告警、容错等关键点,确保系统稳定可靠运行。

0

评论区