Java 订单超时未支付,如何自动关闭?掌握这 3 种方案,轻松拿 offer!

192 阅读5分钟

前几天有个小伙伴跟我吐槽面试挂在了订单超时关闭这个问题上。 我一听,这不巧了嘛,正好我以前做过实际的需求!今天把我实现的几种实现思路分享给大家,不管你是面试用还是实际开发,都妥妥的!

业务场景

大家肯定都网购过,下单之后如果一直不付款,订单就会被自动取消。为啥要这么设计?很简单:

  • 防止商品被占着卖不出去

  • 释放系统资源,避免无效订单堆积

  • 提升库存周转效率

Java怎么实现这个"超时自动关单"呢?主要有四种常见方案。

1. 定时任务扫表

这是最直接的想法:搞个定时任务,每隔一段时间去数据库查一下"待支付"的订单,把超过时限的捞出来,一个个关掉。

具体实现:

@Component
public class OrderTimeoutTask {
    
    @Autowired
    private OrderService orderService;

    // 每5分钟跑一次
    @Scheduled(cron = "0 */5 * * * ?")
    public void closeTimeoutOrders() {
        // 查30分钟前创建的未支付订单
        List<Order> orders = orderService.selectTimeoutOrders(30);
        for (Order order : orders) {
            // 执行关单逻辑
            orderService.closeOrder(order.getId(), "超时未支付");
        }
    }
}

优点: 简单易懂,几分钟就能写出来 缺点: 有点傻!如果订单量大了,每次全表扫描特别耗数据库性能。而且延迟高,最坏情况可能要 5 分多钟才能关单。 适用场景: 小项目、低频业务,或者你只是想快速实现一版

2. JDK延迟队列

如果你不想老去烦数据库,可以试试用内存队列,比如JDK自带的DelayQueue。 思路是这样的:

  1. 订单创建时,塞个关单任务进延迟队列
  2. 任务设置了延迟时间(比如30分钟)
  3. 时间到了自动执行关单操作

代码大概长这样:

// 1. 定义延迟任务
public class CloseOrderTask implements Delayed {
    private long expireTime; // 到期时间
    private long orderId;

    public CloseOrderTask(long orderId, long delayMinutes) {
        this.orderId = orderId;
        this.expireTime = System.currentTimeMillis() + delayMinutes * 60 * 1000;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    // 实现compareTo方法...
}

// 2. 放到队列里
public class OrderDelayQueue {
    private static DelayQueue<CloseOrderTask> queue = new DelayQueue<>();

    public static void addTask(long orderId, long delayMinutes) {
        queue.offer(new CloseOrderTask(orderId, delayMinutes));
    }

    // 启动个线程一直消费
    static {
        new Thread(() -> {
            while (true) {
                try {
                    CloseOrderTask task = queue.take();
                    orderService.closeOrder(task.getOrderId(), "超时未支付");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

优点: 内存操作,速度快,精度高 缺点: 重启或宕机任务就全没了!得想办法持久化 适用场景: 单机应用、订单量不大,且对重启容忍度较高的场景

3. Redis 过期监听

Rediskey过期监听也是个不错的选择,特别适合已经用了Redis的项目。

实现步骤:

  1. 订单创建时,在Redis存个key,设置过期时间
  2. 配置Redis的键空间通知
  3. 写个监听器,捕获key过期事件
  4. 事件触发后执行关单逻辑

代码示例:

// 1. 订单创建时设置key
public void createOrder(Order order) {
    // ...其他业务逻辑
    redisTemplate.opsForValue().set(
        "order:timeout:" + order.getId(), 
        "1", 
        30, // 30分钟
        TimeUnit.MINUTES);
}

// 2. 配置Redis监听器
@Configuration
public class RedisConfig {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory factory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        return container;
    }
}

// 3. 监听过期事件
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    
    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        if (expiredKey.startsWith("order:timeout:")) {
            long orderId = Long.parseLong(expiredKey.substring(14));
            orderService.closeOrder(orderId, "超时未支付");
        }
    }
}

优点: 性能好,可靠性高,支持分布式 缺点: Redis的过期通知可能有延迟(实测通常在1分钟内),且需要开启键空间通知(会消耗额外资源) 适用场景: 已经使用Redis的项目,对时效性要求不是特别苛刻的场景

4. RabbitMQ 死信队列(推荐方案)

这是我个人最推荐的一种方式,也是我们生产环境在用的方案。利用消息队列的 TTL + 死信机制,非常靠谱。

原理很简单:

  1. 订单创建后,发一条消息到RabbitMQ
  2. 这条消息设个TTL(比如30分钟)
  3. 时间一到,消息自动变成"死信",被路由到死信交换机
  4. 死信交换机再把消息扔给关单业务队列
  5. 消费者拿到消息,执行关单逻辑

代码示例:

// 发送延迟消息
public void sendDelayMessage(long orderId) {
    rabbitTemplate.convertAndSend(
        "order.delay.exchange", 
        "order.delay.key", 
        orderId, 
        message -> {
            // 设置30分钟过期
            message.getMessageProperties().setExpiration("1800000");
            return message;
        });
}

// 监听死信队列
@Component
public class OrderTimeoutListener {
    
    @RabbitListener(queues = "order.close.queue")
    public void handleCloseOrder(long orderId) {
        orderService.closeOrder(orderId, "超时未支付");
    }
}

优点: 解耦、可靠(消息持久化)、精度高 缺点: 得引入RabbitMQ,增加系统复杂度 适用场景: 绝大多数需要订单超时关闭的场景,特别是分布式系统

总结

  • 要快手上手:用方案一,定时任务扫表,简单粗暴

  • 要轻量单机:用方案二,延迟队列,记得处理重启问题

  • Redis生态:用方案三,性能好,适合已有Redis的项目

  • 要稳定可靠:用方案四,RabbitMQ死信队列,yyds!

实际开发中,最推荐方案三和方案四。希望这篇文章对你有帮助!下次面试再被问到,直接把这几个方案甩出去,拿 offer 的概率又高了几分!

你们项目里用的是哪种方案?有没有踩过坑?欢迎评论区留言!

我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《工作 5 年没碰过分布式锁,是我太菜还是公司太稳?网友:太真实了!》

《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》

《只会写 Mapper 就敢说会 MyBatis?面试官:原理都没懂》

《别再手写判空了!SpringBoot 自带的 20 个高效工具类》