基于Java的订单超时自动取消方案 - 单体架构最佳实践

1,728 阅读5分钟

背景

在电商或服务类订单系统中,订单支付超时未完成支付的情况非常常见。为保证系统效率和用户体验,需要一个可靠的方案来自动处理这些超时订单。本文介绍在单体架构下处理订单超时自动取消的几种方案,并讨论它们的适用场景及具体实现方法。

实现方案

在单体架构中,所有功能模块运行在一个独立节点上,处理订单超时的方案相对简单,适用于中小型系统。以下将介绍三种常见的实现方法:数据库轮询(定时任务)、JDK延迟队列和时间轮算法。

方案一:数据库轮询(定时任务)

实现思路:

通过定时任务(如使用Quartz或Spring自带的定时调度功能),定期查询数据库中未支付的订单数据,检查订单的创建时间是否超时。若已超时,则更新订单状态为“已取消”。

优点:

  • 简单易实现,代码逻辑清晰,维护成本低。
  • 适用于订单量不大的小型系统,且对实时性要求不高。

缺点:

  • 占用服务器资源,定时任务周期的设置需要权衡性能和及时性。
  • 不适合大数据量场景,可能对数据库造成压力,影响系统性能。

优化建议:

  1. 批量处理:分页查询和处理未支付订单,减少单次查询的数据量,减轻数据库压力。
  2. 异步执行:使用异步任务执行定时轮询,避免阻塞主线程,提升系统响应速度。
  3. 索引优化:确保在订单表的支付状态和创建时间字段上建立适当的索引,减少查询延迟。

关键代码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
​
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
​
@Service
public class OrderService {
​
    @Autowired
    private OrderInfoMapper orderInfoMapper;
​
    // 定义订单超时时间,例如:30分钟
    public static final Duration ORDER_TIMEOUT = Duration.ofMinutes(30);
​
    /**
     * 定时任务:每分钟检查并取消超时未支付的订单。
     */
    @Scheduled(fixedRate = 60000)  // 每分钟执行一次
    public void cancelUnpaidOrders() {
        int page = 0;
        int size = 100;  // 每页处理100条订单,具体大小可根据实际情况调整
        List<OrderInfo> unpaidOrdersPage;
​
        do {
            unpaidOrdersPage = getUnpaidOrders(page, size);
            unpaidOrdersPage.forEach(order -> {
                if (isOrderTimedOut(order)) {
                    order.setOrderStatus(OrderStatus.CANCELED.name());
                    orderInfoMapper.updateOrderInfo(order);
                }
            });
            page++;
        } while (unpaidOrdersPage.size() == size);
    }
​
    /**
     * 分页获取超时未支付的订单。
     *
     * @param page 页码
     * @param size 每页大小
     * @return 分页后的未支付订单
     */
    private List<OrderInfo> getUnpaidOrders(int page, int size) {
        LocalDateTime timeoutThreshold = LocalDateTime.now().minus(ORDER_TIMEOUT);
        int offset = page * size;
        return orderInfoMapper.findUnpaidOrders(OrderStatus.UNPAID.name(), timeoutThreshold, offset, size);
    }
​
    /**
     * 判断订单是否超时。
     *
     * @param order 订单对象
     * @return true 如果订单已超时,否则 false
     */
    private boolean isOrderTimedOut(OrderInfo order) {
        return LocalDateTime.now().isAfter(order.getCreationTime().plus(ORDER_TIMEOUT));
    }
​
    public enum OrderStatus {
        UNPAID,    // 订单已创建,但尚未支付
        PAID,      // 订单已支付
        SHIPPED,   // 订单已发货
        COMPLETED, // 订单已完成
        CANCELED,  // 订单已取消
        REFUNDED   // 订单已退款
    }
}

方案二:JDK延迟队列(DelayQueue)

实现思路:

利用Java的DelayQueue阻塞队列实现订单超时处理,将订单放入延迟队列中,并设置相应的延迟时间。在订单超时时间到达后,通过启动异步线程从队列中取出订单并处理(如取消订单)。

优点:

  • 任务触发延迟较低,适用于单节点应用。
  • 实现较为简单,性能较高。

缺点:

  • 数据在服务器重启后可能会丢失,存在内存溢出的风险。
  • 不支持集群环境,不适合大规模分布式系统。

优化建议:

  1. 持久化处理:将DelayQueue中的数据持久化到数据库或磁盘,防止服务器重启导致数据丢失。
  2. 内存管理:监控队列的内存使用情况,必要时清理过期任务或采用分片存储,避免内存溢出。

关键代码:

定义延时任务

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
​
public class OrderDelayTask implements Delayed {
    private final OrderInfo order;
    private final long startTime;
​
    public OrderDelayTask(OrderInfo order, long delayTime) {
        this.order = order;
        this.startTime = System.currentTimeMillis() + delayTime;
    }
​
    public OrderInfo getOrder() {
        return order;
    }
​
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(startTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
​
    @Override
    public int compareTo(Delayed other) {
        return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), other.getDelay(TimeUnit.MILLISECONDS));
    }
}

延时任务管理

import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
​
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Executors;
​
@Component
public class OrderDelayManager implements CommandLineRunner {
​
    @Resource
    private IOrderInfoService orderInfoService;
​
    private DelayQueue<OrderDelayTask> delayQueue = new DelayQueue<>();
​
    //30分钟
    public static final long ORDER_TIMEOUT = 30 * 60 * 1000;
​
    public void addQueue(OrderInfo order) {
        delayQueue.put(new OrderDelayTask(order, ORDER_TIMEOUT));
    }
​
    // 任务消费线程
    public void processDelayedOrders() {
        while (true) {
            try {
                OrderDelayTask task = delayQueue.take();
                this.processOrder(task);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
​
    public void processOrder(OrderDelayTask task) {
        System.out.println("开始处理超时任务:" + task.getOrder().getOrderNum());
        OrderInfo order = task.getOrder();
        if (order.getOrderStatus().equals(OrderService.OrderStatus.UNPAID.name())) {
            order.setOrderStatus(OrderService.OrderStatus.CANCELED.name());
            orderInfoService.updateOrderInfo(order);
        }
    }
​
    @Override
    public void run(String... args) throws Exception {
        //初始化延时任务消费线程
        Executors.newSingleThreadExecutor().execute(new Thread(this::processDelayedOrders));
    }
}

方案三:时间轮算法(HashedWheelTimer)

实现思路:

使用NettyHashedWheelTimer实现延迟任务处理。时间轮算法通过多个槽位管理延迟任务,减少处理延迟,并有效管理大量延迟任务。

优点:

  • 延迟任务的触发更加精确且延迟更低。
  • 实现复杂度相对适中。

缺点:

  • 存在数据丢失和内存溢出的风险,不支持集群环境。

优化建议:

  1. 持久化支持:使用数据库或Redis对待处理任务进行持久化,保证系统重启后数据不丢失。
  2. 提高扩展性:将不同时间段的任务分配到不同时间轮实例,提升处理能力。

初始化HashedWheelTimer配置:

import io.netty.util.HashedWheelTimer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
import java.util.concurrent.TimeUnit;
​
@Configuration
public class TimerConfig {
​
    @Bean(destroyMethod = "stop")
    public HashedWheelTimer hashedWheelTimer() {
        // 创建HashedWheelTimer,设置tick时长为100ms,时间轮有512个槽,最长延时为512 * 100ms
        return new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 512);
    }
}

实现延迟任务:

import io.netty.util.HashedWheelTimer;
import io.netty.util.TimerTask;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
​
import java.util.concurrent.TimeUnit;
​
@Service
public class OrderTimeoutService {
​
    private static final long ORDER_TIMEOUT = 30; // 30分钟
​
    @Autowired
    private HashedWheelTimer timer;
​
    @Autowired
    private OrderInfoMapper orderInfoMapper;
​
    // 添加订单超时任务
    public void addDelayTask(OrderInfo order) {
        TimerTask task = timeout -> {
            try {
                OrderInfo currentOrder = orderInfoMapper.selectOrderInfoById(order.getId());
                if (currentOrder != null && currentOrder.getOrderStatus().equals(OrderService.OrderStatus.UNPAID.name())) {
                    currentOrder.setOrderStatus(OrderService.OrderStatus.CANCELED.name());
                    orderInfoMapper.updateOrderInfo(currentOrder);
                    System.out.println("Order " + order.getOrderNum() + " has been canceled due to timeout.");
                }
            } catch (Exception e) {
                // 异常处理与日志记录
                e.printStackTrace();
            }
        };
        timer.newTimeout(task, ORDER_TIMEOUT, TimeUnit.MINUTES);
    }
}

总结

针对不同规模和要求的系统,可以选择以下方案:

  • 订单量小或中等,系统可接受一定延迟: 选择数据库轮询结合异步处理的方案,简单且易于维护。
  • 订单量大,且要求高可用、高性能: 优先考虑分布式调度或消息队列方案,结合Redis缓存进一步提升系统性能和可靠性。
  • 订单实时性要求极高: 可以将时间轮算法与Redis结合,实现低延迟且高可用的方案。

点击查看可源码