Skip to content

订单到期关闭实现方案

1. 方案选择与对比

方案优点缺点适用场景
延迟队列实时性高,精准控制关闭时间依赖消息队列可靠性,需处理消息堆积高并发、实时性要求高
定时任务实现简单,无额外中间件依赖实时性低,频繁扫描增加数据库压力低频场景,订单量较小
Redis过期键基于内存,性能高数据持久化风险,需结合数据库兜底中小规模,需快速响应

2. 基于延迟队列的实现(推荐)

2.1 技术选型
  • 消息队列:RabbitMQ(延迟插件) / RocketMQ(定时消息) / Kafka(时间轮+外部存储)。
  • 核心流程
    1. 订单创建时发送延迟消息:设置消息延迟时间为订单超时期限(如30分钟)。
    2. 消费者处理到期消息:检查订单状态,若未支付则关闭订单并释放库存。
2.2 RabbitMQ实现示例
  1. 启用延迟插件
    bash
    rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  2. 声明延迟交换机
    java
    @Bean
    public CustomExchange orderDelayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange("order.delay.exchange", "x-delayed-message", true, false, args);
    }
  3. 订单服务发送延迟消息
    java
    public void sendDelayMessage(String orderId, long delayMs) {
        rabbitTemplate.convertAndSend(
            "order.delay.exchange", 
            "order.delay.routingkey", 
            orderId, 
            message -> {
                message.getMessageProperties().setDelay((int) delayMs);
                return message;
            }
        );
    }
  4. 消费者处理消息
    java
    @RabbitListener(queues = "order.close.queue")
    public void handleOrderClose(String orderId) {
        Order order = orderService.getById(orderId);
        if (order.getStatus() == OrderStatus.UNPAID) {
            orderService.closeOrder(orderId); // 关闭订单并释放库存
        }
    }
2.3 可靠性保障
  • 消息持久化:交换机、队列、消息均设置持久化(durable=true)。
  • 消费幂等性:通过订单状态校验避免重复关闭。
  • 死信队列:处理失败的消息转入死信队列,人工介入或自动重试。

3. 基于定时任务的实现

3.1 Spring Task调度
java
@Scheduled(fixedDelay = 30000) // 每30秒执行一次
public void scanExpiredOrders() {
    Page<Order> page = orderMapper.selectExpiredOrders(OrderStatus.UNPAID, LocalDateTime.now().minusMinutes(30));
    for (Order order : page.getRecords()) {
        closeOrder(order.getId());
    }
}

SQL示例

sql
SELECT * FROM order 
WHERE status = 'UNPAID' 
AND create_time <= NOW() - INTERVAL 30 MINUTE 
LIMIT 100;
3.2 优化策略
  • 分页查询:避免一次性加载大量数据,使用LIMIT分页。
  • 分布式锁:通过Redis锁防止多实例重复执行。
    java
    String lockKey = "scan_expired_orders_lock";
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
    if (locked) {
        try {
            scanExpiredOrders();
        } finally {
            redisTemplate.delete(lockKey);
        }
    }

4. 基于Redis过期键的实现

4.1 键设计与监听
  1. 订单创建时设置过期键
    java
    String key = "order:close:" + orderId;
    redisTemplate.opsForValue().set(key, orderId, 30, TimeUnit.MINUTES);
  2. 订阅过期事件
    java
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory factory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener((message, pattern) -> {
            String expiredKey = new String(message.getBody());
            String orderId = expiredKey.split(":")[2];
            closeOrder(orderId);
        }, new PatternTopic("__keyevent@0__:expired"));
        return container;
    }

注意:需配置Redis的notify-keyspace-events Ex参数启用键过期事件。

4.2 数据兜底
  • 数据库校验:Redis触发关闭后,需再次查询数据库确认订单状态,避免误关。

5. 核心业务逻辑(关闭订单)

java
@Transactional
public void closeOrder(String orderId) {
    // 1. 校验订单状态
    Order order = orderMapper.selectByIdForUpdate(orderId); // 悲观锁
    if (order.getStatus() != OrderStatus.UNPAID) {
        return;
    }
    
    // 2. 更新订单状态为关闭
    order.setStatus(OrderStatus.CLOSED);
    orderMapper.updateById(order);
    
    // 3. 释放库存(异步消息)
    stockService.releaseStock(order.getSkuId(), order.getQuantity());
    
    // 4. 发送通知(MQ或短信)
    notifyService.sendOrderCloseAlert(order.getUserId());
}

6. 性能与容灾

  • 批量处理:每次关闭100-500笔订单,减少数据库事务开销。
  • 熔断降级:Hystrix保护关闭订单接口,异常时降级为记录日志后重试。
  • 监控报警:Prometheus监控关闭订单的TPS、失败率,异常时触发告警。

7. 架构图

订单创建 → 发送延迟消息 → 消息队列(RabbitMQ/RocketMQ)

                          消费者 → 关闭订单 → 释放库存 + 通知用户
定时任务 → 分页扫描超时订单 → 数据库(MySQL) 

                          关闭订单服务

总结

  • 高并发场景:优先选择延迟队列(如RocketMQ定时消息),保障实时性。
  • 中小规模:使用Redis过期键+定时任务兜底,平衡开发成本和性能。
  • 关键要点:幂等处理、分布式锁、异步释放库存、监控告警。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我