在电商或服务类订单系统中,订单支付超时未完成支付的情况非常常见。为保证系统效率和用户体验,需要一个可靠的方案来自动处理这些超时订单。本文介绍在单体架构下处理订单超时自动取消的几种方案,并讨论它们的适用场景及具体实现方法。
基于 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 // 订单已退款
}
}
利用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));
}
}
使用Netty的
HashedWheelTimer
实现延迟任务处理。时间轮算法通过多个槽位管理延迟任务,减少处理延迟,并有效管理大量延迟任务。
-
持久化支持:
使用数据库或Redis对待处理任务进行持久化,保证系统重启后数据不丢失。
-
提高扩展性:
将不同时间段的任务分配到不同时间轮实例,提升处理能力。