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

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

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

d4cc40b53ec40817b1efa8d35894c148.gif

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

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

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

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

优点:
  • 任务触发延迟较低,适用于单节点应用。

  • 实现较为简单,性能较高。

缺点:
  • 数据在服务器重启后可能会丢失,存在内存溢出的风险。

  • 不支持集群环境,不适合大规模分布式系统。

优化建议:
  • 持久化处理: 将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<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)

实现思路:

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

优点:
  • 延迟任务的触发更加精确且延迟更低。

  • 实现复杂度相对适中。

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

优化建议:
  • 持久化支持: 使用数据库或Redis对待处理任务进行持久化,保证系统重启后数据不丢失。

  • 提高扩展性: 将不同时间段的任务分配到不同时间轮实例,提升处理能力。

初始化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结合,实现低延迟且高可用的方案。

代码获取

https://gitee.com/xunyi1026/learn-demo.git

欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

7957c46286bb31b57568c1e020d23d46.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

b7f38b0037140e519e8815dbdc88555e.png

72298171c54c8fdd8a2bcdd28a6d6b09.pngbd0d9482eaab3d58c2b4e5e54e3567ad.png07a5a6a80f45dc872974170b1b7613ff.pngeb770c55609b8dd0e28aca57f8f8487d.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值