美團(tuán)二面:如何設(shè)計(jì)一個(gè)訂單超時(shí)未支付關(guān)閉訂單的解決方案?
訂單超時(shí)未支付自動(dòng)取消是一個(gè)典型的電商和在線交易業(yè)務(wù)場(chǎng)景,在該場(chǎng)景下,用戶在購(gòu)物平臺(tái)上下單后,系統(tǒng)通常會(huì)為用戶提供一段有限的時(shí)間來(lái)完成支付。如果用戶在這個(gè)指定的時(shí)間窗口內(nèi)沒(méi)有成功完成支付,系統(tǒng)將自動(dòng)執(zhí)行訂單取消操作。
當(dāng)然類似的業(yè)務(wù)場(chǎng)景還有:
- 我們預(yù)約釘釘會(huì)議后,釘釘會(huì)在會(huì)議開(kāi)始前15分鐘、5分鐘提醒。
- 淘寶收到貨物簽收之后,超過(guò)7天沒(méi)有確認(rèn)收貨,會(huì)自動(dòng)確認(rèn)收貨。
- 未使用的優(yōu)惠券有效期結(jié)束后,自動(dòng)將優(yōu)惠券狀態(tài)更新為已過(guò)期。
- 用戶登錄失敗次數(shù)過(guò)多后,賬號(hào)鎖定一段時(shí)間,利用延遲隊(duì)列在鎖定期滿后自動(dòng)解鎖賬號(hào)。而針對(duì)這種業(yè)務(wù)需求,我們常見(jiàn)的兩中技術(shù)方向即:定時(shí)輪訓(xùn)訂單之后判斷是否取消以及延遲隊(duì)列實(shí)現(xiàn)。而到具體的技術(shù)方案主要有以下幾種:
圖片
本文主要介紹以下幾種主流方案。
定時(shí)輪訓(xùn)(SpringBoot的Scheduled實(shí)現(xiàn))
定時(shí)輪訓(xùn)的方式都是基于定時(shí)定任務(wù)掃描訂單表,按照下單時(shí)間以及狀態(tài)進(jìn)行過(guò)濾,之后在進(jìn)行判斷是否在有效期內(nèi),如果不在,則取消訂單。
如以下,我們使用SpringBoot中的定時(shí)任務(wù)實(shí)現(xiàn):
我們先創(chuàng)建定時(shí)任務(wù)的配置,設(shè)置任務(wù)每隔5秒執(zhí)行一次。
@Configuration
@EnableScheduling
public class CustomSchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler = threadPoolTaskScheduler();
taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); // 設(shè)置自定義的TaskScheduler
// 根據(jù)任務(wù)信息創(chuàng)建CronTrigger
CronTrigger cronTrigger = new CronTrigger("0/5 * * * * ?");
// 創(chuàng)建任務(wù)執(zhí)行器(假設(shè)TaskExecutor是實(shí)現(xiàn)了Runnable接口的對(duì)象)
MyTaskExecutor taskExecutor = new MyTaskExecutor();
// 使用自定義的TaskScheduler調(diào)度任務(wù)
threadPoolTaskScheduler.schedule(taskExecutor, cronTrigger);
}
@Bean(destroyMethod = "shutdown")
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 設(shè)置線程池大小
scheduler.setThreadNamePrefix("scheduled-task-"); // 設(shè)置線程名稱前綴
scheduler.setAwaitTerminationSeconds(60); // 設(shè)置終止等待時(shí)間
return scheduler;
}
}
然后在MyTaskExecutor中實(shí)現(xiàn)掃描訂單以及判斷訂單是否需要取消:
public class MyTaskExecutor implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 在 "+ LocalDateTime.now() +" 執(zhí)行MyTaskExecutor。。。。。");
}
}
運(yùn)行結(jié)果如下:
圖片
采用定時(shí)任務(wù)機(jī)制來(lái)實(shí)現(xiàn)實(shí)時(shí)監(jiān)測(cè)并取消超時(shí)訂單的方法相對(duì)直接易行,我們可以運(yùn)用諸如Quartz、XXL-Job或Elastic-Job等成熟的定時(shí)任務(wù)框架進(jìn)行集群部署,從而提升任務(wù)執(zhí)行效能。然而,此類方案存在顯著局限:
首先,定時(shí)輪詢訂單表的方式在訂單數(shù)量龐大的情況下會(huì)對(duì)數(shù)據(jù)庫(kù)帶來(lái)持續(xù)且顯著的壓力,因?yàn)轭l繁地全表掃描無(wú)疑會(huì)增加I/O負(fù)擔(dān)和CPU使用率。
其次,定時(shí)任務(wù)執(zhí)行的間隔設(shè)定頗為棘手。若設(shè)定的間隔時(shí)間較長(zhǎng),可能會(huì)導(dǎo)致訂單超時(shí)后的取消動(dòng)作出現(xiàn)延遲,影響用戶體驗(yàn);相反,若時(shí)間間隔設(shè)置得過(guò)短,則會(huì)導(dǎo)致大量訂單被重復(fù)掃描和判斷,不僅浪費(fèi)計(jì)算資源,還可能導(dǎo)致不必要的并發(fā)問(wèn)題和事務(wù)沖突,尤其是在高并發(fā)交易的高峰期。
在實(shí)際應(yīng)用中,針對(duì)大流量訂單場(chǎng)景下的超時(shí)處理,往往更傾向于采用延遲隊(duì)列技術(shù)而非簡(jiǎn)單的定時(shí)任務(wù)輪詢,以實(shí)現(xiàn)更為精確、高效的超時(shí)邏輯處理。
關(guān)于SpringBoot的定時(shí)任務(wù)實(shí)現(xiàn)的幾種方式,請(qǐng)參考:玩轉(zhuǎn)SpringBoot:SpringBoot的幾種定時(shí)任務(wù)實(shí)現(xiàn)方式
JDK的延遲隊(duì)列
使用JDK自帶的DelayQueue實(shí)現(xiàn)一個(gè)延遲隊(duì)列并處理超時(shí)訂單,首先我們需要定義一個(gè)實(shí)現(xiàn)了Delayed接口的訂單對(duì)象類,然后創(chuàng)建DelayQueue實(shí)例并不斷從隊(duì)列中取出已超時(shí)的訂單進(jìn)行處理。
我們定義一個(gè)包含訂單信息和延遲時(shí)間的訂單類:
@Getter
public class DelayedOrder implements Delayed {
private final String orderNo;
private final long expireTimeMillis; // 訂單超時(shí)時(shí)間戳(毫秒)
public DelayedOrder(String orderNo, long delayInSeconds) {
this.orderNo = orderNo;
// 設(shè)置訂單在當(dāng)前時(shí)間多少秒后超時(shí)
this.expireTimeMillis = System.currentTimeMillis() + delayInSeconds;
}
@Override
public long getDelay(TimeUnit unit) {
long remainingNanos = expireTimeMillis - System.currentTimeMillis();
return unit.convert(remainingNanos, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
if (other == this) {
return 0;
}
DelayedOrder t = (DelayedOrder) other;
long d = (getDelay(TimeUnit.MILLISECONDS) - t.getDelay(TimeUnit.MILLISECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
// 其他訂單屬性及方法...
// 處理訂單取消的邏輯
public void cancelOrder() {
// 在這里調(diào)用實(shí)際的服務(wù)接口或方法取消訂單
}
}
然后我們就可以使用DelayQueue處理超時(shí)訂單:
@Component
public class OrderDelayQueue {
private final DelayQueue<DelayedOrder> delayQueue = new DelayQueue<>();
public void addOrderToQueue(DelayedOrder order) {
delayQueue.put(order);
System.out.println("訂單 " + order.getOrderNo() + "在 "+LocalDateTime.now()+" 添加到延遲隊(duì)列");
}
// 啟動(dòng)訂單處理線程池
@Autowired
private ExecutorService executorService;
@PostConstruct
public void init() {
executorService.execute(this::processOrders);
}
private void processOrders() {
while (true) {
try {
DelayedOrder order = delayQueue.take(); // 從延遲隊(duì)列中取出已經(jīng)過(guò)期的訂單
System.out.println("訂單 " + order.getOrderNo() + "在 "+ LocalDateTime.now() +" 取消");
order.cancelOrder();
// 在這里執(zhí)行取消訂單的邏輯,比如更新數(shù)據(jù)庫(kù)狀態(tài)等
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我們實(shí)現(xiàn)一個(gè)創(chuàng)建訂單的接口,模擬訂單創(chuàng)建:
@RestController
@RequestMapping("orderDelay")
public class OrderDelayController {
@Autowired
private OrderDelayQueue orderDelayQueue;
@PostMapping("add")
public void addOrder() {
// 202403221901 2秒后取消
DelayedOrder delayedOrder = new DelayedOrder("202403221901", 2000);
orderDelayQueue.addOrderToQueue(delayedOrder);
// 202403221902 3秒后取消
DelayedOrder delayedOrder1 = new DelayedOrder("202403221902", 3000);
orderDelayQueue.addOrderToQueue(delayedOrder1);
// 202403221903 5秒后取消
delayedOrder = new DelayedOrder("202403221903", 6000);
orderDelayQueue.addOrderToQueue(delayedOrder);
delayedOrder = new DelayedOrder("202403221904", 8000);
orderDelayQueue.addOrderToQueue(delayedOrder);
}
}
請(qǐng)求接口,發(fā)現(xiàn)訂單超過(guò)各自的時(shí)間之后,都超時(shí)了。當(dāng)然真實(shí)場(chǎng)景是超時(shí)時(shí)間一致,只是訂單創(chuàng)建時(shí)間不一致。
圖片
基于JDK的DelayQueue實(shí)現(xiàn)的延遲隊(duì)列解決取消超時(shí)訂單的方案,相比較于定時(shí)輪訓(xùn)有如下優(yōu)點(diǎn):
- DelayQueue基于優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn),內(nèi)部使用了堆數(shù)據(jù)結(jié)構(gòu),插入和刪除操作的時(shí)間復(fù)雜度為O(log n),對(duì)于大量訂單的處理效率較高。
- 相比于定期查詢數(shù)據(jù)庫(kù)的方式,DelayQueue將待處理的訂單信息保留在內(nèi)存中,減少了對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)頻率,降低了IO壓力。
- DelayQueue是java.util.concurrent包下的工具類,本身就具備良好的線程安全特性,可以在多線程環(huán)境下穩(wěn)定工作。
但是因?yàn)镈elayQueue是基于內(nèi)存的,這也導(dǎo)致它在實(shí)現(xiàn)上有一定的缺點(diǎn):
- 所有待處理的訂單信息都需要保留在內(nèi)存中,對(duì)于大量訂單,可能會(huì)造成較大的內(nèi)存消耗。
- 由于所有的超時(shí)信息都依賴于內(nèi)存中的隊(duì)列,如果系統(tǒng)崩潰或重啟,未處理的訂單信息可能丟失,除非有額外的持久化措施。
時(shí)間輪算法
在介紹時(shí)間輪算法實(shí)現(xiàn)取消超時(shí)訂單功能之前,我們先來(lái)看一下什么是時(shí)間輪算法?
時(shí)間輪算法(Time Wheel Algorithm)是一種高效處理定時(shí)任務(wù)調(diào)度的機(jī)制,廣泛應(yīng)用于各類系統(tǒng)如計(jì)時(shí)器、調(diào)度器等組件。該算法的關(guān)鍵理念在于將時(shí)間維度映射至物理空間,即構(gòu)建一個(gè)由多個(gè)時(shí)間槽構(gòu)成的循環(huán)結(jié)構(gòu),每個(gè)槽代表一個(gè)固定的時(shí)間單位(如毫秒、秒等)。
時(shí)間輪實(shí)質(zhì)上是一個(gè)具有多個(gè)槽位的環(huán)形數(shù)據(jù)結(jié)構(gòu),隨著時(shí)間的推進(jìn),時(shí)間輪上的指針按照預(yù)先設(shè)定的速度(例如每秒前進(jìn)一槽)順時(shí)針旋轉(zhuǎn)。每當(dāng)指針移動(dòng)至下一槽位時(shí),系統(tǒng)會(huì)檢視該槽位中掛載的所有定時(shí)任務(wù),并逐一執(zhí)行到期的任務(wù)。
在時(shí)間輪中,每個(gè)待執(zhí)行任務(wù)均與其觸發(fā)時(shí)間點(diǎn)對(duì)應(yīng)的時(shí)間槽關(guān)聯(lián)。添加新任務(wù)時(shí),系統(tǒng)會(huì)根據(jù)任務(wù)的期望執(zhí)行時(shí)間計(jì)算出相應(yīng)的槽位編號(hào),并將任務(wù)插入該槽。對(duì)于未來(lái)執(zhí)行的任務(wù),計(jì)算所需等待的槽位數(shù)目,確保任務(wù)按時(shí)被處理。值得注意的是,時(shí)間輪設(shè)計(jì)為循環(huán)結(jié)構(gòu),意味著當(dāng)指針到達(dá)最后一個(gè)槽位后會(huì)自動(dòng)返回至第一個(gè)槽位,形成連續(xù)不斷的循環(huán)調(diào)度。
借助時(shí)間輪算法,定時(shí)任務(wù)的執(zhí)行時(shí)間以相對(duì)固定的時(shí)間槽來(lái)表示,而非直接依賴于絕對(duì)時(shí)間。任務(wù)執(zhí)行完畢后,系統(tǒng)會(huì)及時(shí)將其從時(shí)間輪中移除,同時(shí),對(duì)于不再需要執(zhí)行的任務(wù),也可以在任何時(shí)候予以移除,確保整個(gè)調(diào)度系統(tǒng)的高效運(yùn)作和實(shí)時(shí)響應(yīng)。
圖片
如上圖為例,假設(shè)一個(gè)格子是1秒,則整個(gè)wheel能表示的時(shí)間段為8s,假設(shè)當(dāng)前指針指向2,此時(shí)需要調(diào)度一個(gè)3s后執(zhí)行的任務(wù), 顯然應(yīng)該加入到(2+3=5)的方格中,指針再走3次就可以執(zhí)行了;如果任務(wù)要在10s后執(zhí)行,應(yīng)該等指針走完一個(gè)round零2格再執(zhí)行, 因此應(yīng)放入4,同時(shí)將round(1)保存到任務(wù)中。檢查到期任務(wù)應(yīng)當(dāng)只執(zhí)行round為0的,格子上其他任務(wù)的round應(yīng)減1.
所以,我們可以使用時(shí)間輪算法去試一下延遲任務(wù),用于實(shí)現(xiàn)取消超時(shí)訂單。
我們以Netty4為例,引入依賴:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.68.Final</version>
</dependency>
然后定義訂單處理服務(wù),在創(chuàng)建訂單時(shí)定義訂單超時(shí)時(shí)間,以及超時(shí)時(shí)取消訂單。
@Service
public class OrderService {
private final Map<String, Timeout> orderTimeouts = new HashMap<>();
private final HashedWheelTimer timer = new HashedWheelTimer();
public void createOrder(String orderId) {
System.out.println("訂單"+orderId+"在"+ LocalDateTime.now() +"創(chuàng)建成功.");
// 創(chuàng)建訂單,設(shè)置超時(shí)時(shí)間為5秒鐘
Timeout timeout = timer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 超時(shí)處理邏輯,取消訂單
cancelOrder(orderId);
}
}, Duration.ofSeconds(5).toMillis(), TimeUnit.MILLISECONDS);
orderTimeouts.put(orderId, timeout);
}
public void cancelOrder(String orderId) {
// 取消訂單的邏輯
orderTimeouts.remove(orderId);
System.out.println(orderId+"訂單超時(shí),在"+ LocalDateTime.now() +"取消訂單:" + orderId);
}
}
我們定義訂單創(chuàng)建接口,模擬訂單創(chuàng)建:
@RestController
@RequestMapping("orderTimeWheel")
public class OrderTimeWheelController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public String createOrder(String orderId) {
orderService.createOrder(orderId);
return "訂單創(chuàng)建成功:" + orderId;
}
}
我們分別請(qǐng)求接口,創(chuàng)建訂單:
圖片
可以看見(jiàn),訂單在5秒鐘之后自動(dòng)調(diào)用取消方法取消訂單。
基于時(shí)間輪實(shí)現(xiàn)延遲任務(wù)來(lái)取消超時(shí)訂單有如下優(yōu)點(diǎn):
- 時(shí)間輪算法能夠高效地管理大量的定時(shí)任務(wù),其執(zhí)行時(shí)間與任務(wù)數(shù)量無(wú)關(guān),因此非常適合處理大規(guī)模的定時(shí)任務(wù)。
- 時(shí)間輪算法能夠提供相對(duì)精確的超時(shí)控制,可以在指定的時(shí)間后執(zhí)行任務(wù)或者取消任務(wù),從而確保超時(shí)訂單能夠及時(shí)取消。并且時(shí)間輪算法允許靈活地管理時(shí)間間隔和超時(shí)時(shí)間,可以根據(jù)具體業(yè)務(wù)需求進(jìn)行調(diào)整和優(yōu)化。
- 時(shí)間輪算法的實(shí)現(xiàn)相對(duì)簡(jiǎn)單,算法本身比較容易理解,且現(xiàn)有的實(shí)現(xiàn)庫(kù)如Netty的HashedWheelTimer已經(jīng)提供了成熟的實(shí)現(xiàn),因此可以很方便地集成到現(xiàn)有的系統(tǒng)中。
- 基于內(nèi)存操作,減少一些IO壓力。
但是相對(duì)應(yīng)的也存在一些缺點(diǎn):
- 時(shí)間輪算法需要維護(hù)一個(gè)槽的數(shù)據(jù)結(jié)構(gòu),因此會(huì)占用一定的內(nèi)存和計(jì)算資源,對(duì)于一些資源受限的環(huán)境可能會(huì)存在一定的壓力。同DelayQueue,在大量訂單時(shí)會(huì)對(duì)內(nèi)存造成較大的內(nèi)存消耗。同時(shí)也會(huì)影響延遲精度。
- 同時(shí),如果系統(tǒng)崩潰或者重啟,未處理的訂單信息可能丟失,除非有額外的持久化措施。
Redis實(shí)現(xiàn)
對(duì)于Redis實(shí)現(xiàn)延遲任務(wù),常見(jiàn)的兩種方案是使用有序集合(Sorted Set,通常簡(jiǎn)稱為zset)和使用key過(guò)期監(jiān)聽(tīng)。
定時(shí)輪訓(xùn)有序集合
利用有序集合的特性,即集合中的元素是有序的,每個(gè)元素都有一個(gè)分?jǐn)?shù)(score)。在延遲任務(wù)的場(chǎng)景中,可以將任務(wù)的執(zhí)行時(shí)間作為分?jǐn)?shù),將任務(wù)的唯一標(biāo)識(shí)(如任務(wù)ID)作為集合中的元素。然后,定時(shí)輪詢有序集合,查找分?jǐn)?shù)小于當(dāng)前時(shí)間的元素,這些元素即為已經(jīng)到期需要執(zhí)行的任務(wù)。執(zhí)行完任務(wù)后,可以從有序集合中刪除對(duì)應(yīng)的元素。因此可以將訂單的過(guò)期時(shí)間作為score,用于實(shí)現(xiàn)取消超時(shí)訂單。
引入Redis依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.0</version>
</dependency>
配置一下RedisTemplate:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
// 其余配置 如序列化等
return new StringRedisTemplate(factory);
}
}
創(chuàng)建訂單創(chuàng)建以及自動(dòng)取消服務(wù):
@EnableScheduling
@Service
public class OrderZSetService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// key: orders:timeout, value: order_id:order_expiration_time
private static final String ORDER_TIMEOUT_SET_KEY = "orders:timeout";
public void createOrder(String orderId) {
System.out.println("訂單"+orderId+"在"+ LocalDateTime.now() +"創(chuàng)建成功.");
// 假設(shè)訂單超時(shí)時(shí)間為5秒
long expirationTime = 5 * 1000 + System.currentTimeMillis();
redisTemplate.opsForZSet().add(ORDER_TIMEOUT_SET_KEY, orderId, expirationTime);
}
@Scheduled(fixedRate = 1000) // 每秒檢查一次,實(shí)際頻率根據(jù)業(yè)務(wù)需求調(diào)整
public void checkAndProcessTimeoutOrders() {
Long now = System.currentTimeMillis();
Set<ZSetOperations.TypedTuple<String>> range = redisTemplate.opsForZSet().rangeByScoreWithScores(ORDER_TIMEOUT_SET_KEY, 0, now);
for (ZSetOperations.TypedTuple<String> tuple : range) {
String orderId = (String) tuple.getValue();
if (tuple.getScore() <= now) {
// 處理超時(shí)訂單
cancelOrder(orderId);
// 從有序集合中移除已處理的超時(shí)訂單
redisTemplate.opsForZSet().remove(ORDER_TIMEOUT_SET_KEY, orderId);
}
}
}
private void cancelOrder(String orderId) {
// 在這里實(shí)現(xiàn)訂單取消的實(shí)際邏輯
System.out.println("訂單 " + orderId + " 在" + LocalDateTime.now() +"取消");
// 更新訂單狀態(tài)、釋放庫(kù)存等操作...
}
}
注意:因本例中基于@Scheduled實(shí)現(xiàn)定時(shí)輪訓(xùn),所以需要使用@EnableScheduling開(kāi)啟Scheduled功能。具體請(qǐng)參考:玩轉(zhuǎn)SpringBoot:SpringBoot的幾種定時(shí)任務(wù)實(shí)現(xiàn)方式
我們定義訂單創(chuàng)建接口,模擬訂單創(chuàng)建:
圖片
可以看到訂單5秒鐘后自動(dòng)取消。
使用Redis有序集合實(shí)現(xiàn)取消超時(shí)訂單有一些優(yōu)點(diǎn):
- 有序集合可以根據(jù)分?jǐn)?shù)(過(guò)期時(shí)間)快速定位到需要處理的超時(shí)訂單,避免了對(duì)全部訂單的全表掃描,提高了查詢效率。
- 在分布式環(huán)境中,Redis作為緩存和中間件,可以很容易地實(shí)現(xiàn)在多節(jié)點(diǎn)間共享超時(shí)訂單信息,有利于分布式系統(tǒng)中統(tǒng)一管理超時(shí)訂單。
- 利用Redis內(nèi)存數(shù)據(jù)結(jié)構(gòu),不需要頻繁讀寫(xiě)數(shù)據(jù)庫(kù),降低了數(shù)據(jù)庫(kù)的壓力,同時(shí)也節(jié)約了數(shù)據(jù)庫(kù)資源。
但是也有一些缺點(diǎn):
- 定時(shí)任務(wù)的執(zhí)行頻率決定了處理超時(shí)訂單的精確程度,頻率太低可能導(dǎo)致部分訂單未能及時(shí)取消,頻率太高則可能浪費(fèi)系統(tǒng)資源。
- 在涉及事務(wù)處理的情況下,可能需要額外的手段來(lái)保證與數(shù)據(jù)庫(kù)之間的數(shù)據(jù)一致性,防止因Redis處理超時(shí)訂單后,數(shù)據(jù)庫(kù)層面的更新失敗導(dǎo)致的數(shù)據(jù)不一致問(wèn)題。
- 在處理超時(shí)訂單過(guò)程中,若出現(xiàn)異常,需要配套的重試機(jī)制。
使用Redis key過(guò)期監(jiān)聽(tīng)
利用Redis的key過(guò)期監(jiān)聽(tīng)功能。當(dāng)設(shè)置一個(gè)key的過(guò)期時(shí)間時(shí),可以設(shè)置一個(gè)回調(diào)函數(shù),當(dāng)key過(guò)期時(shí),Redis會(huì)自動(dòng)調(diào)用這個(gè)回調(diào)函數(shù)。即利用Redis的Keyspace Notifications功能,當(dāng)一個(gè)鍵(Key)過(guò)期時(shí),Redis會(huì)向已訂閱了相關(guān)頻道的客戶端發(fā)送一個(gè)通知。
使用Redis的key的過(guò)期監(jiān)聽(tīng)功能之前我們需要啟用Redis Keyspace Notifications,在Redis配置文件(redis.conf)中啟用Key Space Notifications,即打開(kāi)如下配置:
notify-keyspace-events Ex
notify-keyspace-events設(shè)置為Ex,表示啟用所有類型的鍵空間通知,包括過(guò)期事件。具體配置方法可能因Redis的版本和環(huán)境而有所不同,請(qǐng)根據(jù)實(shí)際情況進(jìn)行配置。
然后我們就可以使用代碼實(shí)現(xiàn),首先實(shí)現(xiàn)MessageListener接口實(shí)現(xiàn)一個(gè)監(jiān)聽(tīng)器來(lái)監(jiān)聽(tīng)Redis的key過(guò)期事件。當(dāng)訂單的key過(guò)期時(shí),將觸發(fā)監(jiān)聽(tīng)器中的邏輯,執(zhí)行取消訂單的操作。
@Component
public class OrderExpirationListener implements MessageListener {
@Autowired
private OrderExpirationService orderService;
@Override
public void onMessage(Message message, byte[] pattern) {
String orderId = message.toString();
// 調(diào)用服務(wù)取消訂單
orderService.cancelOrder(orderId);
}
}
然后配置Redis key過(guò)期事件監(jiān)聽(tīng)器,并將其注冊(cè)到Redis連接工廠中。這樣,監(jiān)聽(tīng)器將會(huì)在Redis的key過(guò)期事件發(fā)生時(shí)被調(diào)用。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
// 其余配置 如序列化等
return new StringRedisTemplate(factory);
}
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
OrderExpirationListener listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listener, new ChannelTopic("__keyevent@0__:expired")); // 監(jiān)聽(tīng)所有數(shù)據(jù)庫(kù)的key過(guò)期事件
return container;
}
}
__keyevent@0__:expired是Redis的系統(tǒng)通道,用于監(jiān)聽(tīng)所有數(shù)據(jù)庫(kù)中的key過(guò)期事件。如果需要監(jiān)聽(tīng)特定數(shù)據(jù)庫(kù)的key過(guò)期事件,則可以修改對(duì)應(yīng)的數(shù)據(jù)庫(kù)號(hào)。例如,__keyevent@1__:expired表示監(jiān)聽(tīng)第一個(gè)數(shù)據(jù)庫(kù)的key過(guò)期事件。
然后我們就可以實(shí)現(xiàn)具體的訂單創(chuàng)建服務(wù)以及訂單取消的邏輯了。這里我們模擬一下:
@Component
public class OrderExpirationService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void createOrder(String orderId) {
System.out.println("訂單"+orderId+"在"+ LocalDateTime.now() +"創(chuàng)建成功.");
// 假設(shè)訂單超時(shí)時(shí)間為5秒
long expirationTime = 5;
redisTemplate.opsForValue().set(orderId, "orderData", expirationTime, TimeUnit.SECONDS);
}
public void cancelOrder(String orderId) {
// 在這里實(shí)現(xiàn)訂單取消的實(shí)際邏輯
System.out.println("訂單 " + orderId + " 在" + LocalDateTime.now() +"取消");
// 更新訂單狀態(tài)、釋放庫(kù)存等操作...
}
}
@RestController
@RequestMapping("orderRedis")
public class RedisOrderController {
@Autowired
private OrderExpirationService orderService;
@PostMapping("/create")
public String createOrder(String orderId) {
orderService.createOrder(orderId);
return "訂單創(chuàng)建成功:" + orderId;
}
}
我們創(chuàng)建4個(gè)訂單,模擬5秒鐘后的訂單取消
圖片
使用Redis的key過(guò)期監(jiān)聽(tīng)事件,實(shí)現(xiàn)取消超時(shí)訂單有以下優(yōu)點(diǎn):
- Redis鍵過(guò)期事件能在鍵過(guò)期時(shí)立即觸發(fā)監(jiān)聽(tīng)器,因此可以在訂單超時(shí)的瞬間準(zhǔn)確執(zhí)行取消操作,大大提高了時(shí)效性。
- 相比定期輪詢數(shù)據(jù)庫(kù)查詢超時(shí)訂單的方式,Redis鍵過(guò)期事件是被動(dòng)觸發(fā),節(jié)省了CPU和網(wǎng)絡(luò)資源,減少了無(wú)效查詢。
- Redis的鍵過(guò)期事件處理機(jī)制天然支持高并發(fā)場(chǎng)景,只要Redis集群足夠強(qiáng)大,可以輕松處理大量訂單的過(guò)期處理。
- 資源占用小,只需要維護(hù)Redis中少量的鍵,相對(duì)于數(shù)據(jù)庫(kù)存儲(chǔ)所有訂單信息并做定時(shí)任務(wù)查詢,內(nèi)存和磁盤(pán)資源占用較少。
但是也存在一些缺點(diǎn):
- 整個(gè)方案依賴于Redis服務(wù)的穩(wěn)定性和性能,如果Redis服務(wù)出現(xiàn)問(wèn)題,可能會(huì)影響訂單超時(shí)處理。
- 在高并發(fā)場(chǎng)景下,Redis過(guò)期事件產(chǎn)生的速率可能非常高,如果處理不當(dāng),監(jiān)聽(tīng)器本身的處理能力可能成為瓶頸,導(dǎo)致消息堆積,這時(shí)需要考慮消息隊(duì)列或者其他緩沖機(jī)制。
- Redis的鍵過(guò)期并不是嚴(yán)格意義上的實(shí)時(shí),而是基于定期檢查機(jī)制,極端情況下可能存在一定的延遲。盡管在實(shí)踐中這種延遲很小,但對(duì)于極高精度要求的場(chǎng)景,可能需要額外關(guān)注。
MQ消息隊(duì)列
使用消息隊(duì)列實(shí)現(xiàn)取消超時(shí)訂單的常見(jiàn)方法是利用延遲隊(duì)列以及死信隊(duì)列。比如RabbitMq,在介紹實(shí)現(xiàn)方式之前,我們先來(lái)了解一下RabbitMq的延遲隊(duì)列以及死信隊(duì)列。
- 延遲隊(duì)列:RabbitMQ本身并不直接支持延遲隊(duì)列,但可以通過(guò)安裝rabbitmq_delayed_message_exchange插件來(lái)實(shí)現(xiàn)延遲消息的功能。當(dāng)啟用這個(gè)插件后,你可以創(chuàng)建一個(gè)類型為x-delayed-message的交換機(jī)。在發(fā)送消息時(shí),可以設(shè)置消息頭中的x-delay字段,表示消息應(yīng)該在多久之后才開(kāi)始被路由到綁定的目標(biāo)隊(duì)列。這樣,當(dāng)一個(gè)訂單創(chuàng)建時(shí),可以將包含訂單ID和過(guò)期時(shí)間的消息發(fā)送到延遲交換機(jī),并設(shè)置相應(yīng)的延遲時(shí)間。當(dāng)延遲時(shí)間結(jié)束時(shí),消息將被發(fā)送到處理超時(shí)訂單的隊(duì)列,隨后由消費(fèi)者進(jìn)行訂單狀態(tài)檢查和取消操作。
我的RabbitMq是部署在docker中的,所以順帶提議一下關(guān)于安裝rabbitmq_delayed_message_exchange插件,我們需要在 Releases · rabbitmq/rabbitmq-delayed-message-exchange (github.com)下載.ez結(jié)尾的插件,然后使用docker cp命令將其拷貝到rabbitmq容器內(nèi):
docker cp <本地路徑>/rabbitmq_delayed_message_exchange-3.13.0.ez <容器ID>:/plugins
然后我們進(jìn)入容器后啟動(dòng)插件:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
然后驗(yàn)證一下插件是否開(kāi)啟成功:
rabbitmq-plugins list | grep delayed
圖片
我的rabbitmq的版本是3.13.0
- 死信隊(duì)列死信隊(duì)列是指當(dāng)消息在原始隊(duì)列中遇到某種情況(如消息過(guò)期、消息被拒絕等)時(shí),會(huì)被重新路由到另一個(gè)預(yù)定義的隊(duì)列中。當(dāng)消息在隊(duì)列中停留的時(shí)間超過(guò)TTL,該消息就會(huì)變成死信,并根據(jù)隊(duì)列配置轉(zhuǎn)發(fā)到死信隊(duì)列。
基于RabbitMq的延遲隊(duì)列
延遲隊(duì)列可以直接處理延遲消息,即消息在指定的延遲時(shí)間過(guò)后才被投遞給消費(fèi)者。在超時(shí)取消訂單的場(chǎng)景中,訂單創(chuàng)建時(shí)將訂單信息封裝成消息,并設(shè)置消息的延遲時(shí)間,當(dāng)訂單超時(shí)時(shí),消息自動(dòng)被投遞到處理超時(shí)訂單的隊(duì)列,消費(fèi)者接收到消息后執(zhí)行取消操作。
引入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.18</version>
</dependency>
配置rabbitmq的相關(guān)參數(shù):
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.password=guest
spring.rabbitmq.username=guest
配置延遲交換機(jī),并且初始化延遲交換機(jī)、隊(duì)列及綁定關(guān)系
@Configuration
@EnableRabbit
public class RabbitConfig {
public static final String ORDER_EXCHANGE = "order.delayed.exchange";
public static final String ORDER_QUEUE = "order.delayed.queue";
public static final String ROUTING_KEY = "delayed-routing-key";
@Bean
public CustomExchange delayedExchange() {
return new CustomExchange(ORDER_EXCHANGE, "x-delayed-message", true, false);
}
@Bean
public Queue delayedQueue() {
return new Queue(ORDER_QUEUE);
}
@Bean
public Binding delayedBinding(CustomExchange delayedExchange, Queue delayedQueue) {
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(ROUTING_KEY).noargs();
}
}
這里交換機(jī)exchange,需要我們事先在rabbitmq中創(chuàng)建好,訪問(wèn)http://localhost:15672/在Exchanges中,添加Exchange,設(shè)置type= x-delayed-message,如圖:
圖片
在定義一個(gè)監(jiān)聽(tīng)rabbitmq消息的監(jiān)聽(tīng)器,當(dāng)消息延遲時(shí)間到了之后,就會(huì)被該監(jiān)聽(tīng)器見(jiàn)聽(tīng)到,在這里判斷訂單是否已經(jīng)被支付,如果沒(méi)有支付則取消。
@Component
public class DelayedQueueListener {
@Autowired
private OrderMqService orderMqService;
@RabbitListener(queues = RabbitConfig.ORDER_QUEUE)
public void handleDelayedOrder(String orderId) {
orderMqService.cancelOrder(orderId);
}
}
然后我們?cè)谟唵蝿?chuàng)建時(shí),將訂單信息發(fā)送到MQ中,等延遲時(shí)間到了之后,如果訂單還沒(méi)有支付,則執(zhí)行取消訂單操作。
@Service
public class OrderMqService {
private final AmqpTemplate rabbitTemplate;
@Autowired
public OrderMqService(AmqpTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void createOrder(String orderId) {
System.out.println("訂單"+orderId+"在"+ LocalDateTime.now() +"創(chuàng)建成功.");
rabbitTemplate.convertAndSend(RabbitConfig.ORDER_EXCHANGE, RabbitConfig.ROUTING_KEY, orderId, message -> {
message.getMessageProperties().setDelay(5 * 1000);
return message;
});
}
public void cancelOrder(String orderId) {
// 在這里實(shí)現(xiàn)訂單取消的實(shí)際邏輯
System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消");
// 更新訂單狀態(tài)、釋放庫(kù)存等操作...
}
}
我們模擬創(chuàng)建訂單請(qǐng)求:
@RestController
@RequestMapping("orderMq")
public class MqOrderController {
@Autowired
private OrderMqService orderMqService;
@PostMapping("/create")
public String createOrder(String orderId) {
orderMqService.createOrder(orderId);
return "訂單創(chuàng)建成功:" + orderId;
}
}
圖片
可以看見(jiàn)訂單過(guò)了5秒之后開(kāi)始執(zhí)行取消。
使用延遲隊(duì)列方案來(lái)實(shí)現(xiàn)訂單超時(shí)取消等場(chǎng)景的優(yōu)點(diǎn):
- 延遲隊(duì)列能夠在消息到達(dá)指定時(shí)間后立刻觸發(fā)處理,減少不必要的輪詢查詢,提高了處理效率和實(shí)時(shí)性。
- 訂單超時(shí)處理是異步進(jìn)行的,不會(huì)影響主線業(yè)務(wù)流程,有利于提升整體系統(tǒng)的響應(yīng)速度和穩(wěn)定性。
- 延遲隊(duì)列方案使得訂單創(chuàng)建、支付和超時(shí)取消三個(gè)環(huán)節(jié)相互獨(dú)立,有利于系統(tǒng)的模塊化和擴(kuò)展性
- 當(dāng)系統(tǒng)規(guī)模擴(kuò)大時(shí),可以通過(guò)增加消費(fèi)者數(shù)量來(lái)應(yīng)對(duì)更多的超時(shí)訂單處理,實(shí)現(xiàn)水平擴(kuò)展。
但是也有一些缺點(diǎn):
- 高度依賴消息隊(duì)列服務(wù)的可用性和穩(wěn)定性,一旦消息隊(duì)列出現(xiàn)故障,可能導(dǎo)致超時(shí)訂單無(wú)法正常處理。
- 延遲隊(duì)列方案涉及更多的中間件配置和管理,增加了系統(tǒng)的復(fù)雜性。
- 在分布式系統(tǒng)中,如果訂單狀態(tài)不僅在消息隊(duì)列中維護(hù),還要同步到數(shù)據(jù)庫(kù),需要額外保證消息隊(duì)列處理和數(shù)據(jù)庫(kù)操作的一致性。
- 雖然大部分消息隊(duì)列的延遲機(jī)制相當(dāng)可靠,但仍有極小概率出現(xiàn)消息延遲到達(dá)或丟失的情況,需要有相應(yīng)的容錯(cuò)和補(bǔ)償機(jī)制。
對(duì)于延遲隊(duì)列,并非只有rabbitmq才有,RocketMQ也有延遲隊(duì)列。在RocketMQ中,延遲消息的發(fā)送是通過(guò)設(shè)置消息的延遲級(jí)別來(lái)實(shí)現(xiàn)的。每個(gè)延遲級(jí)別都對(duì)應(yīng)著一個(gè)具體的延遲時(shí)間,例如 1 表示 1 秒、2 表示 5 秒、3 表示 10 秒,以此類推。用戶可以根據(jù)自己的需求選擇合適的延遲級(jí)別。但是也可以看出他并沒(méi)有支持的那么精確,如果想要精確的就必須使用RocketMQ的企業(yè)版,在企業(yè)版中可以自定義設(shè)置延遲時(shí)間。這里就不過(guò)多講解,有興趣的可以自己研究一下。
基于RabbitMq的死信隊(duì)列實(shí)現(xiàn)
訂單創(chuàng)建時(shí),將訂單信息發(fā)送到一個(gè)具有TTL的隊(duì)列,當(dāng)消息在隊(duì)列中停留的時(shí)間超過(guò)了TTL(也就是訂單的有效支付期限),消息就會(huì)變?yōu)樗佬?。然后再配置?duì)列,使得這些過(guò)期的死信消息被路由到一個(gè)預(yù)先設(shè)置好的死信隊(duì)列。最后創(chuàng)建一個(gè)消費(fèi)者監(jiān)聽(tīng)這個(gè)死信隊(duì)列,一旦有消息進(jìn)來(lái)(即訂單超時(shí)),消費(fèi)者便處理這些死信,檢查訂單狀態(tài)并執(zhí)行取消操作。
使用的rabbitmq依賴以及配置同上使用延遲隊(duì)列方案。我們來(lái)看一下創(chuàng)建處理訂單即帶有TTL的隊(duì)列:
@Configuration
public class RabbitMQConfig {
/**訂單隊(duì)列*/
public static final String ORDER_QUEUE = "order.queue";
/**死信隊(duì)列交換機(jī)*/
public static final String DEAD_LETTER_EXCHANGE = "order.deadLetter.exchange";
/**死信隊(duì)列*/
public static final String DEAD_LETTER_QUEUE = "order.deadLetter.queue";
/**死信路由*/
public static final String ROUTING_KEY = "delayed-routing-key";
/**
* 創(chuàng)建訂單隊(duì)列
* @return
*/
@Bean
public Queue orderQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000L); // 設(shè)置訂單隊(duì)列消息有效期為30秒(可以根據(jù)實(shí)際情況調(diào)整)
args.put("type", "java.lang.Long");
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
args.put("x-dead-letter-routing-key", ROUTING_KEY);
return new Queue(ORDER_QUEUE, true, false, false, args);
}
}
同理也是需要先創(chuàng)建交換機(jī):
圖片
創(chuàng)建訂單業(yè)務(wù)類,將訂單發(fā)送到訂單消息隊(duì)列:
@Service
public class OrderMqService {
private final AmqpTemplate rabbitTemplate;
@Autowired
public OrderMqService(AmqpTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void createOrder(String orderId) {
System.out.println("訂單"+orderId+"在"+ LocalDateTime.now() +"創(chuàng)建成功.");
rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_QUEUE, orderId);
}
public void cancelOrder(String orderId) {
// 在這里實(shí)現(xiàn)訂單取消的實(shí)際邏輯
System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消");
// 更新訂單狀態(tài)、釋放庫(kù)存等操作...
}
}
在創(chuàng)建死信隊(duì)列,私信隊(duì)列交換機(jī),通過(guò)訂單隊(duì)列路由將私信隊(duì)列綁定到訂單訂單隊(duì)列中:
@Configuration
public class RabbitMQConfig {
/**訂單隊(duì)列*/
public static final String ORDER_QUEUE = "order.queue";
/**死信隊(duì)列交換機(jī)*/
public static final String DEAD_LETTER_EXCHANGE = "order.deadLetter.exchange";
/**死信隊(duì)列*/
public static final String DEAD_LETTER_QUEUE = "order.deadLetter.queue";
/**死信路由*/
public static final String ROUTING_KEY = "delayed-routing-key";
/**
* 創(chuàng)建死信隊(duì)列交換機(jī)
* @return
*/
@Bean
public DirectExchange deadLetterExchange() {
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
/**
* 創(chuàng)建死信隊(duì)列
* @return
*/
@Bean
public Queue deadLetterQueue() {
return new Queue(DEAD_LETTER_QUEUE);
}
/**
* 將死信隊(duì)列與私信交換機(jī)綁定通過(guò)路由幫訂單訂單隊(duì)列中
* @return
*/
@Bean
public Binding bindingDeadLetterQueue() {
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(ROUTING_KEY);
}
}
在創(chuàng)建一個(gè)死信隊(duì)列消息監(jiān)聽(tīng)器,用于判斷訂單是否超時(shí):
@Component
public class DelayedQueueListener {
@Autowired
private OrderMqService orderMqService;
@RabbitListener(queues = RabbitMQConfig.DEAD_LETTER_QUEUE)
public void handleDeadLetterOrder(String orderId) {
orderMqService.cancelOrder(orderId);
}
}
然后我們?cè)谟唵蝿?chuàng)建時(shí),將訂單信息發(fā)送到訂單MQ中,等消息的TTL到期之后,會(huì)自動(dòng)轉(zhuǎn)到死信隊(duì)列中。
@Service
public class OrderMqService {
private final AmqpTemplate rabbitTemplate;
@Autowired
public OrderMqService(AmqpTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void createOrder(String orderId) {
System.out.println("訂單"+orderId+"在"+ LocalDateTime.now() +"創(chuàng)建成功.");
rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_QUEUE, orderId);
}
public void cancelOrder(String orderId) {
// 在這里實(shí)現(xiàn)訂單取消的實(shí)際邏輯
System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消");
// 更新訂單狀態(tài)、釋放庫(kù)存等操作...
}
}
我們模擬創(chuàng)建訂單接口:
@RestController
@RequestMapping("orderMq")
public class MqOrderController {
@Autowired
private OrderMqService orderMqService;
@PostMapping("/create")
public String createOrder(String orderId) {
orderMqService.createOrder(orderId);
return "訂單創(chuàng)建成功:" + orderId;
}
}
圖片
可以看見(jiàn)訂單過(guò)了5秒之后開(kāi)始執(zhí)行取消。
使用死信隊(duì)列實(shí)現(xiàn)取消超時(shí)訂單的優(yōu)點(diǎn):
- 死信隊(duì)列可以捕獲并隔離那些在原始隊(duì)列中無(wú)法正常處理的消息,比如訂單超時(shí)未支付等情況。這樣有助于保障主業(yè)務(wù)流程不受影響,同時(shí)可以對(duì)異常情況進(jìn)行統(tǒng)一管理和處理。
- 通過(guò)設(shè)置消息TTL(Time-to-Live)或最大重試次數(shù)等條件,將無(wú)法正常處理的消息轉(zhuǎn)移到死信隊(duì)列,可以避免消息堆積導(dǎo)致的資源浪費(fèi),如內(nèi)存、磁盤(pán)空間等。
- 死信隊(duì)列可以作為訂單生命周期中特定階段的處理通道,如訂單超時(shí)后的處理流程,從而實(shí)現(xiàn)業(yè)務(wù)邏輯的清晰分離和模塊化。
- 所有的死信消息都被記錄在死信隊(duì)列中,方便跟蹤和分析訂單處理過(guò)程中出現(xiàn)的問(wèn)題,也有助于完善系統(tǒng)的監(jiān)控報(bào)警和數(shù)據(jù)分析。
- 死信隊(duì)列的處理過(guò)程也是異步進(jìn)行的,不影響主線程的執(zhí)行效率,增強(qiáng)系統(tǒng)的并發(fā)處理能力和響應(yīng)速度。
當(dāng)然他也有一些缺點(diǎn):
- 相較于專門(mén)的延遲隊(duì)列,死信隊(duì)列機(jī)制通常不會(huì)自動(dòng)將消息在特定時(shí)間后發(fā)出,需要通過(guò)設(shè)置消息TTL(過(guò)期時(shí)間)并在過(guò)期后觸發(fā)轉(zhuǎn)移至死信隊(duì)列。這種方式對(duì)于精確到秒級(jí)別的超時(shí)處理不夠友好,可能需要配合定時(shí)任務(wù)來(lái)檢查即將超時(shí)的訂單。
- 死信隊(duì)列的配置相對(duì)復(fù)雜,需要設(shè)置死信交換機(jī)、綁定關(guān)系以及消息TTL等,而且在處理死信時(shí)也需要額外的邏輯判斷。
- 如果沒(méi)有妥善處理死信隊(duì)列的消息,比如沒(méi)有監(jiān)聽(tīng)死信隊(duì)列或者處理邏輯存在缺陷,可能會(huì)導(dǎo)致部分死信消息未被正確處理。
- 在分布式環(huán)境下,如果訂單狀態(tài)不僅在消息隊(duì)列中維護(hù),還涉及到數(shù)據(jù)庫(kù)的更新,那么需要保證消息隊(duì)列與數(shù)據(jù)庫(kù)之間的事務(wù)一致性。
總結(jié)
訂單超時(shí)自動(dòng)取消是電商平臺(tái)中非常重要的功能之一,通過(guò)合適的技術(shù)方案,可以實(shí)現(xiàn)自動(dòng)化處理訂單超時(shí)的邏輯,提升用戶體驗(yàn)和系統(tǒng)效率。本文討論了多種實(shí)現(xiàn)訂單超時(shí)自動(dòng)取消的技術(shù)方案,包括定時(shí)輪詢、JDK的延遲隊(duì)列、時(shí)間輪算法、Redis實(shí)現(xiàn)以及MQ消息隊(duì)列中的延遲隊(duì)列和死信隊(duì)列。
- 定時(shí)輪詢:基于SpringBoot的Scheduled實(shí)現(xiàn),通過(guò)定時(shí)任務(wù)掃描數(shù)據(jù)庫(kù)中的訂單。優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單直接,但缺點(diǎn)是會(huì)給數(shù)據(jù)庫(kù)帶來(lái)持續(xù)壓力,處理效率受任務(wù)執(zhí)行間隔影響較大,且在高并發(fā)場(chǎng)景下可能引發(fā)并發(fā)問(wèn)題和資源浪費(fèi)。
- JDK的延遲隊(duì)列(DelayQueue):基于優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn),減少數(shù)據(jù)庫(kù)訪問(wèn),提供高效的任務(wù)處理。優(yōu)點(diǎn)是內(nèi)部數(shù)據(jù)結(jié)構(gòu)高效,線程安全。缺點(diǎn)是所有待處理訂單需保留在內(nèi)存中,可能導(dǎo)致內(nèi)存消耗大,且無(wú)持久化機(jī)制,系統(tǒng)崩潰時(shí)可能丟失數(shù)據(jù)。
- 時(shí)間輪算法:通過(guò)時(shí)間輪結(jié)構(gòu)實(shí)現(xiàn)定時(shí)任務(wù)調(diào)度,能高效處理大量定時(shí)任務(wù),提供精確的超時(shí)控制。優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,執(zhí)行效率高,且有成熟實(shí)現(xiàn)庫(kù)。缺點(diǎn)同樣是內(nèi)存占用和崩潰時(shí)數(shù)據(jù)丟失的問(wèn)題。
- Redis實(shí)現(xiàn):
有序集合(Sorted Set):利用有序集合的特性,定時(shí)輪詢查找已超時(shí)的任務(wù)。優(yōu)點(diǎn)是查詢效率高,適用于分布式環(huán)境,減少數(shù)據(jù)庫(kù)壓力。缺點(diǎn)是依賴定時(shí)任務(wù)執(zhí)行頻率,處理超時(shí)訂單的實(shí)時(shí)性受限,且在處理事務(wù)一致性方面需要額外努力。
Key過(guò)期監(jiān)聽(tīng):利用Redis鍵過(guò)期事件自動(dòng)觸發(fā)訂單取消邏輯。優(yōu)點(diǎn)是實(shí)時(shí)性好,資源消耗少,支持高并發(fā)。缺點(diǎn)是對(duì)Redis服務(wù)的依賴性強(qiáng),極端情況下處理能力可能成為瓶頸,且鍵過(guò)期有一定的不確定性。
- MQ消息隊(duì)列:
延遲隊(duì)列(如RabbitMQ的rabbitmq_delayed_message_exchange插件):實(shí)現(xiàn)消息在指定延遲后送達(dá)處理隊(duì)列。優(yōu)點(diǎn)是處理高效,異步執(zhí)行,易于擴(kuò)展,模塊化程度高。缺點(diǎn)是高度依賴消息隊(duì)列服務(wù),配置復(fù)雜度增加,可能涉及消息丟失或延遲風(fēng)險(xiǎn),以及消息隊(duì)列與數(shù)據(jù)庫(kù)操作一致性問(wèn)題。
死信隊(duì)列:通過(guò)設(shè)置隊(duì)列TTL將超時(shí)訂單轉(zhuǎn)為死信,由監(jiān)聽(tīng)死信隊(duì)列的消費(fèi)者處理。優(yōu)點(diǎn)是能捕獲并隔離異常消息,實(shí)現(xiàn)業(yè)務(wù)邏輯分離,資源保護(hù)良好,方便追蹤和分析問(wèn)題。缺點(diǎn)是相比延遲隊(duì)列,處理超時(shí)不夠精確,配置復(fù)雜,且同樣存在消息處理完整性及一致性問(wèn)題。
不同方案各有優(yōu)劣,實(shí)際應(yīng)用中應(yīng)根據(jù)系統(tǒng)的具體需求、資源狀況以及技術(shù)棧等因素綜合評(píng)估,選擇最適合的方案。在許多現(xiàn)代大型系統(tǒng)中,通常會(huì)選擇消息隊(duì)列的延遲隊(duì)列或死信隊(duì)列方案,以充分利用其異步處理、資源優(yōu)化和擴(kuò)展性方面的優(yōu)勢(shì)。