自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

美團(tuán)二面:如何設(shè)計(jì)一個(gè)訂單超時(shí)未支付關(guān)閉訂單的解決方案?

開(kāi)發(fā) 前端
定時(shí)輪訓(xùn)的方式都是基于定時(shí)定任務(wù)掃描訂單表,按照下單時(shí)間以及狀態(tài)進(jìn)行過(guò)濾,之后在進(jìn)行判斷是否在有效期內(nèi),如果不在,則取消訂單。

訂單超時(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):

  1. DelayQueue基于優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn),內(nèi)部使用了堆數(shù)據(jù)結(jié)構(gòu),插入和刪除操作的時(shí)間復(fù)雜度為O(log n),對(duì)于大量訂單的處理效率較高。
  2. 相比于定期查詢數(shù)據(jù)庫(kù)的方式,DelayQueue將待處理的訂單信息保留在內(nèi)存中,減少了對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)頻率,降低了IO壓力。
  3. DelayQueue是java.util.concurrent包下的工具類,本身就具備良好的線程安全特性,可以在多線程環(huán)境下穩(wěn)定工作。

但是因?yàn)镈elayQueue是基于內(nèi)存的,這也導(dǎo)致它在實(shí)現(xiàn)上有一定的缺點(diǎn):

  1. 所有待處理的訂單信息都需要保留在內(nèi)存中,對(duì)于大量訂單,可能會(huì)造成較大的內(nèi)存消耗。
  2. 由于所有的超時(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):

  1. 時(shí)間輪算法能夠高效地管理大量的定時(shí)任務(wù),其執(zhí)行時(shí)間與任務(wù)數(shù)量無(wú)關(guān),因此非常適合處理大規(guī)模的定時(shí)任務(wù)。
  2. 時(shí)間輪算法能夠提供相對(duì)精確的超時(shí)控制,可以在指定的時(shí)間后執(zhí)行任務(wù)或者取消任務(wù),從而確保超時(shí)訂單能夠及時(shí)取消。并且時(shí)間輪算法允許靈活地管理時(shí)間間隔和超時(shí)時(shí)間,可以根據(jù)具體業(yè)務(wù)需求進(jìn)行調(diào)整和優(yōu)化。
  3. 時(shí)間輪算法的實(shí)現(xiàn)相對(duì)簡(jiǎn)單,算法本身比較容易理解,且現(xiàn)有的實(shí)現(xiàn)庫(kù)如Netty的HashedWheelTimer已經(jīng)提供了成熟的實(shí)現(xiàn),因此可以很方便地集成到現(xiàn)有的系統(tǒng)中。
  4. 基于內(nèi)存操作,減少一些IO壓力。

但是相對(duì)應(yīng)的也存在一些缺點(diǎn):

  1. 時(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ì)影響延遲精度。
  2. 同時(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):

  1. 有序集合可以根據(jù)分?jǐn)?shù)(過(guò)期時(shí)間)快速定位到需要處理的超時(shí)訂單,避免了對(duì)全部訂單的全表掃描,提高了查詢效率。
  2. 在分布式環(huán)境中,Redis作為緩存和中間件,可以很容易地實(shí)現(xiàn)在多節(jié)點(diǎn)間共享超時(shí)訂單信息,有利于分布式系統(tǒng)中統(tǒng)一管理超時(shí)訂單。
  3. 利用Redis內(nèi)存數(shù)據(jù)結(jié)構(gòu),不需要頻繁讀寫(xiě)數(shù)據(jù)庫(kù),降低了數(shù)據(jù)庫(kù)的壓力,同時(shí)也節(jié)約了數(shù)據(jù)庫(kù)資源。

但是也有一些缺點(diǎn):

  1. 定時(shí)任務(wù)的執(zhí)行頻率決定了處理超時(shí)訂單的精確程度,頻率太低可能導(dǎo)致部分訂單未能及時(shí)取消,頻率太高則可能浪費(fèi)系統(tǒng)資源。
  2. 在涉及事務(wù)處理的情況下,可能需要額外的手段來(lái)保證與數(shù)據(jù)庫(kù)之間的數(shù)據(jù)一致性,防止因Redis處理超時(shí)訂單后,數(shù)據(jù)庫(kù)層面的更新失敗導(dǎo)致的數(shù)據(jù)不一致問(wèn)題。
  3. 在處理超時(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):

  1. Redis鍵過(guò)期事件能在鍵過(guò)期時(shí)立即觸發(fā)監(jiān)聽(tīng)器,因此可以在訂單超時(shí)的瞬間準(zhǔn)確執(zhí)行取消操作,大大提高了時(shí)效性。
  2. 相比定期輪詢數(shù)據(jù)庫(kù)查詢超時(shí)訂單的方式,Redis鍵過(guò)期事件是被動(dòng)觸發(fā),節(jié)省了CPU和網(wǎng)絡(luò)資源,減少了無(wú)效查詢。
  3. Redis的鍵過(guò)期事件處理機(jī)制天然支持高并發(fā)場(chǎng)景,只要Redis集群足夠強(qiáng)大,可以輕松處理大量訂單的過(guò)期處理。
  4. 資源占用小,只需要維護(hù)Redis中少量的鍵,相對(duì)于數(shù)據(jù)庫(kù)存儲(chǔ)所有訂單信息并做定時(shí)任務(wù)查詢,內(nèi)存和磁盤(pán)資源占用較少。

但是也存在一些缺點(diǎn):

  1. 整個(gè)方案依賴于Redis服務(wù)的穩(wěn)定性和性能,如果Redis服務(wù)出現(xiàn)問(wèn)題,可能會(huì)影響訂單超時(shí)處理。
  2. 在高并發(fā)場(chǎng)景下,Redis過(guò)期事件產(chǎn)生的速率可能非常高,如果處理不當(dāng),監(jiān)聽(tīng)器本身的處理能力可能成為瓶頸,導(dǎo)致消息堆積,這時(shí)需要考慮消息隊(duì)列或者其他緩沖機(jī)制。
  3. 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ì)列。

  1. 延遲隊(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

  1. 死信隊(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):

  1. 延遲隊(duì)列能夠在消息到達(dá)指定時(shí)間后立刻觸發(fā)處理,減少不必要的輪詢查詢,提高了處理效率和實(shí)時(shí)性。
  2. 訂單超時(shí)處理是異步進(jìn)行的,不會(huì)影響主線業(yè)務(wù)流程,有利于提升整體系統(tǒng)的響應(yīng)速度和穩(wěn)定性。
  3. 延遲隊(duì)列方案使得訂單創(chuàng)建、支付和超時(shí)取消三個(gè)環(huán)節(jié)相互獨(dú)立,有利于系統(tǒng)的模塊化和擴(kuò)展性
  4. 當(dāng)系統(tǒng)規(guī)模擴(kuò)大時(shí),可以通過(guò)增加消費(fèi)者數(shù)量來(lái)應(yīng)對(duì)更多的超時(shí)訂單處理,實(shí)現(xiàn)水平擴(kuò)展。

但是也有一些缺點(diǎn):

  1. 高度依賴消息隊(duì)列服務(wù)的可用性和穩(wěn)定性,一旦消息隊(duì)列出現(xiàn)故障,可能導(dǎo)致超時(shí)訂單無(wú)法正常處理。
  2. 延遲隊(duì)列方案涉及更多的中間件配置和管理,增加了系統(tǒng)的復(fù)雜性。
  3. 在分布式系統(tǒng)中,如果訂單狀態(tài)不僅在消息隊(duì)列中維護(hù),還要同步到數(shù)據(jù)庫(kù),需要額外保證消息隊(duì)列處理和數(shù)據(jù)庫(kù)操作的一致性。
  4. 雖然大部分消息隊(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):

  1. 死信隊(duì)列可以捕獲并隔離那些在原始隊(duì)列中無(wú)法正常處理的消息,比如訂單超時(shí)未支付等情況。這樣有助于保障主業(yè)務(wù)流程不受影響,同時(shí)可以對(duì)異常情況進(jìn)行統(tǒng)一管理和處理。
  2. 通過(guò)設(shè)置消息TTL(Time-to-Live)或最大重試次數(shù)等條件,將無(wú)法正常處理的消息轉(zhuǎn)移到死信隊(duì)列,可以避免消息堆積導(dǎo)致的資源浪費(fèi),如內(nèi)存、磁盤(pán)空間等。
  3. 死信隊(duì)列可以作為訂單生命周期中特定階段的處理通道,如訂單超時(shí)后的處理流程,從而實(shí)現(xiàn)業(yè)務(wù)邏輯的清晰分離和模塊化。
  4. 所有的死信消息都被記錄在死信隊(duì)列中,方便跟蹤和分析訂單處理過(guò)程中出現(xiàn)的問(wèn)題,也有助于完善系統(tǒng)的監(jiān)控報(bào)警和數(shù)據(jù)分析。
  5. 死信隊(duì)列的處理過(guò)程也是異步進(jìn)行的,不影響主線程的執(zhí)行效率,增強(qiáng)系統(tǒng)的并發(fā)處理能力和響應(yīng)速度。

當(dāng)然他也有一些缺點(diǎn):

  1. 相較于專門(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í)的訂單。
  2. 死信隊(duì)列的配置相對(duì)復(fù)雜,需要設(shè)置死信交換機(jī)、綁定關(guān)系以及消息TTL等,而且在處理死信時(shí)也需要額外的邏輯判斷。
  3. 如果沒(méi)有妥善處理死信隊(duì)列的消息,比如沒(méi)有監(jiān)聽(tīng)死信隊(duì)列或者處理邏輯存在缺陷,可能會(huì)導(dǎo)致部分死信消息未被正確處理。
  4. 在分布式環(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ì)列。

  1. 定時(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)。
  2. 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ù)。
  3. 時(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)題。
  4. 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ò)期有一定的不確定性。

  1. 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ì)。

責(zé)任編輯:武曉燕 來(lái)源: 碼農(nóng)Academy
相關(guān)推薦

2021-09-07 08:14:26

訂單超時(shí)未支付

2024-02-26 08:50:37

訂單自動(dòng)取消消息

2024-10-14 12:05:56

2017-06-01 10:52:35

互聯(lián)網(wǎng)

2024-09-12 15:43:46

C#代碼后端

2015-09-28 13:00:45

美團(tuán)云SDN解決方案華為

2024-10-31 08:50:14

2024-06-20 07:59:49

2024-09-04 08:55:56

2010-09-06 11:17:28

Dynamics微軟訂單管理

2009-03-17 09:27:52

ITSMITIL解決方案

2022-03-02 15:14:09

訂單計(jì)時(shí)器持久化

2024-08-08 08:09:38

2023-01-30 08:12:53

訂單超時(shí)自動(dòng)取消延長(zhǎng)訂單

2021-12-26 18:24:51

MySQL InnoDB引擎

2021-04-20 08:00:31

Redisson關(guān)閉訂單支付系統(tǒng)

2019-08-23 08:09:18

訂單號(hào)生成數(shù)據(jù)庫(kù)ID

2023-03-06 07:35:30

狀態(tài)機(jī)工具訂單狀態(tài)

2024-12-20 08:20:46

2020-10-21 09:25:01

互聯(lián)網(wǎng)訂單自動(dòng)關(guān)閉
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)