专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
通信头条  ·  李强总理为什么到三家通信央企调研? ·  昨天  
巴比特资讯  ·  细致扒一下DeepSeek-R1论文到底讲了些什么 ·  2 天前  
巴比特资讯  ·  全球首个混合推理模型Claude ... ·  3 天前  
51好读  ›  专栏  ›  Java基基

电商订单超时处理:三种高效自动取消方案详解

Java基基  · 公众号  ·  · 2025-02-10 11:55

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、 商城 、支付、工作流、大屏报表、微信公众号、 ERP CRM AI 大模型 等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本

来源:juejin.cn/post/
7414856908223627279


背景

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

实现方案

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

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

实现思路:

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

优点:

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

缺点:

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

优化建议:

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

关键代码:

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 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 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阻塞队列实现订单超时处理,将订单放入延迟队列中,并设置相应的延迟时间。在订单超时时间到达后,通过启动异步线程从队列中取出订单并处理(如取消订单)。

优点:

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

缺点:

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

优化建议:

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

关键代码:

定义延时任务

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 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)

实现思路:

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

优点:

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

缺点:

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

优化建议:

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







请到「今天看啥」查看全文