Spring Boot 訂單超時自動取消的 3 種主流實現方案
關鍵詞:Spring Boot、訂單超時、延遲任務、RabbitMQ、Redis、定時任務
在電商、外賣、票務等業務中,“下單后若 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 {@Beanpublic CustomExchange delayExchange() {Map<String, Object> args = Map.of("x-delayed-type", "direct");return new CustomExchange("order.delay", "x-delayed-message", true, false, args);}@Beanpublic Queue cancelQueue() {return QueueBuilder.durable("order.cancel.queue").build();}@Beanpublic 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 minreturn msg;});}
}
3.3 消費并取消
@Component
@RabbitListener(queues = "order.cancel.queue")
public class CancelConsumer {private final OrderService orderService;@RabbitHandlerpublic void handle(Long orderId) {Order order = orderService.find(orderId);if (order != null && order.getStatus() == OrderStatus.PENDING) {orderService.cancel(orderId);}}
}
4. 優缺點
- ? 實時性好(秒級);支持分布式;消息持久化
- ? 需要 RabbitMQ;鏈路更長
5. 適用場景
中高并發,需秒級取消,已用 MQ 或愿意引入 MQ。
五、方案 3:Redis Keyspace 過期事件
1. 思路
以 order:{id}
作為 key,30 min TTL;Redis 鍵過期時推送事件;應用監聽后取消訂單。
2. Redis 配置
# redis.conf
notify-keyspace-events Ex
或 CLI:
CONFIG SET notify-keyspace-events Ex
3. 代碼實現
3.1 訂單創建時寫 Redis
@Service
public class OrderService {private final StringRedisTemplate redisTemplate;public void createOrder(Order order) {orderMapper.insert(order);// value 隨意,這里用 idredisTemplate.opsForValue().set("order:" + order.getId(),String.valueOf(order.getId()),Duration.ofMinutes(30));}
}
3.2 監聽過期事件
@Configuration
public class RedisListenerConfig {@Beanpublic 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
。 - 可靠性:Redis 重啟會丟失未過期 key,需 兜底定時任務(方案 1)雙保險。
5. 優缺點
- ? 實時性高,組件少
- ? Redis 重啟可能丟事件;需處理冪等
6. 適用場景
已用 Redis,訂單量中等,能接受極低概率漏單。
六、3 種方案對比與選型
維度 | 定時任務 | RabbitMQ 延遲隊列 | Redis 過期事件 |
---|---|---|---|
實時性 | 分鐘級 | 秒級 | 秒級 |
吞吐量 | 低 | 高 | 中 |
額外組件 | 無 | RabbitMQ | Redis |
可靠性 | 高 | 高 | 中(需兜底) |
實現復雜度 | ★☆☆ | ★★☆ | ★★☆ |
推薦場景 | 小流量、兜底 | 高并發、已用 MQ | 已用 Redis、中等并發 |
建議:
- 小項目 → 定時任務即可;
- 大流量 → 延遲隊列;
- 已用 Redis → 過期事件 + 定時任務兜底雙保險。
七、灰度 & 監控
- 灰度發布:按用戶尾號或城市分批切換方案。
- 監控指標:
- 取消成功率
- MQ 消息積壓
- Redis 過期 QPS
- 定時任務掃描耗時
八、小結
一句話總結 |
---|
定時任務 簡單但慢;延遲隊列 實時但重;Redis 過期 輕量但需兜底。 |
在實際落地中,可以 并行運行 兩種方案(如延遲隊列 + 兜底定時任務),通過配置開關靈活切換,確保業務永遠在線。祝你的訂單永不超賣!