订单超时自动取消的7种方案,我用这种!

向量数据库大模型关系型数据库

大家好,我是苏三,又跟大家见面了。

前言

在电商、外卖、票务等系统中,订单超时未支付自动取消是一个常见的需求。

这个功能乍一看很简单,甚至很多初学者会觉得:"不就是加个定时器么?" 但真到了实际工作中,细节的复杂程度往往会超乎预期。

这里我们从基础到高级,逐步分析各种实现方案,最后分享一些在生产中常见的优化技巧,希望对你会有所帮助。

  1. 使用延时队列(DelayQueue)

适用场景: 订单数量较少,系统并发量不高。

延时队列是Java并发包(java.util.concurrent)中的一个数据结构,专门用于处理延时任务。

订单在创建时,将其放入延时队列,并设置超时时间。

延时时间到了以后,队列会触发消费逻辑,执行取消操作。

示例代码:


        
          
import java.util.concurrent.*;  
  
public class OrderCancelService {  
    private static final DelayQueue<OrderTask> delayQueue = new DelayQueue<>();  
  
    public static void main(String[] args) throws InterruptedException {  
        // 启动消费者线程  
        new Thread(() -> {  
            while (true) {  
                try {  
                    OrderTask task = delayQueue.take(); // 获取到期任务  
                    System.out.println("取消订单:" + task.getOrderId());  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
            }  
        }).start();  
  
        // 模拟订单创建  
        for (int i = 1; i <= 5; i++) {  
            delayQueue.put(new OrderTask(i, System.currentTimeMillis() + 5000)); // 5秒后取消  
            System.out.println("订单" + i + "已创建");  
        }  
    }  
  
    static class OrderTask implements Delayed {  
        private final long expireTime;  
        private final int orderId;  
  
        public OrderTask(int orderId, long expireTime) {  
            this.orderId = orderId;  
            this.expireTime = expireTime;  
        }  
  
        public int getOrderId() {  
            return orderId;  
        }  
  
        @Override  
        public long getDelay(TimeUnit unit) {  
            return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);  
        }  
  
        @Override  
        public int compareTo(Delayed o) {  
            return Long.compare(this.expireTime, ((OrderTask) o).expireTime);  
        }  
    }  
}  

      

优点:

  • 实现简单,逻辑清晰。

缺点:

  • 依赖内存,系统重启会丢失任务。
  • 随着订单量增加,内存占用会显著上升。
  1. 基于数据库轮询

适用场景: 订单数量较多,但系统对实时性要求不高。

轮询是最容易想到的方案:定期扫描数据库,将超时的订单状态更新为“已取消”。

示例代码:


        
          
public void cancelExpiredOrders() {  
    String sql = "UPDATE orders SET status = 'CANCELLED' WHERE status = 'PENDING' AND create\_time < ?";  
    try (Connection conn = dataSource.getConnection();  
         PreparedStatement ps = conn.prepareStatement(sql)) {  
        ps.setTimestamp(1, new Timestamp(System.currentTimeMillis() - 30 * 60 * 1000)); // 30分钟未支付取消  
        int affectedRows = ps.executeUpdate();  
        System.out.println("取消订单数量:" + affectedRows);  
    } catch (SQLException e) {  
        e.printStackTrace();  
    }  
}  

      

优点:

  • 数据可靠性强,不依赖内存。
  • 实现成本低,无需引入第三方组件。

缺点:

  • 频繁扫描数据库,会带来较大的性能开销。
  • 实时性较差(通常定时任务间隔为分钟级别)。

优化建议:

  • 为相关字段加索引,避免全表扫描。
  • 结合分表分库策略,减少单表压力。
  1. 基于Redis队列

适用场景: 适合对实时性有要求的中小型项目。

Redis 的 List 或 Sorted Set 数据结构非常适合用作延时任务队列。

我们可以把订单的超时时间作为 Score,订单 ID 作为 Value 存到 Redis 的 ZSet 中,定时去取出到期的订单进行取消。

例子:


        
          
public void addOrderToQueue(String orderId, long expireTime) {  
    jedis.zadd("order\_delay\_queue", expireTime, orderId);  
}  
  
public void processExpiredOrders() {  
    long now = System.currentTimeMillis();  
    Set<String> expiredOrders = jedis.zrangeByScore("order\_delay\_queue", 0, now);  
    for (String orderId : expiredOrders) {  
        System.out.println("取消订单:" + orderId);  
        jedis.zrem("order\_delay\_queue", orderId); // 删除已处理的订单  
    }  
}  

      

优点:

  1. 实时性高。
  2. Redis 的性能优秀,延迟小。

缺点:

  1. Redis 容量有限,适合中小规模任务。

  2. 需要额外处理 Redis 宕机或数据丢失的问题。

  3. Redis Key 过期回调


适用场景: 对超时事件实时性要求高,并且希望依赖 Redis 本身的特性实现简单的任务调度。

Redis 提供了 Key 的过期功能,结合 keyevent 事件通知机制,可以实现订单的自动取消逻辑。

当订单设置超时时间后,Redis 会在 Key 过期时发送通知,我们只需要订阅这个事件并进行相应的处理。

例子:

  1. 设置订单的过期时间:

        
          
public void setOrderWithExpiration(String orderId, long expireSeconds) {  
    jedis.setex("order:" + orderId, expireSeconds, "PENDING");  
}  

      
  1. 订阅 Redis 的过期事件:

        
          
public void subscribeToExpirationEvents() {  
    Jedis jedis = new Jedis("localhost");  
    jedis.psubscribe(new JedisPubSub() {  
        @Override  
        public void onPMessage(String pattern, String channel, String message) {  
            if (channel.equals("\_\_keyevent@0\_\_:expired")) {  
                System.out.println("接收到过期事件,取消订单:" + message);  
                // 执行取消订单的业务逻辑  
            }  
        }  
    }, "\_\_keyevent@0\_\_:expired"); // 订阅过期事件  
}  

      

优点:

  1. 实现简单,直接利用 Redis 的过期机制。
  2. 实时性高,过期事件触发后立即响应。

缺点:

  1. 依赖 Redis 的事件通知功能,需要开启 notify-keyspace-events 配置。
  2. 如果 Redis 中大量使用过期 Key,可能导致性能问题。

注意事项: 要使用 Key 过期事件,需要确保 Redis 配置文件中 notify-keyspace-events 的值包含 Ex。比如:


        
          
notify-keyspace-events Ex  

      
  1. 基于消息队列(如RabbitMQ)

适用场景: 高并发系统,实时性要求高。

订单创建时,将订单消息发送到延迟队列(如RabbitMQ 的 x-delayed-message 插件)。

延迟时间到了以后,消息会重新投递到消费者,消费者执行取消操作。

示例代码(以RabbitMQ为例):


        
          
public void sendOrderToDelayQueue(String orderId, long delay) {  
    Map<String, Object> args = new HashMap<>();  
    args.put("x-delayed-type", "direct");  
    ConnectionFactory factory = new ConnectionFactory();  
    try (Connection connection = factory.newConnection();  
         Channel channel = connection.createChannel()) {  
        channel.exchangeDeclare("delayed\_exchange", "x-delayed-message", true, false, args);  
        channel.queueDeclare("delay\_queue", true, false, false, null);  
        channel.queueBind("delay\_queue", "delayed\_exchange", "order.cancel");  
  
        AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()  
                .headers(Map.of("x-delay", delay)) // 延迟时间  
                .build();  
        channel.basicPublish("delayed\_exchange", "order.cancel", props, orderId.getBytes());  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
}  

      

优点:

  1. 消息队列支持分布式,高并发下表现优秀。
  2. 数据可靠性高,不容易丢消息。

缺点:

  1. 引入消息队列增加了系统复杂性。

  2. 需要处理队列堆积的问题。

  3. 使用定时任务框架


适用场景: 订单取消操作复杂,需要分布式支持。

定时任务框架,比如:Quartz、Elastic-Job,能够高效地管理任务调度,适合处理批量任务。

比如 Quartz 可以通过配置 Cron 表达式,定时执行订单取消逻辑。

示例代码:


        
          
@Scheduled(cron = "0 */5 * * * ?")  
public void scanAndCancelOrders() {  
    System.out.println("开始扫描并取消过期订单");  
    // 这里调用数据库更新逻辑  
}  

      

优点:

  1. 成熟的调度框架支持复杂任务调度。
  2. 灵活性高,支持分布式扩展。

缺点:

  1. 对实时性支持有限。

  2. 框架本身较复杂。

  3. 基于触发式事件流处理


适用场景: 需要处理实时性较高的订单取消,同时结合复杂业务逻辑,例如根据用户行为动态调整超时时间。

可以借助事件流处理框架(如 Apache Flink 或 Spark Streaming),实时地处理订单状态,并触发超时事件。

每个订单生成后,可以作为事件流的一部分,订单未支付时通过流计算触发超时取消逻辑。

示例代码(以 Apache Flink 为例):


        
          
DataStream<OrderEvent> orderStream = env.fromCollection(orderEvents);  
  
orderStream  
    .keyBy(OrderEvent::getOrderId)  
    .process(new KeyedProcessFunction<String, OrderEvent, Void>() {  
        @Override  
        public void processElement(OrderEvent event, Context ctx, Collector<Void> out) throws Exception {  
            // 注册一个定时器  
            ctx.timerService().registerProcessingTimeTimer(event.getTimestamp() + 30000); // 30秒超时  
        }  
  
        @Override  
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<Void> out) throws Exception {  
            // 定时器触发,执行订单取消逻辑  
            System.out.println("订单超时取消,订单ID:" + ctx.getCurrentKey());  
        }  
    });  

      

优点:

  1. 实时性高,支持复杂事件处理逻辑。
  2. 适合动态调整超时时间,满足灵活的业务需求。

缺点:

  1. 引入了流计算框架,系统复杂度增加。
  2. 对运维要求较高。

总结

每种方案都有自己的适用场景,大家在选择的时候,记得结合业务需求、订单量、并发量来综合考虑。

如果你的项目规模较小,可以直接用延时队列或 Redis;而在大型高并发系统中,消息队列和事件流处理往往是首选。

当然,代码实现只是第一步,更重要的是在实际部署和运行中进行性能调优,保证系统的稳定性。

希望这篇文章能给大家一些启发,也欢迎讨论其他可能的实现思路!

最后欢迎加入苏三的星球,你将获得:商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、高频面试题、底层原理、Spring源码解读、工作经验分享、痛点问题等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

目前星球已经更新了4500+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1500+小伙伴加入学习。

此外,推荐一下我的技术专栏《程序员最常见的100个问题》,目前已经更新了80多篇干货文章,里面收录了很多踩坑经历,对你的职业生涯或许有些帮助,最近收到的好评挺多的。

这个专栏总结了我10年工作中,遇到过的100个非常有代表性的技术问题,非常有参考和学习价值。

Java、Spring、分布式、高并发、数据库、海量数据、线上问题什么都有。

每篇文章从发现问题、分析问题、解决问题和问题总结等多个维度,深入浅出,分享了很多技术细节,定位和排查问题思路,解决问题技巧,以及实际工作经验。

你能从中学到很多有用知识,帮你少走很多弯路。

扫描下方二维码即可订阅:

picture.image

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

picture.image

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论