⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7414856908223627279 「用户8491371754716」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

背景

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

实现方案

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

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

实现思路:

通过定时任务(如使用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
文章目录
  1. 1. 背景
  2. 2. 实现方案
  3. 3. 方案一:数据库轮询(定时任务)
    1. 3.0.1. 实现思路:
    2. 3.0.2. 优点:
    3. 3.0.3. 缺点:
    4. 3.0.4. 优化建议:
    5. 3.0.5. 关键代码:
  • 4. 方案二:JDK延迟队列(DelayQueue)
    1. 4.0.1. 实现思路:
    2. 4.0.2. 优点:
    3. 4.0.3. 缺点:
    4. 4.0.4. 优化建议:
    5. 4.0.5. 关键代码:
  • 5. 方案三:时间轮算法(HashedWheelTimer)
    1. 5.0.1. 实现思路:
    2. 5.0.2. 优点:
    3. 5.0.3. 缺点:
    4. 5.0.4. 优化建议:
    5. 5.0.5. 初始化HashedWheelTimer配置:
    6. 5.0.6. 实现延迟任务:
  • 6. 总结
  • 7. 代码获取