订单超时自动取消,这7种方案真香!

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

前言

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

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

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

苏三的免费刷题网站:www.susan.net.cn 里面:面试八股文、BAT面试真题、工作内推、工作经验分享、技术专栏等等什么都有,欢迎收藏和转发。

1. 使用延时队列(DelayQueue)

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

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

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

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

示例代码:

java 复制代码
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);
        }
    }
}

优点:

  • 实现简单,逻辑清晰。

缺点:

  • 依赖内存,系统重启会丢失任务。
  • 随着订单量增加,内存占用会显著上升。

2. 基于数据库轮询

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

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

示例代码:

ini 复制代码
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();
    }
}

优点:

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

缺点:

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

优化建议:

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

3. 基于Redis队列

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

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

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

例子:

typescript 复制代码
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 宕机或数据丢失的问题。

4. Redis Key 过期回调

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

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

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

例子:

  1. 设置订单的过期时间:
arduino 复制代码
public void setOrderWithExpiration(String orderId, long expireSeconds) {
    jedis.setex("order:" + orderId, expireSeconds, "PENDING");
}
  1. 订阅 Redis 的过期事件:
typescript 复制代码
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。比如:

matlab 复制代码
notify-keyspace-events Ex

最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。

你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。

添加苏三的私人微信 :su_san_java,备注:掘金+所在城市,即可加入。

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

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

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

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

示例代码(以RabbitMQ为例):

typescript 复制代码
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. 需要处理队列堆积的问题。

6. 使用定时任务框架

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

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

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

示例代码:

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

优点:

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

缺点:

  1. 对实时性支持有限。
  2. 框架本身较复杂。

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

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

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

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

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

csharp 复制代码
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;而在大型高并发系统中,消息队列和事件流处理往往是首选。

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

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

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

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

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

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

相关推荐
鬼火儿5 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin5 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧6 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧6 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧7 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧7 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧7 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧7 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧7 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang7 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构