订单到期关闭实现方案
1. 方案选择与对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
延迟队列 | 实时性高,精准控制关闭时间 | 依赖消息队列可靠性,需处理消息堆积 | 高并发、实时性要求高 |
定时任务 | 实现简单,无额外中间件依赖 | 实时性低,频繁扫描增加数据库压力 | 低频场景,订单量较小 |
Redis过期键 | 基于内存,性能高 | 数据持久化风险,需结合数据库兜底 | 中小规模,需快速响应 |
2. 基于延迟队列的实现(推荐)
2.1 技术选型
- 消息队列:RabbitMQ(延迟插件) / RocketMQ(定时消息) / Kafka(时间轮+外部存储)。
- 核心流程:
- 订单创建时发送延迟消息:设置消息延迟时间为订单超时期限(如30分钟)。
- 消费者处理到期消息:检查订单状态,若未支付则关闭订单并释放库存。
2.2 RabbitMQ实现示例
- 启用延迟插件:bash
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 声明延迟交换机:java
@Bean public CustomExchange orderDelayExchange() { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); return new CustomExchange("order.delay.exchange", "x-delayed-message", true, false, args); }
- 订单服务发送延迟消息:java
public void sendDelayMessage(String orderId, long delayMs) { rabbitTemplate.convertAndSend( "order.delay.exchange", "order.delay.routingkey", orderId, message -> { message.getMessageProperties().setDelay((int) delayMs); return message; } ); }
- 消费者处理消息:java
@RabbitListener(queues = "order.close.queue") public void handleOrderClose(String orderId) { Order order = orderService.getById(orderId); if (order.getStatus() == OrderStatus.UNPAID) { orderService.closeOrder(orderId); // 关闭订单并释放库存 } }
2.3 可靠性保障
- 消息持久化:交换机、队列、消息均设置持久化(
durable=true
)。 - 消费幂等性:通过订单状态校验避免重复关闭。
- 死信队列:处理失败的消息转入死信队列,人工介入或自动重试。
3. 基于定时任务的实现
3.1 Spring Task调度
java
@Scheduled(fixedDelay = 30000) // 每30秒执行一次
public void scanExpiredOrders() {
Page<Order> page = orderMapper.selectExpiredOrders(OrderStatus.UNPAID, LocalDateTime.now().minusMinutes(30));
for (Order order : page.getRecords()) {
closeOrder(order.getId());
}
}
SQL示例:
sql
SELECT * FROM order
WHERE status = 'UNPAID'
AND create_time <= NOW() - INTERVAL 30 MINUTE
LIMIT 100;
3.2 优化策略
- 分页查询:避免一次性加载大量数据,使用
LIMIT
分页。 - 分布式锁:通过Redis锁防止多实例重复执行。java
String lockKey = "scan_expired_orders_lock"; Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS); if (locked) { try { scanExpiredOrders(); } finally { redisTemplate.delete(lockKey); } }
4. 基于Redis过期键的实现
4.1 键设计与监听
- 订单创建时设置过期键:java
String key = "order:close:" + orderId; redisTemplate.opsForValue().set(key, orderId, 30, TimeUnit.MINUTES);
- 订阅过期事件:java
@Bean public RedisMessageListenerContainer container(RedisConnectionFactory factory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener((message, pattern) -> { String expiredKey = new String(message.getBody()); String orderId = expiredKey.split(":")[2]; closeOrder(orderId); }, new PatternTopic("__keyevent@0__:expired")); return container; }
注意:需配置Redis的notify-keyspace-events Ex
参数启用键过期事件。
4.2 数据兜底
- 数据库校验:Redis触发关闭后,需再次查询数据库确认订单状态,避免误关。
5. 核心业务逻辑(关闭订单)
java
@Transactional
public void closeOrder(String orderId) {
// 1. 校验订单状态
Order order = orderMapper.selectByIdForUpdate(orderId); // 悲观锁
if (order.getStatus() != OrderStatus.UNPAID) {
return;
}
// 2. 更新订单状态为关闭
order.setStatus(OrderStatus.CLOSED);
orderMapper.updateById(order);
// 3. 释放库存(异步消息)
stockService.releaseStock(order.getSkuId(), order.getQuantity());
// 4. 发送通知(MQ或短信)
notifyService.sendOrderCloseAlert(order.getUserId());
}
6. 性能与容灾
- 批量处理:每次关闭100-500笔订单,减少数据库事务开销。
- 熔断降级:Hystrix保护关闭订单接口,异常时降级为记录日志后重试。
- 监控报警:Prometheus监控关闭订单的TPS、失败率,异常时触发告警。
7. 架构图
订单创建 → 发送延迟消息 → 消息队列(RabbitMQ/RocketMQ)
↓
消费者 → 关闭订单 → 释放库存 + 通知用户
定时任务 → 分页扫描超时订单 → 数据库(MySQL)
↓
关闭订单服务
总结
- 高并发场景:优先选择延迟队列(如RocketMQ定时消息),保障实时性。
- 中小规模:使用Redis过期键+定时任务兜底,平衡开发成本和性能。
- 关键要点:幂等处理、分布式锁、异步释放库存、监控告警。