偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

訂單超時未支付自動取消是一個典型的電商和在線交易業(yè)務(wù)場景,在該場景下,用戶在購物平臺上下單后,系統(tǒng)通常會為用戶提供一段有限的時間來完成支付。如果用戶在這個指定的時間窗口內(nèi)沒有成功完成支付,系統(tǒng)將自動執(zhí)行訂單取消操作。

當(dāng)然類似的業(yè)務(wù)場景還有:

  • 我們預(yù)約釘釘會議后,釘釘會在會議開始前15分鐘、5分鐘提醒。
  • 淘寶收到貨物簽收之后,超過7天沒有確認收貨,會自動確認收貨。
  • 未使用的優(yōu)惠券有效期結(jié)束后,自動將優(yōu)惠券狀態(tài)更新為已過期。
  • 用戶登錄失敗次數(shù)過多后,賬號鎖定一段時間,利用延遲隊列在鎖定期滿后自動解鎖賬號。而針對這種業(yè)務(wù)需求,我們常見的兩中技術(shù)方向即:定時輪訓(xùn)訂單之后判斷是否取消以及延遲隊列實現(xiàn)。而到具體的技術(shù)方案主要有以下幾種:

圖片圖片

本文主要介紹以下幾種主流方案。

定時輪訓(xùn)(SpringBoot的Scheduled實現(xiàn))

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

如以下,我們使用SpringBoot中的定時任務(wù)實現(xiàn):

我們先創(chuàng)建定時任務(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是實現(xiàn)了Runnable接口的對象)
        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è)置終止等待時間
        return scheduler;
    }
}

然后在MyTaskExecutor中實現(xiàn)掃描訂單以及判斷訂單是否需要取消:

public class MyTaskExecutor implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 在 "+ LocalDateTime.now() +" 執(zhí)行MyTaskExecutor。。。。。");
    }
}

運行結(jié)果如下:

圖片圖片

采用定時任務(wù)機制來實現(xiàn)實時監(jiān)測并取消超時訂單的方法相對直接易行,我們可以運用諸如Quartz、XXL-Job或Elastic-Job等成熟的定時任務(wù)框架進行集群部署,從而提升任務(wù)執(zhí)行效能。然而,此類方案存在顯著局限:

首先,定時輪詢訂單表的方式在訂單數(shù)量龐大的情況下會對數(shù)據(jù)庫帶來持續(xù)且顯著的壓力,因為頻繁地全表掃描無疑會增加I/O負擔(dān)和CPU使用率。

其次,定時任務(wù)執(zhí)行的間隔設(shè)定頗為棘手。若設(shè)定的間隔時間較長,可能會導(dǎo)致訂單超時后的取消動作出現(xiàn)延遲,影響用戶體驗;相反,若時間間隔設(shè)置得過短,則會導(dǎo)致大量訂單被重復(fù)掃描和判斷,不僅浪費計算資源,還可能導(dǎo)致不必要的并發(fā)問題和事務(wù)沖突,尤其是在高并發(fā)交易的高峰期。

在實際應(yīng)用中,針對大流量訂單場景下的超時處理,往往更傾向于采用延遲隊列技術(shù)而非簡單的定時任務(wù)輪詢,以實現(xiàn)更為精確、高效的超時邏輯處理。

關(guān)于SpringBoot的定時任務(wù)實現(xiàn)的幾種方式,請參考:玩轉(zhuǎn)SpringBoot:SpringBoot的幾種定時任務(wù)實現(xiàn)方式

JDK的延遲隊列

使用JDK自帶的DelayQueue實現(xiàn)一個延遲隊列并處理超時訂單,首先我們需要定義一個實現(xiàn)了Delayed接口的訂單對象類,然后創(chuàng)建DelayQueue實例并不斷從隊列中取出已超時的訂單進行處理。

我們定義一個包含訂單信息和延遲時間的訂單類:

@Getter
public class DelayedOrder implements Delayed {

    private final String orderNo;
    private final long expireTimeMillis; // 訂單超時時間戳(毫秒)

    public DelayedOrder(String orderNo, long delayInSeconds) {
        this.orderNo = orderNo;
        // 設(shè)置訂單在當(dāng)前時間多少秒后超時
        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)用實際的服務(wù)接口或方法取消訂單
    }
}

然后我們就可以使用DelayQueue處理超時訂單:

@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()+" 添加到延遲隊列");
    }

    // 啟動訂單處理線程池
    @Autowired
    private ExecutorService executorService;

    @PostConstruct
    public void init() {
        executorService.execute(this::processOrders);
    }

    private void processOrders() {
        while (true) {
            try {
                DelayedOrder order = delayQueue.take(); // 從延遲隊列中取出已經(jīng)過期的訂單
                System.out.println("訂單 " + order.getOrderNo() + "在 "+ LocalDateTime.now() +" 取消");
                order.cancelOrder();
                // 在這里執(zhí)行取消訂單的邏輯,比如更新數(shù)據(jù)庫狀態(tài)等
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我們實現(xiàn)一個創(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);
    }
}

請求接口,發(fā)現(xiàn)訂單超過各自的時間之后,都超時了。當(dāng)然真實場景是超時時間一致,只是訂單創(chuàng)建時間不一致。

圖片圖片

基于JDK的DelayQueue實現(xiàn)的延遲隊列解決取消超時訂單的方案,相比較于定時輪訓(xùn)有如下優(yōu)點:

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

但是因為DelayQueue是基于內(nèi)存的,這也導(dǎo)致它在實現(xiàn)上有一定的缺點:

  1. 所有待處理的訂單信息都需要保留在內(nèi)存中,對于大量訂單,可能會造成較大的內(nèi)存消耗。
  2. 由于所有的超時信息都依賴于內(nèi)存中的隊列,如果系統(tǒng)崩潰或重啟,未處理的訂單信息可能丟失,除非有額外的持久化措施。

時間輪算法

在介紹時間輪算法實現(xiàn)取消超時訂單功能之前,我們先來看一下什么是時間輪算法?

時間輪算法(Time Wheel Algorithm)是一種高效處理定時任務(wù)調(diào)度的機制,廣泛應(yīng)用于各類系統(tǒng)如計時器、調(diào)度器等組件。該算法的關(guān)鍵理念在于將時間維度映射至物理空間,即構(gòu)建一個由多個時間槽構(gòu)成的循環(huán)結(jié)構(gòu),每個槽代表一個固定的時間單位(如毫秒、秒等)。

時間輪實質(zhì)上是一個具有多個槽位的環(huán)形數(shù)據(jù)結(jié)構(gòu),隨著時間的推進,時間輪上的指針按照預(yù)先設(shè)定的速度(例如每秒前進一槽)順時針旋轉(zhuǎn)。每當(dāng)指針移動至下一槽位時,系統(tǒng)會檢視該槽位中掛載的所有定時任務(wù),并逐一執(zhí)行到期的任務(wù)。

在時間輪中,每個待執(zhí)行任務(wù)均與其觸發(fā)時間點對應(yīng)的時間槽關(guān)聯(lián)。添加新任務(wù)時,系統(tǒng)會根據(jù)任務(wù)的期望執(zhí)行時間計算出相應(yīng)的槽位編號,并將任務(wù)插入該槽。對于未來執(zhí)行的任務(wù),計算所需等待的槽位數(shù)目,確保任務(wù)按時被處理。值得注意的是,時間輪設(shè)計為循環(huán)結(jié)構(gòu),意味著當(dāng)指針到達最后一個槽位后會自動返回至第一個槽位,形成連續(xù)不斷的循環(huán)調(diào)度。

借助時間輪算法,定時任務(wù)的執(zhí)行時間以相對固定的時間槽來表示,而非直接依賴于絕對時間。任務(wù)執(zhí)行完畢后,系統(tǒng)會及時將其從時間輪中移除,同時,對于不再需要執(zhí)行的任務(wù),也可以在任何時候予以移除,確保整個調(diào)度系統(tǒng)的高效運作和實時響應(yīng)。

圖片圖片

如上圖為例,假設(shè)一個格子是1秒,則整個wheel能表示的時間段為8s,假設(shè)當(dāng)前指針指向2,此時需要調(diào)度一個3s后執(zhí)行的任務(wù), 顯然應(yīng)該加入到(2+3=5)的方格中,指針再走3次就可以執(zhí)行了;如果任務(wù)要在10s后執(zhí)行,應(yīng)該等指針走完一個round零2格再執(zhí)行, 因此應(yīng)放入4,同時將round(1)保存到任務(wù)中。檢查到期任務(wù)應(yīng)當(dāng)只執(zhí)行round為0的,格子上其他任務(wù)的round應(yīng)減1.

所以,我們可以使用時間輪算法去試一下延遲任務(wù),用于實現(xiàn)取消超時訂單。

我們以Netty4為例,引入依賴:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.68.Final</version>
</dependency>

然后定義訂單處理服務(wù),在創(chuàng)建訂單時定義訂單超時時間,以及超時時取消訂單。

@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è)置超時時間為5秒鐘
        Timeout timeout = timer.newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                // 超時處理邏輯,取消訂單
                cancelOrder(orderId);
            }
        }, Duration.ofSeconds(5).toMillis(), TimeUnit.MILLISECONDS);
        orderTimeouts.put(orderId, timeout);
    }

    public void cancelOrder(String orderId) {
        // 取消訂單的邏輯
        orderTimeouts.remove(orderId);
        System.out.println(orderId+"訂單超時,在"+ 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;
    }
}

我們分別請求接口,創(chuàng)建訂單:

圖片圖片

可以看見,訂單在5秒鐘之后自動調(diào)用取消方法取消訂單。

基于時間輪實現(xiàn)延遲任務(wù)來取消超時訂單有如下優(yōu)點:

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

但是相對應(yīng)的也存在一些缺點:

  1. 時間輪算法需要維護一個槽的數(shù)據(jù)結(jié)構(gòu),因此會占用一定的內(nèi)存和計算資源,對于一些資源受限的環(huán)境可能會存在一定的壓力。同DelayQueue,在大量訂單時會對內(nèi)存造成較大的內(nèi)存消耗。同時也會影響延遲精度。
  2. 同時,如果系統(tǒng)崩潰或者重啟,未處理的訂單信息可能丟失,除非有額外的持久化措施。

Redis實現(xiàn)

對于Redis實現(xiàn)延遲任務(wù),常見的兩種方案是使用有序集合(Sorted Set,通常簡稱為zset)和使用key過期監(jiān)聽。

定時輪訓(xùn)有序集合

利用有序集合的特性,即集合中的元素是有序的,每個元素都有一個分數(shù)(score)。在延遲任務(wù)的場景中,可以將任務(wù)的執(zhí)行時間作為分數(shù),將任務(wù)的唯一標(biāo)識(如任務(wù)ID)作為集合中的元素。然后,定時輪詢有序集合,查找分數(shù)小于當(dāng)前時間的元素,這些元素即為已經(jīng)到期需要執(zhí)行的任務(wù)。執(zhí)行完任務(wù)后,可以從有序集合中刪除對應(yīng)的元素。因此可以將訂單的過期時間作為score,用于實現(xiàn)取消超時訂單。

引入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)建以及自動取消服務(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è)訂單超時時間為5秒
        long expirationTime = 5 * 1000 + System.currentTimeMillis();
        redisTemplate.opsForZSet().add(ORDER_TIMEOUT_SET_KEY, orderId, expirationTime);
    }

    @Scheduled(fixedRate = 1000) // 每秒檢查一次,實際頻率根據(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) {
                // 處理超時訂單
                cancelOrder(orderId);
                // 從有序集合中移除已處理的超時訂單
                redisTemplate.opsForZSet().remove(ORDER_TIMEOUT_SET_KEY, orderId);
            }
        }
    }

    private void cancelOrder(String orderId) {
        // 在這里實現(xiàn)訂單取消的實際邏輯
        System.out.println("訂單 " + orderId + " 在" + LocalDateTime.now() +"取消");
        // 更新訂單狀態(tài)、釋放庫存等操作...
    }
}

注意:因本例中基于@Scheduled實現(xiàn)定時輪訓(xùn),所以需要使用@EnableScheduling開啟Scheduled功能。具體請參考:玩轉(zhuǎn)SpringBoot:SpringBoot的幾種定時任務(wù)實現(xiàn)方式

我們定義訂單創(chuàng)建接口,模擬訂單創(chuàng)建:

圖片圖片

可以看到訂單5秒鐘后自動取消。

使用Redis有序集合實現(xiàn)取消超時訂單有一些優(yōu)點:

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

但是也有一些缺點:

  1. 定時任務(wù)的執(zhí)行頻率決定了處理超時訂單的精確程度,頻率太低可能導(dǎo)致部分訂單未能及時取消,頻率太高則可能浪費系統(tǒng)資源。
  2. 在涉及事務(wù)處理的情況下,可能需要額外的手段來保證與數(shù)據(jù)庫之間的數(shù)據(jù)一致性,防止因Redis處理超時訂單后,數(shù)據(jù)庫層面的更新失敗導(dǎo)致的數(shù)據(jù)不一致問題。
  3. 在處理超時訂單過程中,若出現(xiàn)異常,需要配套的重試機制。

使用Redis key過期監(jiān)聽

利用Redis的key過期監(jiān)聽功能。當(dāng)設(shè)置一個key的過期時間時,可以設(shè)置一個回調(diào)函數(shù),當(dāng)key過期時,Redis會自動調(diào)用這個回調(diào)函數(shù)。即利用Redis的Keyspace Notifications功能,當(dāng)一個鍵(Key)過期時,Redis會向已訂閱了相關(guān)頻道的客戶端發(fā)送一個通知。

使用Redis的key的過期監(jiān)聽功能之前我們需要啟用Redis Keyspace Notifications,在Redis配置文件(redis.conf)中啟用Key Space Notifications,即打開如下配置:

notify-keyspace-events Ex

notify-keyspace-events設(shè)置為Ex,表示啟用所有類型的鍵空間通知,包括過期事件。具體配置方法可能因Redis的版本和環(huán)境而有所不同,請根據(jù)實際情況進行配置。

然后我們就可以使用代碼實現(xiàn),首先實現(xiàn)MessageListener接口實現(xiàn)一個監(jiān)聽器來監(jiān)聽Redis的key過期事件。當(dāng)訂單的key過期時,將觸發(fā)監(jiān)聽器中的邏輯,執(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過期事件監(jiān)聽器,并將其注冊到Redis連接工廠中。這樣,監(jiān)聽器將會在Redis的key過期事件發(fā)生時被調(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)聽所有數(shù)據(jù)庫的key過期事件
        return container;
    }
}

__keyevent@0__:expired是Redis的系統(tǒng)通道,用于監(jiān)聽所有數(shù)據(jù)庫中的key過期事件。如果需要監(jiān)聽特定數(shù)據(jù)庫的key過期事件,則可以修改對應(yīng)的數(shù)據(jù)庫號。例如,__keyevent@1__:expired表示監(jiān)聽第一個數(shù)據(jù)庫的key過期事件。

然后我們就可以實現(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è)訂單超時時間為5秒
        long expirationTime = 5;
        redisTemplate.opsForValue().set(orderId, "orderData", expirationTime, TimeUnit.SECONDS);
    }

    public void cancelOrder(String orderId) {
        // 在這里實現(xiàn)訂單取消的實際邏輯
        System.out.println("訂單 " + orderId + " 在" + LocalDateTime.now() +"取消");
        // 更新訂單狀態(tài)、釋放庫存等操作...
    }
}
@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個訂單,模擬5秒鐘后的訂單取消

圖片圖片

使用Redis的key過期監(jiān)聽事件,實現(xiàn)取消超時訂單有以下優(yōu)點:

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

但是也存在一些缺點:

  1. 整個方案依賴于Redis服務(wù)的穩(wěn)定性和性能,如果Redis服務(wù)出現(xiàn)問題,可能會影響訂單超時處理。
  2. 在高并發(fā)場景下,Redis過期事件產(chǎn)生的速率可能非常高,如果處理不當(dāng),監(jiān)聽器本身的處理能力可能成為瓶頸,導(dǎo)致消息堆積,這時需要考慮消息隊列或者其他緩沖機制。
  3. Redis的鍵過期并不是嚴格意義上的實時,而是基于定期檢查機制,極端情況下可能存在一定的延遲。盡管在實踐中這種延遲很小,但對于極高精度要求的場景,可能需要額外關(guān)注。

MQ消息隊列

使用消息隊列實現(xiàn)取消超時訂單的常見方法是利用延遲隊列以及死信隊列。比如RabbitMq,在介紹實現(xiàn)方式之前,我們先來了解一下RabbitMq的延遲隊列以及死信隊列。

  1. 延遲隊列:RabbitMQ本身并不直接支持延遲隊列,但可以通過安裝rabbitmq_delayed_message_exchange插件來實現(xiàn)延遲消息的功能。當(dāng)啟用這個插件后,你可以創(chuàng)建一個類型為x-delayed-message的交換機。在發(fā)送消息時,可以設(shè)置消息頭中的x-delay字段,表示消息應(yīng)該在多久之后才開始被路由到綁定的目標(biāo)隊列。這樣,當(dāng)一個訂單創(chuàng)建時,可以將包含訂單ID和過期時間的消息發(fā)送到延遲交換機,并設(shè)置相應(yīng)的延遲時間。當(dāng)延遲時間結(jié)束時,消息將被發(fā)送到處理超時訂單的隊列,隨后由消費者進行訂單狀態(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

然后我們進入容器后啟動插件:

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

然后驗證一下插件是否開啟成功:

rabbitmq-plugins list | grep delayed

圖片圖片

我的rabbitmq的版本是3.13.0

  1. 死信隊列死信隊列是指當(dāng)消息在原始隊列中遇到某種情況(如消息過期、消息被拒絕等)時,會被重新路由到另一個預(yù)定義的隊列中。當(dāng)消息在隊列中停留的時間超過TTL,該消息就會變成死信,并根據(jù)隊列配置轉(zhuǎn)發(fā)到死信隊列。

基于RabbitMq的延遲隊列

延遲隊列可以直接處理延遲消息,即消息在指定的延遲時間過后才被投遞給消費者。在超時取消訂單的場景中,訂單創(chuàng)建時將訂單信息封裝成消息,并設(shè)置消息的延遲時間,當(dāng)訂單超時時,消息自動被投遞到處理超時訂單的隊列,消費者接收到消息后執(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

配置延遲交換機,并且初始化延遲交換機、隊列及綁定關(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();
    }
}

這里交換機exchange,需要我們事先在rabbitmq中創(chuàng)建好,訪問http://localhost:15672/在Exchanges中,添加Exchange,設(shè)置type= x-delayed-message,如圖:

圖片圖片

在定義一個監(jiān)聽rabbitmq消息的監(jiān)聽器,當(dāng)消息延遲時間到了之后,就會被該監(jiān)聽器見聽到,在這里判斷訂單是否已經(jīng)被支付,如果沒有支付則取消。

@Component
public class DelayedQueueListener {

    @Autowired
    private OrderMqService orderMqService;

    @RabbitListener(queues = RabbitConfig.ORDER_QUEUE)
    public void handleDelayedOrder(String orderId) {
        orderMqService.cancelOrder(orderId);
    }   
}

然后我們在訂單創(chuàng)建時,將訂單信息發(fā)送到MQ中,等延遲時間到了之后,如果訂單還沒有支付,則執(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) {
        // 在這里實現(xiàn)訂單取消的實際邏輯
        System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消");
        // 更新訂單狀態(tài)、釋放庫存等操作...
    }
}

我們模擬創(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;
    }
}

圖片圖片

可以看見訂單過了5秒之后開始執(zhí)行取消。

使用延遲隊列方案來實現(xiàn)訂單超時取消等場景的優(yōu)點:

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

但是也有一些缺點:

  1. 高度依賴消息隊列服務(wù)的可用性和穩(wěn)定性,一旦消息隊列出現(xiàn)故障,可能導(dǎo)致超時訂單無法正常處理。
  2. 延遲隊列方案涉及更多的中間件配置和管理,增加了系統(tǒng)的復(fù)雜性。
  3. 在分布式系統(tǒng)中,如果訂單狀態(tài)不僅在消息隊列中維護,還要同步到數(shù)據(jù)庫,需要額外保證消息隊列處理和數(shù)據(jù)庫操作的一致性。
  4. 雖然大部分消息隊列的延遲機制相當(dāng)可靠,但仍有極小概率出現(xiàn)消息延遲到達或丟失的情況,需要有相應(yīng)的容錯和補償機制。

對于延遲隊列,并非只有rabbitmq才有,RocketMQ也有延遲隊列。在RocketMQ中,延遲消息的發(fā)送是通過設(shè)置消息的延遲級別來實現(xiàn)的。每個延遲級別都對應(yīng)著一個具體的延遲時間,例如 1 表示 1 秒、2 表示 5 秒、3 表示 10 秒,以此類推。用戶可以根據(jù)自己的需求選擇合適的延遲級別。但是也可以看出他并沒有支持的那么精確,如果想要精確的就必須使用RocketMQ的企業(yè)版,在企業(yè)版中可以自定義設(shè)置延遲時間。這里就不過多講解,有興趣的可以自己研究一下。

基于RabbitMq的死信隊列實現(xiàn)

訂單創(chuàng)建時,將訂單信息發(fā)送到一個具有TTL的隊列,當(dāng)消息在隊列中停留的時間超過了TTL(也就是訂單的有效支付期限),消息就會變?yōu)樗佬?。然后再配置隊列,使得這些過期的死信消息被路由到一個預(yù)先設(shè)置好的死信隊列。最后創(chuàng)建一個消費者監(jiān)聽這個死信隊列,一旦有消息進來(即訂單超時),消費者便處理這些死信,檢查訂單狀態(tài)并執(zhí)行取消操作。

使用的rabbitmq依賴以及配置同上使用延遲隊列方案。我們來看一下創(chuàng)建處理訂單即帶有TTL的隊列:

@Configuration
public class RabbitMQConfig {

    /**訂單隊列*/
    public static final String ORDER_QUEUE = "order.queue";
    /**死信隊列交換機*/
    public static final String DEAD_LETTER_EXCHANGE = "order.deadLetter.exchange";
    /**死信隊列*/
    public static final String DEAD_LETTER_QUEUE = "order.deadLetter.queue";
    /**死信路由*/
    public static final String ROUTING_KEY = "delayed-routing-key";

    /**
     * 創(chuàng)建訂單隊列
     * @return
     */
    @Bean
    public Queue orderQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 5000L); // 設(shè)置訂單隊列消息有效期為30秒(可以根據(jù)實際情況調(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)建交換機:

圖片圖片

創(chuàng)建訂單業(yè)務(wù)類,將訂單發(fā)送到訂單消息隊列:

@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) {
        // 在這里實現(xiàn)訂單取消的實際邏輯
        System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消");
        // 更新訂單狀態(tài)、釋放庫存等操作...
    }

}

在創(chuàng)建死信隊列,私信隊列交換機,通過訂單隊列路由將私信隊列綁定到訂單訂單隊列中:

@Configuration
public class RabbitMQConfig {

    /**訂單隊列*/
    public static final String ORDER_QUEUE = "order.queue";
    /**死信隊列交換機*/
    public static final String DEAD_LETTER_EXCHANGE = "order.deadLetter.exchange";
    /**死信隊列*/
    public static final String DEAD_LETTER_QUEUE = "order.deadLetter.queue";
    /**死信路由*/
    public static final String ROUTING_KEY = "delayed-routing-key";

    /**
     * 創(chuàng)建死信隊列交換機
     * @return
     */
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }

    /**
     * 創(chuàng)建死信隊列
     * @return
     */
    @Bean
    public Queue deadLetterQueue() {
        return new Queue(DEAD_LETTER_QUEUE);
    }

    /**
     * 將死信隊列與私信交換機綁定通過路由幫訂單訂單隊列中
     * @return
     */
    @Bean
    public Binding bindingDeadLetterQueue() {
        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(ROUTING_KEY);
    }
}

在創(chuàng)建一個死信隊列消息監(jiān)聽器,用于判斷訂單是否超時:

@Component
public class DelayedQueueListener {

    @Autowired
    private OrderMqService orderMqService;

    @RabbitListener(queues = RabbitMQConfig.DEAD_LETTER_QUEUE)
    public void handleDeadLetterOrder(String orderId) {
        orderMqService.cancelOrder(orderId);
    }
}

然后我們在訂單創(chuàng)建時,將訂單信息發(fā)送到訂單MQ中,等消息的TTL到期之后,會自動轉(zhuǎn)到死信隊列中。

@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) {
        // 在這里實現(xiàn)訂單取消的實際邏輯
        System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消");
        // 更新訂單狀態(tài)、釋放庫存等操作...
    }

}

我們模擬創(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;
    }
}

圖片圖片

可以看見訂單過了5秒之后開始執(zhí)行取消。

使用死信隊列實現(xiàn)取消超時訂單的優(yōu)點:

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

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

  1. 相較于專門的延遲隊列,死信隊列機制通常不會自動將消息在特定時間后發(fā)出,需要通過設(shè)置消息TTL(過期時間)并在過期后觸發(fā)轉(zhuǎn)移至死信隊列。這種方式對于精確到秒級別的超時處理不夠友好,可能需要配合定時任務(wù)來檢查即將超時的訂單。
  2. 死信隊列的配置相對復(fù)雜,需要設(shè)置死信交換機、綁定關(guān)系以及消息TTL等,而且在處理死信時也需要額外的邏輯判斷。
  3. 如果沒有妥善處理死信隊列的消息,比如沒有監(jiān)聽死信隊列或者處理邏輯存在缺陷,可能會導(dǎo)致部分死信消息未被正確處理。
  4. 在分布式環(huán)境下,如果訂單狀態(tài)不僅在消息隊列中維護,還涉及到數(shù)據(jù)庫的更新,那么需要保證消息隊列與數(shù)據(jù)庫之間的事務(wù)一致性。

總結(jié)

訂單超時自動取消是電商平臺中非常重要的功能之一,通過合適的技術(shù)方案,可以實現(xiàn)自動化處理訂單超時的邏輯,提升用戶體驗和系統(tǒng)效率。本文討論了多種實現(xiàn)訂單超時自動取消的技術(shù)方案,包括定時輪詢、JDK的延遲隊列、時間輪算法、Redis實現(xiàn)以及MQ消息隊列中的延遲隊列和死信隊列。

  1. 定時輪詢:基于SpringBoot的Scheduled實現(xiàn),通過定時任務(wù)掃描數(shù)據(jù)庫中的訂單。優(yōu)點是實現(xiàn)簡單直接,但缺點是會給數(shù)據(jù)庫帶來持續(xù)壓力,處理效率受任務(wù)執(zhí)行間隔影響較大,且在高并發(fā)場景下可能引發(fā)并發(fā)問題和資源浪費。
  2. JDK的延遲隊列(DelayQueue):基于優(yōu)先級隊列實現(xiàn),減少數(shù)據(jù)庫訪問,提供高效的任務(wù)處理。優(yōu)點是內(nèi)部數(shù)據(jù)結(jié)構(gòu)高效,線程安全。缺點是所有待處理訂單需保留在內(nèi)存中,可能導(dǎo)致內(nèi)存消耗大,且無持久化機制,系統(tǒng)崩潰時可能丟失數(shù)據(jù)。
  3. 時間輪算法:通過時間輪結(jié)構(gòu)實現(xiàn)定時任務(wù)調(diào)度,能高效處理大量定時任務(wù),提供精確的超時控制。優(yōu)點是實現(xiàn)簡單,執(zhí)行效率高,且有成熟實現(xiàn)庫。缺點同樣是內(nèi)存占用和崩潰時數(shù)據(jù)丟失的問題。
  4. Redis實現(xiàn):

有序集合(Sorted Set):利用有序集合的特性,定時輪詢查找已超時的任務(wù)。優(yōu)點是查詢效率高,適用于分布式環(huán)境,減少數(shù)據(jù)庫壓力。缺點是依賴定時任務(wù)執(zhí)行頻率,處理超時訂單的實時性受限,且在處理事務(wù)一致性方面需要額外努力。

Key過期監(jiān)聽:利用Redis鍵過期事件自動觸發(fā)訂單取消邏輯。優(yōu)點是實時性好,資源消耗少,支持高并發(fā)。缺點是對Redis服務(wù)的依賴性強,極端情況下處理能力可能成為瓶頸,且鍵過期有一定的不確定性。

  1. MQ消息隊列:

延遲隊列(如RabbitMQ的rabbitmq_delayed_message_exchange插件):實現(xiàn)消息在指定延遲后送達處理隊列。優(yōu)點是處理高效,異步執(zhí)行,易于擴展,模塊化程度高。缺點是高度依賴消息隊列服務(wù),配置復(fù)雜度增加,可能涉及消息丟失或延遲風(fēng)險,以及消息隊列與數(shù)據(jù)庫操作一致性問題。

死信隊列:通過設(shè)置隊列TTL將超時訂單轉(zhuǎn)為死信,由監(jiān)聽死信隊列的消費者處理。優(yōu)點是能捕獲并隔離異常消息,實現(xiàn)業(yè)務(wù)邏輯分離,資源保護良好,方便追蹤和分析問題。缺點是相比延遲隊列,處理超時不夠精確,配置復(fù)雜,且同樣存在消息處理完整性及一致性問題。

不同方案各有優(yōu)劣,實際應(yīng)用中應(yīng)根據(jù)系統(tǒng)的具體需求、資源狀況以及技術(shù)棧等因素綜合評估,選擇最適合的方案。在許多現(xiàn)代大型系統(tǒng)中,通常會選擇消息隊列的延遲隊列或死信隊列方案,以充分利用其異步處理、資源優(yōu)化和擴展性方面的優(yōu)勢。

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

2021-09-07 08:14:26

訂單超時未支付

2024-02-26 08:50:37

訂單自動取消消息

2024-10-14 12:05:56

2024-09-12 15:43:46

C#代碼后端

2017-06-01 10:52:35

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

2015-09-28 13:00:45

美團云SDN解決方案華為

2024-10-31 08:50:14

2009-03-17 09:27:52

ITSMITIL解決方案

2024-06-20 07:59:49

2024-09-04 08:55:56

2010-09-06 11:17:28

Dynamics微軟訂單管理

2025-05-09 09:31:31

支付訂單Timer

2019-08-23 08:09:18

訂單號生成數(shù)據(jù)庫ID

2021-12-26 18:24:51

MySQL InnoDB引擎

2022-03-02 15:14:09

訂單計時器持久化

2023-01-30 08:12:53

訂單超時自動取消延長訂單

2024-08-08 08:09:38

2023-03-06 07:35:30

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

2021-04-20 08:00:31

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

2025-03-25 12:00:00

@Value?Spring開發(fā)
點贊
收藏

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