前几天有个小伙伴跟我吐槽面试挂在了订单超时关闭这个问题上。 我一听,这不巧了嘛,正好我以前做过实际的需求!今天把我实现的几种实现思路分享给大家,不管你是面试用还是实际开发,都妥妥的!
业务场景
大家肯定都网购过,下单之后如果一直不付款,订单就会被自动取消。为啥要这么设计?很简单:
-
防止商品被占着卖不出去
-
释放系统资源,避免无效订单堆积
-
提升库存周转效率
那Java怎么实现这个"超时自动关单"呢?主要有四种常见方案。
1. 定时任务扫表
这是最直接的想法:搞个定时任务,每隔一段时间去数据库查一下"待支付"的订单,把超过时限的捞出来,一个个关掉。
具体实现:
@Component
public class OrderTimeoutTask {
@Autowired
private OrderService orderService;
// 每5分钟跑一次
@Scheduled(cron = "0 */5 * * * ?")
public void closeTimeoutOrders() {
// 查30分钟前创建的未支付订单
List<Order> orders = orderService.selectTimeoutOrders(30);
for (Order order : orders) {
// 执行关单逻辑
orderService.closeOrder(order.getId(), "超时未支付");
}
}
}
优点: 简单易懂,几分钟就能写出来 缺点: 有点傻!如果订单量大了,每次全表扫描特别耗数据库性能。而且延迟高,最坏情况可能要 5 分多钟才能关单。 适用场景: 小项目、低频业务,或者你只是想快速实现一版
2. JDK延迟队列
如果你不想老去烦数据库,可以试试用内存队列,比如JDK自带的DelayQueue。
思路是这样的:
- 订单创建时,塞个关单任务进延迟队列
- 任务设置了延迟时间(比如30分钟)
- 时间到了自动执行关单操作
代码大概长这样:
// 1. 定义延迟任务
public class CloseOrderTask implements Delayed {
private long expireTime; // 到期时间
private long orderId;
public CloseOrderTask(long orderId, long delayMinutes) {
this.orderId = orderId;
this.expireTime = System.currentTimeMillis() + delayMinutes * 60 * 1000;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
// 实现compareTo方法...
}
// 2. 放到队列里
public class OrderDelayQueue {
private static DelayQueue<CloseOrderTask> queue = new DelayQueue<>();
public static void addTask(long orderId, long delayMinutes) {
queue.offer(new CloseOrderTask(orderId, delayMinutes));
}
// 启动个线程一直消费
static {
new Thread(() -> {
while (true) {
try {
CloseOrderTask task = queue.take();
orderService.closeOrder(task.getOrderId(), "超时未支付");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
优点: 内存操作,速度快,精度高 缺点: 重启或宕机任务就全没了!得想办法持久化 适用场景: 单机应用、订单量不大,且对重启容忍度较高的场景
3. Redis 过期监听
Redis的key过期监听也是个不错的选择,特别适合已经用了Redis的项目。
实现步骤:
- 订单创建时,在
Redis存个key,设置过期时间 - 配置
Redis的键空间通知 - 写个监听器,捕获
key过期事件 - 事件触发后执行关单逻辑
代码示例:
// 1. 订单创建时设置key
public void createOrder(Order order) {
// ...其他业务逻辑
redisTemplate.opsForValue().set(
"order:timeout:" + order.getId(),
"1",
30, // 30分钟
TimeUnit.MINUTES);
}
// 2. 配置Redis监听器
@Configuration
public class RedisConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory factory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
return container;
}
}
// 3. 监听过期事件
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
if (expiredKey.startsWith("order:timeout:")) {
long orderId = Long.parseLong(expiredKey.substring(14));
orderService.closeOrder(orderId, "超时未支付");
}
}
}
优点: 性能好,可靠性高,支持分布式
缺点: Redis的过期通知可能有延迟(实测通常在1分钟内),且需要开启键空间通知(会消耗额外资源)
适用场景: 已经使用Redis的项目,对时效性要求不是特别苛刻的场景
4. RabbitMQ 死信队列(推荐方案)
这是我个人最推荐的一种方式,也是我们生产环境在用的方案。利用消息队列的 TTL + 死信机制,非常靠谱。
原理很简单:
- 订单创建后,发一条消息到
RabbitMQ - 这条消息设个TTL(比如30分钟)
- 时间一到,消息自动变成"死信",被路由到死信交换机
- 死信交换机再把消息扔给关单业务队列
- 消费者拿到消息,执行关单逻辑
代码示例:
// 发送延迟消息
public void sendDelayMessage(long orderId) {
rabbitTemplate.convertAndSend(
"order.delay.exchange",
"order.delay.key",
orderId,
message -> {
// 设置30分钟过期
message.getMessageProperties().setExpiration("1800000");
return message;
});
}
// 监听死信队列
@Component
public class OrderTimeoutListener {
@RabbitListener(queues = "order.close.queue")
public void handleCloseOrder(long orderId) {
orderService.closeOrder(orderId, "超时未支付");
}
}
优点: 解耦、可靠(消息持久化)、精度高
缺点: 得引入RabbitMQ,增加系统复杂度
适用场景: 绝大多数需要订单超时关闭的场景,特别是分布式系统
总结
-
要快手上手:用方案一,定时任务扫表,简单粗暴
-
要轻量单机:用方案二,延迟队列,记得处理重启问题
-
要
Redis生态:用方案三,性能好,适合已有Redis的项目 -
要稳定可靠:用方案四,
RabbitMQ死信队列,yyds!
实际开发中,最推荐方案三和方案四。希望这篇文章对你有帮助!下次面试再被问到,直接把这几个方案甩出去,拿 offer 的概率又高了几分!
你们项目里用的是哪种方案?有没有踩过坑?欢迎评论区留言!
我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《工作 5 年没碰过分布式锁,是我太菜还是公司太稳?网友:太真实了!》
《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》