👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事中“练”
《互联网高频面试题》:面朝简历学习,春暖花开
《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题
《精进 Java 学习指南》:系统学习,互联网主流技术栈
《必读 Java 源码专栏》:知其然,知其所以然
👉这是一个或许对你有用的开源项目
国产 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<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
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)