最近一段时间参与的一个电商平台项目中,关于支付模块设计到一个功能:对于用户下单但长时间未支付的订单,系统应自动将其关闭。这个看似简单的需求,背后其实有不少实现方案可选,比如利用数据库的定时任务、消息队列的延迟消息、Redis 的过期键等。每种方案都有各自的优缺点,比如延迟精度、系统开销、可扩展性等。面对这些选择,我们该如何权衡利弊,选择最适合当前业务场景的方案呢?
微信支付系列相关的文章:
在上篇中,我们介绍了定时任务扫描的“巡逻小保安”模型,虽简单易用,但并不精确。这一篇,我们要升级工具箱,引入 JDK 原生延迟队列 DelayQueue,让每个订单拥有一块专属倒计时牌,时间一到,就自动触发关闭。
这是实现订单定时关闭的一个相对轻量、无外部依赖、更精确的方案。用 Java 内建的 DelayQueue 实现一个内存级定时器,每个订单排进队列,到时间就执行关单。
🔧 什么是 DelayQueue?
DelayQueue 是 JDK 提供的一个无界阻塞队列,里面的元素必须实现 Delayed 接口。放入队列的元素只有在延迟时间到了之后才能被消费。
它的底层是最小堆 + ReentrantLock,实现线程安全的延迟出队行为,适合做本地延迟任务调度。
🎯 实现思路
- 用户下单后创建一个包含订单 ID 与过期时间的延迟任务对象
- 将该对象放入
DelayQueue - 后台线程轮询从队列中获取过期任务
- 获取到的订单进行关闭操作(修改状态、回滚库存、发送通知等)
sequenceDiagram
participant 用户
participant 订单服务
participant DelayQueue
participant 后台线程
participant 数据库
participant 库存服务
participant 通知服务
Note over 用户,通知服务: 1. 下单流程
用户->>订单服务: 提交订单
订单服务->>数据库: 创建订单(状态=待支付)
订单服务->>库存服务: 预占库存
订单服务->>DelayQueue: 创建延迟任务(订单ID+过期时间)
DelayQueue-->>订单服务: 确认加入队列
Note over 用户,通知服务: 2. 支付期处理(两种情况)
alt 用户支付成功
用户->>订单服务: 完成支付
订单服务->>数据库: 更新订单状态=已支付
订单服务->>DelayQueue: 尝试移除对应订单任务(可选)
else 超时未支付(进入关单流程)
loop 后台线程轮询
后台线程->>DelayQueue: take()阻塞获取到期订单
DelayQueue-->>后台线程: 返回过期订单对象
后台线程->>数据库: 检查订单状态(二次确认)
alt 状态仍为待支付
后台线程->>数据库: 更新状态=已取消
后台线程->>库存服务: 释放预占库存
后台线程->>通知服务: 发送关单通知
else 状态已更新(如已支付)
后台线程->>DelayQueue: 丢弃该任务
end
end
end
代码实现
1. 延迟任务定义
/**
* 订单实体类,实现Delayed接口
*/
@Data
class OrderDelayTask implements Delayed {
private final String orderId; // 订单ID
private final long expireTime; // 过期时间戳(毫秒)
private final LocalDateTime createTime; // 订单创建时间
public OrderDelayTask(String orderId, long delay, TimeUnit unit) {
this.orderId = orderId;
this.expireTime = System.currentTimeMillis() + unit.toMillis(delay);
this.createTime = LocalDateTime.now();
}
@Override
public long getDelay(TimeUnit unit) {
// 计算剩余延迟时间
long diff = expireTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
// 比较两个订单的过期时间,用于队列排序
return Long.compare(this.expireTime, ((Order) o).expireTime);
}
}
2. 延迟队列消费者线程
@Slf4j
@Component
public class DelayQueueWorker implements InitializingBean {
private static final DelayQueue<OrderDelayTask> queue = new DelayQueue<>();
@Autowired
private OrderService orderService;
public static void addTask(OrderDelayTask task) {
queue.offer(task);
}
@Override
public void afterPropertiesSet() {
Thread worker = new Thread(() -> {
while (true) {
try {
OrderDelayTask task = queue.take(); // 阻塞直到有任务到期
log.info("订单 {} 到期,准备关闭", task.getOrderId());
orderService.closeOrder(task.getOrderId());
} catch (Exception e) {
log.error("处理延迟订单任务出错", e);
}
}
});
worker.setDaemon(true);
worker.start();
}
}
3. 下单时添加任务
// 订单创建成功后,设置过期时间
Order newOrder = orderService.createOrder(...);
long expireTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15);
DelayQueueWorker.addTask(new OrderDelayTask(newOrder.getId(), expireTime));
这种方案,JDK原生支持,无需引入额外依赖,基于JVM内部时间,精度可达毫秒级,对于小规模数据,处理效率高。
但是因为 layQueue是一个无界队列,如果放入的订单过多,会造成 VM OOM。 DelayQueue基于 JVM 内存,如果 JVM 重启了,那所有数据就丢失了。而且队列内部状态不可见,不支持取消或变更任务执行时间
如果是关于服务崩溃导致丢失数据的问题,需要在JVM重启时从持久化存储恢复未处理的订单,保证没有漏单。
DelayQueue 是一个本地化、轻量级的延迟任务方案,在中小型项目、功能初期、单机部署下非常实用。它解决了定时任务扫描带来的资源浪费与时间误差,是一人一张倒计时牌的真实演绎。