SpringBoot订单超时自动取消的三种主流实现方案
目录
- 引言
- 一、需求拆解
- 二、方案总览
- 三、方案 1:定时任务(@Scheduled)
- 1. 思路
- 2. 代码实现
- 3. 优化技巧
- 4. 优缺点
- 5. 适用场景
- 四、方案 2:RabbitMQ 延迟队列
- 1. 思路
- 2. 架构图
- 3. 代码实现
- 3.1 声明交换机 & 队列
- 3.2 发送延迟消息
- 3.3 消费并取消
- 4. 优缺点
- 5. 适用场景
- 五、方案 3:Redis Keyspace 过期事件
- 1. 思路
- 2. Redis 配置
- 3. 代码实现
- 3.1 订单创建时写 Redis
- 3.2 监听过期事件
- 4. 幂等 & 可靠性
- 5. 优缺点
- 6. 适用场景
- 六、3 种方案对比与选型
- 七、灰度 & 监控
- 八、小结
引言
在电商、外卖、票务等业务中,“下单后若 30 分钟未支付则自动取消”是一道经典需求。实现方式既要保证 实时性,又要在 高并发 下保持 低成本、高可靠。
本文基于 Spring Boot,给出 3 种生产级落地方案,并附完整代码与选型对比,方便快速决策。
一、需求拆解
功能点 | 约束 |
---|---|
触发条件 | 创建时间 + 30 min 仍未支付 |
实时性 | 秒级(理想) / 分钟级(可接受) |
幂等 | 重复取消需幂等 |
高并发 | 峰值 10 w+/日 |
数据一致性 | 不能漏单、不能错单 |
二、方案总览
方案 | 核心机制 | 实时性 | 额外组件 | 代码复杂度 |
---|---|---|---|---|
① 定时任务 | @Scheduled + DB 扫描 | 分钟级 | 无 | ★☆☆ |
② 延迟队列 | RabbitMQ TTL + DLX | 秒级 | RabbitMQ | ★★☆ |
③ Redis 过期事件 | Key TTL + Keyspace Notify | 秒级 | Redis | ★★☆ |
三、方案 1:定时任务(@Scheduled)
1. 思路
周期性扫描订单表,把“创建时间 + 30 min < 当前时间”且状态为 PENDING
的订单置为 CANCELLED
。
2. 代码实现
@EnableScheduling @Component @RequiredArgsConstructor public class OrderCancelSchedule { private final OrderService orderService; /** 每 30s 跑一次,可根据数据量调整 */ @Scheduled(fixedDelay = 30_000) public void cancelUnpaidOrders() { LocalDateTime expirePoint = LocalDateTime.now().minusMinutes(30); List<Long> ids = orderService.findUnpaidBefore(expirePoint); if (!ids.isEmpty()) { int affected = orderService.BATchCancel(ids); log.info("自动取消订单 {} 条", affected); } } }
3. 优化技巧
- 分页 + 索引:
CREATE INDEX idx_order_status_created ON t_order(status, created_time);
- 分片扫描:按 ID 或时间分片,避免大表锁。
- 单机多线程:
@Async("cancelExecutor")
+ 线程池。
4. 优缺点
- ✅ 零依赖、实现快
- ❌ 数据量大时 DB 压力大;实时性受轮询间隔限制
5. 适用场景
日订单 < 1 w,或作为兜底方案。
四、方案 2:RabbitMQ 延迟队列
1. 思路
订单创建后发送一条 30 min TTL 的消息;到期自动路由到消费队列,消费者检查订单状态并取消。
2. 架构图
Producer ──> Delay Exchange (x-delayed-message) ──> 30min TTL ──> Cancel Queue ──> Consumer
3. 代码实现
3.1 声明交换机 & 队列
@Configuration public class RabbitDelayConfig { @Bean public CustomExchange delayExchange() { Map<String, Object> args = Map.of("x-delayed-type", "direct"); return new CustomExchange("order.delay", "x-delayed-mjLLTseCnbessage", true, false, args); } @Bean public Queue cancelQueue() { return QueueBuilder.durable("order.cancel.queue").build(); } @Bean public Binding binding() { return BindingBuilder.bind(cancelQueue()).to(delayExchange()).with("order.cancel").noargs(); } }
3.2 发送延迟消息
@Service @RequiredArgsConstructor public class OrderPublisher { private final RabbitTemplate rabbitTemplate; public void createOrder(Order order) { // 1. 落库 orderMapper.insert(order); // 2. 发送延迟消息 rabbitTemplate.convertAndSend( "order.delay", "order.cancel", order.getId(), msg -> { msg.getMessageProperties().setDelay(30 * 60 * 1000); // 30 min return msg; } ); } }
3.3 消费并取消
@Component @RabbitListener(queues = "order.cancel.queue") public class CancelConsumer { private final OrderService orderService; @RabbitHandler public void handle(Long orderId) { Order order = orderService.find(orderId); if (order != null && order.getStatus() == OrderStatus.PENDING) { orderService.cancel(orderId); } } }
4. 优缺点
- ✅ 实时性好(秒级);支持分布式;消息持久化
- ❌ 需要 RabbitMQ;链路更长
5. 适用场景
中高并发,需秒级取消,已用 MQ 或javascript愿意引入 MQ。
五、方案 3:Redis Keyspace 过期事件
1. 思路
以 order:{id}
作为 key,30 min TTL;Redis 键过期时推送事件js;应用监听后取消订单。
2. Redis 配置
# redis.conf notify-keyspace-events Ex
或 CLI:
CONFIG SET notify-keyspace-events Ex
3. 代码实现
3.1 订单创建时写 Redis
@Service public classwww.devze.com OrderService { private final StringRedisTemplate redisTemplate; public void createOrder(Order order) { orderMapper.insert(order); // value 随意,这里用 id redisTemplate.opsForValue() .set("order:" + order.getId(), String.valueOf(order.getId()), Duration.ofMinutes(30)); } }
3.2 监听过期事件
@Configuration public class RedisListenerConfig { @Bean public RedisMessageListenerContainer container(RedisConnectionFactory cf) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(cf); container.addMessageListener( (message, pattern) -> { String key = message.toString(); if (key.startsWith("order:")) { String orderId = key.substring(6); // 幂等取消 orderService.cancelIfUnpaid(Long.valueOf(orderId)); } }, new PatternTopic("__keyevent@*__:expired") ); return container; } }
4. 幂等 & 可靠性
- 幂等:取消 SQL 加状态条件
WHERE status = PENDING
。 - 可靠性php:Redis 重启会丢失未过期 key,需 兜底定时任务(方案 1)双保险。
5. 优缺点
- ✅ 实时性高,组件少
- ❌ Redis 重启可能丢事件;需处理幂等
6. 适用场景
已用 Redis,订单量中等,能接受极低概率漏单。
六、3 种方案对比与选型
维度 | 定时任务 | RabbitMQ 延迟队列 | Redis 过期事件 |
---|---|---|---|
实时性 | 分钟级 | 秒级 | 秒级 |
吞吐量 | 低 | 高 | 中 |
额外组件 | 无 | RabbitMQ | Redis |
可靠性 | 高 | 高 | 中(需兜底) |
实现复杂度 | ★☆☆ | ★★☆ | ★★☆ |
推荐场景 | 小流量、兜底 | 高并发、已用 MQ | 已用 Redis、中等并发 |
建议:
- 小项目 → 定时任务即可;
- 大流量 → 延迟队列;
- 已用 Redis → 过期事件 + 定时任务兜底双保险。
七、灰度 & 监控
- 灰度发布:按用户尾号或城市分批切换方案。
- 监控指标:
- 取消成功率
- MQ 消息积压
- Redis 过期 QPS
- 定时任务扫描耗时
八、小结
一句话总结:定时任务 简单但慢;延迟队列 实时但重;Redis 过期 轻量但需兜底。
在实际落地中,可以 并行运行 两种方案(如延迟队列 + 兜底定时任务),通过配置开关灵活切换,确保业务永远在线。祝你的订单永不超卖!
以上就是SpringBoot订单超时自动取消的三种主流实现方案的详细内容,更多关于SpringBoot订单超时自动取消的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论