SpringBoot訂單超時(shí)自動(dòng)取消的三種主流實(shí)現(xiàn)方案
引言
在電商、外賣、票務(wù)等業(yè)務(wù)中,“下單后若 30 分鐘未支付則自動(dòng)取消”是一道經(jīng)典需求。實(shí)現(xiàn)方式既要保證 實(shí)時(shí)性,又要在 高并發(fā) 下保持 低成本、高可靠。
本文基于 Spring Boot,給出 3 種生產(chǎn)級(jí)落地方案,并附完整代碼與選型對(duì)比,方便快速?zèng)Q策。
一、需求拆解
| 功能點(diǎn) | 約束 |
|---|---|
| 觸發(fā)條件 | 創(chuàng)建時(shí)間 + 30 min 仍未支付 |
| 實(shí)時(shí)性 | 秒級(jí)(理想) / 分鐘級(jí)(可接受) |
| 冪等 | 重復(fù)取消需冪等 |
| 高并發(fā) | 峰值 10 w+/日 |
| 數(shù)據(jù)一致性 | 不能漏單、不能錯(cuò)單 |
二、方案總覽
| 方案 | 核心機(jī)制 | 實(shí)時(shí)性 | 額外組件 | 代碼復(fù)雜度 |
|---|---|---|---|---|
| ① 定時(shí)任務(wù) | @Scheduled + DB 掃描 | 分鐘級(jí) | 無(wú) | ★☆☆ |
| ② 延遲隊(duì)列 | RabbitMQ TTL + DLX | 秒級(jí) | RabbitMQ | ★★☆ |
| ③ Redis 過(guò)期事件 | Key TTL + Keyspace Notify | 秒級(jí) | Redis | ★★☆ |
三、方案 1:定時(shí)任務(wù)(@Scheduled)
1. 思路
周期性掃描訂單表,把“創(chuàng)建時(shí)間 + 30 min < 當(dāng)前時(shí)間”且狀態(tài)為 PENDING 的訂單置為 CANCELLED。
2. 代碼實(shí)現(xiàn)
@EnableScheduling
@Component
@RequiredArgsConstructor
public class OrderCancelSchedule {
private final OrderService orderService;
/** 每 30s 跑一次,可根據(jù)數(shù)據(jù)量調(diào)整 */
@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("自動(dòng)取消訂單 {} 條", affected);
}
}
}
3. 優(yōu)化技巧
- 分頁(yè) + 索引:
CREATE INDEX idx_order_status_created ON t_order(status, created_time);
- 分片掃描:按 ID 或時(shí)間分片,避免大表鎖。
- 單機(jī)多線程:
@Async("cancelExecutor")+ 線程池。
4. 優(yōu)缺點(diǎn)
- ? 零依賴、實(shí)現(xiàn)快
- ? 數(shù)據(jù)量大時(shí) DB 壓力大;實(shí)時(shí)性受輪詢間隔限制
5. 適用場(chǎng)景
日訂單 < 1 w,或作為兜底方案。
四、方案 2:RabbitMQ 延遲隊(duì)列
1. 思路
訂單創(chuàng)建后發(fā)送一條 30 min TTL 的消息;到期自動(dòng)路由到消費(fèi)隊(duì)列,消費(fèi)者檢查訂單狀態(tài)并取消。
2. 架構(gòu)圖
Producer ──> Delay Exchange (x-delayed-message) ──> 30min TTL ──> Cancel Queue ──> Consumer
3. 代碼實(shí)現(xiàn)
3.1 聲明交換機(jī) & 隊(duì)列
@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-message", 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 發(fā)送延遲消息
@Service
@RequiredArgsConstructor
public class OrderPublisher {
private final RabbitTemplate rabbitTemplate;
public void createOrder(Order order) {
// 1. 落庫(kù)
orderMapper.insert(order);
// 2. 發(fā)送延遲消息
rabbitTemplate.convertAndSend(
"order.delay",
"order.cancel",
order.getId(),
msg -> {
msg.getMessageProperties().setDelay(30 * 60 * 1000); // 30 min
return msg;
}
);
}
}
3.3 消費(fèi)并取消
@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. 優(yōu)缺點(diǎn)
- ? 實(shí)時(shí)性好(秒級(jí));支持分布式;消息持久化
- ? 需要 RabbitMQ;鏈路更長(zhǎng)
5. 適用場(chǎng)景
中高并發(fā),需秒級(jí)取消,已用 MQ 或愿意引入 MQ。
五、方案 3:Redis Keyspace 過(guò)期事件
1. 思路
以 order:{id} 作為 key,30 min TTL;Redis 鍵過(guò)期時(shí)推送事件;應(yīng)用監(jiān)聽(tīng)后取消訂單。
2. Redis 配置
# redis.conf notify-keyspace-events Ex
或 CLI:
CONFIG SET notify-keyspace-events Ex
3. 代碼實(shí)現(xiàn)
3.1 訂單創(chuàng)建時(shí)寫(xiě) Redis
@Service
public class 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 監(jiān)聽(tīng)過(guò)期事件
@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 加狀態(tài)條件
WHERE status = PENDING。 - 可靠性:Redis 重啟會(huì)丟失未過(guò)期 key,需 兜底定時(shí)任務(wù)(方案 1)雙保險(xiǎn)。
5. 優(yōu)缺點(diǎn)
- ? 實(shí)時(shí)性高,組件少
- ? Redis 重啟可能丟事件;需處理冪等
6. 適用場(chǎng)景
已用 Redis,訂單量中等,能接受極低概率漏單。
六、3 種方案對(duì)比與選型
| 維度 | 定時(shí)任務(wù) | RabbitMQ 延遲隊(duì)列 | Redis 過(guò)期事件 |
|---|---|---|---|
| 實(shí)時(shí)性 | 分鐘級(jí) | 秒級(jí) | 秒級(jí) |
| 吞吐量 | 低 | 高 | 中 |
| 額外組件 | 無(wú) | RabbitMQ | Redis |
| 可靠性 | 高 | 高 | 中(需兜底) |
| 實(shí)現(xiàn)復(fù)雜度 | ★☆☆ | ★★☆ | ★★☆ |
| 推薦場(chǎng)景 | 小流量、兜底 | 高并發(fā)、已用 MQ | 已用 Redis、中等并發(fā) |
建議:
- 小項(xiàng)目 → 定時(shí)任務(wù)即可;
- 大流量 → 延遲隊(duì)列;
- 已用 Redis → 過(guò)期事件 + 定時(shí)任務(wù)兜底雙保險(xiǎn)。
七、灰度 & 監(jiān)控
- 灰度發(fā)布:按用戶尾號(hào)或城市分批切換方案。
- 監(jiān)控指標(biāo):
- 取消成功率
- MQ 消息積壓
- Redis 過(guò)期 QPS
- 定時(shí)任務(wù)掃描耗時(shí)
八、小結(jié)
一句話總結(jié):定時(shí)任務(wù) 簡(jiǎn)單但慢;延遲隊(duì)列 實(shí)時(shí)但重;Redis 過(guò)期 輕量但需兜底。
在實(shí)際落地中,可以 并行運(yùn)行 兩種方案(如延遲隊(duì)列 + 兜底定時(shí)任務(wù)),通過(guò)配置開(kāi)關(guān)靈活切換,確保業(yè)務(wù)永遠(yuǎn)在線。祝你的訂單永不超賣!
以上就是SpringBoot訂單超時(shí)自動(dòng)取消的三種主流實(shí)現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot訂單超時(shí)自動(dòng)取消的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
出現(xiàn)SLF4J:?Failed?to?load?class?“org.slf4j.impl.StaticLog
本文主要介紹了出現(xiàn)SLF4J:?Failed?to?load?class?“org.slf4j.impl.StaticLoggerBinder“.的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
LRU算法及Apache?LRUMap源碼實(shí)例解析
這篇文章主要給大家介紹了關(guān)于LRU算法及Apache?LRUMap源碼解析的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2021-11-11
Springboot使用zxing實(shí)現(xiàn)二維碼生成和解析
ZXing支持各種條形碼,二維碼掃描,由多個(gè)模塊組成,?而且支持PC端,移動(dòng)端,本文將利用zxing實(shí)現(xiàn)二維碼生成和解析,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-10-10
Java 中的HashMap詳解和使用示例_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java 中的HashMap詳解和使用示例_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理,需要的朋友可以參考下2017-05-05
idea在運(yùn)行期間,實(shí)現(xiàn)讓修改的頁(yè)面實(shí)時(shí)生效
這篇文章主要介紹了idea在運(yùn)行期間,實(shí)現(xiàn)讓修改的頁(yè)面實(shí)時(shí)生效問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Dubbo之SPI機(jī)制的實(shí)現(xiàn)原理和優(yōu)勢(shì)分析
這篇文章主要介紹了Dubbo之SPI機(jī)制的實(shí)現(xiàn)原理和優(yōu)勢(shì),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-05-05

