SpringBoot訂單超時(shí)自動(dòng)取消的三種主流實(shí)現(xiàn)方案
引言
在電商、外賣(mài)、票務(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. 思路
周期性?huà)呙栌唵伪恚?ldquo;創(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ī)多線(xiàn)程:
@Async("cancelExecutor")+ 線(xiàn)程池。
4. 優(yōu)缺點(diǎn)
- ? 零依賴(lài)、實(shí)現(xiàn)快
- ? 數(shù)據(jù)量大時(shí) DB 壓力大;實(shí)時(shí)性受輪詢(xún)間隔限制
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ù)尾號(hào)或城市分批切換方案。
- 監(jiān)控指標(biāo):
- 取消成功率
- MQ 消息積壓
- Redis 過(guò)期 QPS
- 定時(shí)任務(wù)掃描耗時(shí)
八、小結(jié)
一句話(huà)總結(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)在線(xiàn)。祝你的訂單永不超賣(mài)!
以上就是SpringBoot訂單超時(shí)自動(dòng)取消的三種主流實(shí)現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot訂單超時(shí)自動(dòng)取消的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Can''t use Subversion command line client:svn 報(bào)錯(cuò)處理
這篇文章主要介紹了Can't use Subversion command line client:svn 報(bào)錯(cuò)處理的相關(guān)資料,需要的朋友可以參考下2016-09-09
Spring Boot 整合 MyBatis 連接數(shù)據(jù)庫(kù)及常見(jiàn)問(wèn)題
MyBatis 是一個(gè)優(yōu)秀的持久層框架,支持定制化 SQL、存儲(chǔ)過(guò)程以及高級(jí)映射,下面詳細(xì)介紹如何在 Spring Boot 項(xiàng)目中整合 MyBatis 并連接數(shù)據(jù)庫(kù),感興趣的朋友一起看看吧2025-03-03
SpringBoot項(xiàng)目如何添加2FA雙因素身份認(rèn)證
雙因素身份驗(yàn)證2FA是一種安全系統(tǒng),要求用戶(hù)提供兩種不同的身份驗(yàn)證方式才能訪(fǎng)問(wèn)某個(gè)系統(tǒng)或服務(wù),國(guó)內(nèi)普遍做短信驗(yàn)證碼這種的用的比較少,不過(guò)在國(guó)外的網(wǎng)站中使用雙因素身份驗(yàn)證的還是很多的,這篇文章主要介紹了SpringBoot項(xiàng)目如何添加2FA雙因素身份認(rèn)證,需要的朋友參考下2024-04-04
阿里SpringBoot應(yīng)用自動(dòng)化部署實(shí)現(xiàn)IDEA版Jenkins
這篇文章主要為大家介紹了阿里SpringBoot應(yīng)用自動(dòng)化部署實(shí)現(xiàn)IDEA版Jenkins過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
java -jar后臺(tái)啟動(dòng)的四種方式小結(jié)
這篇文章主要介紹了java -jar后臺(tái)啟動(dòng)的四種方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09
Java中JWT令牌實(shí)現(xiàn)登錄驗(yàn)證
本文主要介紹了JWT令牌在Java中實(shí)現(xiàn)登錄驗(yàn)證的方法,JWT是一種自我包含的、無(wú)狀態(tài)的認(rèn)證機(jī)制,可以用來(lái)在客戶(hù)端和服務(wù)器之間傳遞安全可靠的信息,感興趣的可以了解一下2024-12-12

