訂單超時未支付自動取消是一個典型的電商和在線交易業務場景,在該場景下,用戶在購物平臺上下單后,系統通常會為用戶提供一段有限的時間來完成支付。如果用戶在這個指定的時間窗口內沒有成功完成支付,系統將自動執行訂單取消操作。
當然類似的業務場景還有:
圖片
本文主要介紹以下幾種主流方案。
定時輪訓的方式都是基于定時定任務掃描訂單表,按照下單時間以及狀態進行過濾,之后在進行判斷是否在有效期內,如果不在,則取消訂單。
如以下,我們使用SpringBoot中的定時任務實現:
我們先創建定時任務的配置,設置任務每隔5秒執行一次。
@Configuration@EnableSchedulingpublic class CustomSchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler threadPoolTaskScheduler = threadPoolTaskScheduler(); taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); // 設置自定義的TaskScheduler // 根據任務信息創建CronTrigger CronTrigger cronTrigger = new CronTrigger("0/5 * * * * ?"); // 創建任務執行器(假設TaskExecutor是實現了Runnable接口的對象) MyTaskExecutor taskExecutor = new MyTaskExecutor(); // 使用自定義的TaskScheduler調度任務 threadPoolTaskScheduler.schedule(taskExecutor, cronTrigger); } @Bean(destroyMethod = "shutdown") public ThreadPoolTaskScheduler threadPoolTaskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); // 設置線程池大小 scheduler.setThreadNamePrefix("scheduled-task-"); // 設置線程名稱前綴 scheduler.setAwaitTerminationSeconds(60); // 設置終止等待時間 return scheduler; }}
然后在MyTaskExecutor中實現掃描訂單以及判斷訂單是否需要取消:
public class MyTaskExecutor implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + " 在 "+ LocalDateTime.now() +" 執行MyTaskExecutor。。。。。"); }}
運行結果如下:
圖片
基于JDK的DelayQueue實現的延遲隊列解決取消超時訂單的方案,相比較于定時輪訓有如下優點:
但是因為DelayQueue是基于內存的,這也導致它在實現上有一定的缺點:
在介紹時間輪算法實現取消超時訂單功能之前,我們先來看一下什么是時間輪算法?
時間輪算法(Time Wheel Algorithm)是一種高效處理定時任務調度的機制,廣泛應用于各類系統如計時器、調度器等組件。該算法的關鍵理念在于將時間維度映射至物理空間,即構建一個由多個時間槽構成的循環結構,每個槽代表一個固定的時間單位(如毫秒、秒等)。
時間輪實質上是一個具有多個槽位的環形數據結構,隨著時間的推進,時間輪上的指針按照預先設定的速度(例如每秒前進一槽)順時針旋轉。每當指針移動至下一槽位時,系統會檢視該槽位中掛載的所有定時任務,并逐一執行到期的任務。
在時間輪中,每個待執行任務均與其觸發時間點對應的時間槽關聯。添加新任務時,系統會根據任務的期望執行時間計算出相應的槽位編號,并將任務插入該槽。對于未來執行的任務,計算所需等待的槽位數目,確保任務按時被處理。值得注意的是,時間輪設計為循環結構,意味著當指針到達最后一個槽位后會自動返回至第一個槽位,形成連續不斷的循環調度。
借助時間輪算法,定時任務的執行時間以相對固定的時間槽來表示,而非直接依賴于絕對時間。任務執行完畢后,系統會及時將其從時間輪中移除,同時,對于不再需要執行的任務,也可以在任何時候予以移除,確保整個調度系統的高效運作和實時響應。
圖片
如上圖為例,假設一個格子是1秒,則整個wheel能表示的時間段為8s,假設當前指針指向2,此時需要調度一個3s后執行的任務, 顯然應該加入到(2+3=5)的方格中,指針再走3次就可以執行了;如果任務要在10s后執行,應該等指針走完一個round零2格再執行, 因此應放入4,同時將round(1)保存到任務中。檢查到期任務應當只執行round為0的,格子上其他任務的round應減1.
所以,我們可以使用時間輪算法去試一下延遲任務,用于實現取消超時訂單。
我們以Netty4為例,引入依賴:
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.68.Final</version></dependency>
然后定義訂單處理服務,在創建訂單時定義訂單超時時間,以及超時時取消訂單。
@Servicepublic 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() +"創建成功."); // 創建訂單,設置超時時間為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); }}
我們定義訂單創建接口,模擬訂單創建:
@RestController@RequestMapping("orderTimeWheel")public class OrderTimeWheelController { @Autowired private OrderService orderService; @PostMapping("/create") public String createOrder(String orderId) { orderService.createOrder(orderId); return "訂單創建成功:" + orderId; }}
我們分別請求接口,創建訂單:
圖片
可以看見,訂單在5秒鐘之后自動調用取消方法取消訂單。
基于時間輪實現延遲任務來取消超時訂單有如下優點:
但是相對應的也存在一些缺點:
對于Redis實現延遲任務,常見的兩種方案是使用有序集合(Sorted Set,通常簡稱為zset)和使用key過期監聽。
利用有序集合的特性,即集合中的元素是有序的,每個元素都有一個分數(score)。在延遲任務的場景中,可以將任務的執行時間作為分數,將任務的唯一標識(如任務ID)作為集合中的元素。然后,定時輪詢有序集合,查找分數小于當前時間的元素,這些元素即為已經到期需要執行的任務。執行完任務后,可以從有序集合中刪除對應的元素。因此可以將訂單的過期時間作為score,用于實現取消超時訂單。
引入Redis依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.7.0</version></dependency>
配置一下RedisTemplate:
@Configurationpublic class RedisConfig { @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { // 其余配置 如序列化等 return new StringRedisTemplate(factory); }}
創建訂單創建以及自動取消服務:
@EnableScheduling@Servicepublic 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() +"創建成功."); // 假設訂單超時時間為5秒 long expirationTime = 5 * 1000 + System.currentTimeMillis(); redisTemplate.opsForZSet().add(ORDER_TIMEOUT_SET_KEY, orderId, expirationTime); } @Scheduled(fixedRate = 1000) // 每秒檢查一次,實際頻率根據業務需求調整 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) { // 在這里實現訂單取消的實際邏輯 System.out.println("訂單 " + orderId + " 在" + LocalDateTime.now() +"取消"); // 更新訂單狀態、釋放庫存等操作... }}
注意:因本例中基于@Scheduled實現定時輪訓,所以需要使用@EnableScheduling開啟Scheduled功能。具體請參考:玩轉SpringBoot:SpringBoot的幾種定時任務實現方式
我們定義訂單創建接口,模擬訂單創建:
圖片
可以看到訂單5秒鐘后自動取消。
使用Redis有序集合實現取消超時訂單有一些優點:
但是也有一些缺點:
利用Redis的key過期監聽功能。當設置一個key的過期時間時,可以設置一個回調函數,當key過期時,Redis會自動調用這個回調函數。即利用Redis的Keyspace Notifications功能,當一個鍵(Key)過期時,Redis會向已訂閱了相關頻道的客戶端發送一個通知。
使用Redis的key的過期監聽功能之前我們需要啟用Redis Keyspace Notifications,在Redis配置文件(redis.conf)中啟用Key Space Notifications,即打開如下配置:
notify-keyspace-events Ex
notify-keyspace-events設置為Ex,表示啟用所有類型的鍵空間通知,包括過期事件。具體配置方法可能因Redis的版本和環境而有所不同,請根據實際情況進行配置。
然后我們就可以使用代碼實現,首先實現MessageListener接口實現一個監聽器來監聽Redis的key過期事件。當訂單的key過期時,將觸發監聽器中的邏輯,執行取消訂單的操作。
@Componentpublic class OrderExpirationListener implements MessageListener { @Autowired private OrderExpirationService orderService; @Override public void onMessage(Message message, byte[] pattern) { String orderId = message.toString(); // 調用服務取消訂單 orderService.cancelOrder(orderId); }}
然后配置Redis key過期事件監聽器,并將其注冊到Redis連接工廠中。這樣,監聽器將會在Redis的key過期事件發生時被調用。
@Configurationpublic 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")); // 監聽所有數據庫的key過期事件 return container; }}
__keyevent@0__:expired是Redis的系統通道,用于監聽所有數據庫中的key過期事件。如果需要監聽特定數據庫的key過期事件,則可以修改對應的數據庫號。例如,__keyevent@1__:expired表示監聽第一個數據庫的key過期事件。
然后我們就可以實現具體的訂單創建服務以及訂單取消的邏輯了。這里我們模擬一下:
@Componentpublic class OrderExpirationService { @Autowired private RedisTemplate<String, String> redisTemplate; public void createOrder(String orderId) { System.out.println("訂單"+orderId+"在"+ LocalDateTime.now() +"創建成功."); // 假設訂單超時時間為5秒 long expirationTime = 5; redisTemplate.opsForValue().set(orderId, "orderData", expirationTime, TimeUnit.SECONDS); } public void cancelOrder(String orderId) { // 在這里實現訂單取消的實際邏輯 System.out.println("訂單 " + orderId + " 在" + LocalDateTime.now() +"取消"); // 更新訂單狀態、釋放庫存等操作... }}
@RestController@RequestMapping("orderRedis")public class RedisOrderController { @Autowired private OrderExpirationService orderService; @PostMapping("/create") public String createOrder(String orderId) { orderService.createOrder(orderId); return "訂單創建成功:" + orderId; }}
我們創建4個訂單,模擬5秒鐘后的訂單取消
圖片
使用Redis的key過期監聽事件,實現取消超時訂單有以下優點:
但是也存在一些缺點:
使用消息隊列實現取消超時訂單的常見方法是利用延遲隊列以及死信隊列。比如RabbitMq,在介紹實現方式之前,我們先來了解一下RabbitMq的延遲隊列以及死信隊列。
我的RabbitMq是部署在docker中的,所以順帶提議一下關于安裝rabbitmq_delayed_message_exchange插件,我們需要在 Releases · rabbitmq/rabbitmq-delayed-message-exchange (github.com)下載.ez結尾的插件,然后使用docker cp命令將其拷貝到rabbitmq容器內:
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
延遲隊列可以直接處理延遲消息,即消息在指定的延遲時間過后才被投遞給消費者。在超時取消訂單的場景中,訂單創建時將訂單信息封裝成消息,并設置消息的延遲時間,當訂單超時時,消息自動被投遞到處理超時訂單的隊列,消費者接收到消息后執行取消操作。
引入依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>2.7.18</version></dependency>
配置rabbitmq的相關參數:
spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.password=guest spring.rabbitmq.username=guest
配置延遲交換機,并且初始化延遲交換機、隊列及綁定關系
@Configuration@EnableRabbitpublic 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中創建好,訪問http://localhost:15672/在Exchanges中,添加Exchange,設置type= x-delayed-message,如圖:
圖片
在定義一個監聽rabbitmq消息的監聽器,當消息延遲時間到了之后,就會被該監聽器見聽到,在這里判斷訂單是否已經被支付,如果沒有支付則取消。
@Componentpublic class DelayedQueueListener { @Autowired private OrderMqService orderMqService; @RabbitListener(queues = RabbitConfig.ORDER_QUEUE) public void handleDelayedOrder(String orderId) { orderMqService.cancelOrder(orderId); } }
然后我們在訂單創建時,將訂單信息發送到MQ中,等延遲時間到了之后,如果訂單還沒有支付,則執行取消訂單操作。
@Servicepublic 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() +"創建成功."); rabbitTemplate.convertAndSend(RabbitConfig.ORDER_EXCHANGE, RabbitConfig.ROUTING_KEY, orderId, message -> { message.getMessageProperties().setDelay(5 * 1000); return message; }); } public void cancelOrder(String orderId) { // 在這里實現訂單取消的實際邏輯 System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消"); // 更新訂單狀態、釋放庫存等操作... }}
我們模擬創建訂單請求:
@RestController@RequestMapping("orderMq")public class MqOrderController { @Autowired private OrderMqService orderMqService; @PostMapping("/create") public String createOrder(String orderId) { orderMqService.createOrder(orderId); return "訂單創建成功:" + orderId; }}
圖片
可以看見訂單過了5秒之后開始執行取消。
使用延遲隊列方案來實現訂單超時取消等場景的優點:
但是也有一些缺點:
對于延遲隊列,并非只有rabbitmq才有,RocketMQ也有延遲隊列。在RocketMQ中,延遲消息的發送是通過設置消息的延遲級別來實現的。每個延遲級別都對應著一個具體的延遲時間,例如 1 表示 1 秒、2 表示 5 秒、3 表示 10 秒,以此類推。用戶可以根據自己的需求選擇合適的延遲級別。但是也可以看出他并沒有支持的那么精確,如果想要精確的就必須使用RocketMQ的企業版,在企業版中可以自定義設置延遲時間。這里就不過多講解,有興趣的可以自己研究一下。
訂單創建時,將訂單信息發送到一個具有TTL的隊列,當消息在隊列中停留的時間超過了TTL(也就是訂單的有效支付期限),消息就會變為死信。然后再配置隊列,使得這些過期的死信消息被路由到一個預先設置好的死信隊列。最后創建一個消費者監聽這個死信隊列,一旦有消息進來(即訂單超時),消費者便處理這些死信,檢查訂單狀態并執行取消操作。
使用的rabbitmq依賴以及配置同上使用延遲隊列方案。我們來看一下創建處理訂單即帶有TTL的隊列:
@Configurationpublic 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"; /** * 創建訂單隊列 * @return */ @Bean public Queue orderQueue() { Map<String, Object> args = new HashMap<>(); args.put("x-message-ttl", 5000L); // 設置訂單隊列消息有效期為30秒(可以根據實際情況調整) 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); }}
同理也是需要先創建交換機:
圖片
創建訂單業務類,將訂單發送到訂單消息隊列:
@Servicepublic 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() +"創建成功."); rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_QUEUE, orderId); } public void cancelOrder(String orderId) { // 在這里實現訂單取消的實際邏輯 System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消"); // 更新訂單狀態、釋放庫存等操作... }}
在創建死信隊列,私信隊列交換機,通過訂單隊列路由將私信隊列綁定到訂單訂單隊列中:
@Configurationpublic 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"; /** * 創建死信隊列交換機 * @return */ @Bean public DirectExchange deadLetterExchange() { return new DirectExchange(DEAD_LETTER_EXCHANGE); } /** * 創建死信隊列 * @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); }}
在創建一個死信隊列消息監聽器,用于判斷訂單是否超時:
@Componentpublic class DelayedQueueListener { @Autowired private OrderMqService orderMqService; @RabbitListener(queues = RabbitMQConfig.DEAD_LETTER_QUEUE) public void handleDeadLetterOrder(String orderId) { orderMqService.cancelOrder(orderId); }}
然后我們在訂單創建時,將訂單信息發送到訂單MQ中,等消息的TTL到期之后,會自動轉到死信隊列中。
@Servicepublic 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() +"創建成功."); rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_QUEUE, orderId); } public void cancelOrder(String orderId) { // 在這里實現訂單取消的實際邏輯 System.out.println("訂單" + orderId + " 在" + LocalDateTime.now() +"取消"); // 更新訂單狀態、釋放庫存等操作... }}
我們模擬創建訂單接口:
@RestController@RequestMapping("orderMq")public class MqOrderController { @Autowired private OrderMqService orderMqService; @PostMapping("/create") public String createOrder(String orderId) { orderMqService.createOrder(orderId); return "訂單創建成功:" + orderId; }}
圖片
可以看見訂單過了5秒之后開始執行取消。
使用死信隊列實現取消超時訂單的優點:
當然他也有一些缺點:
訂單超時自動取消是電商平臺中非常重要的功能之一,通過合適的技術方案,可以實現自動化處理訂單超時的邏輯,提升用戶體驗和系統效率。本文討論了多種實現訂單超時自動取消的技術方案,包括定時輪詢、JDK的延遲隊列、時間輪算法、Redis實現以及MQ消息隊列中的延遲隊列和死信隊列。
有序集合(Sorted Set):利用有序集合的特性,定時輪詢查找已超時的任務。優點是查詢效率高,適用于分布式環境,減少數據庫壓力。缺點是依賴定時任務執行頻率,處理超時訂單的實時性受限,且在處理事務一致性方面需要額外努力。
Key過期監聽:利用Redis鍵過期事件自動觸發訂單取消邏輯。優點是實時性好,資源消耗少,支持高并發。缺點是對Redis服務的依賴性強,極端情況下處理能力可能成為瓶頸,且鍵過期有一定的不確定性。
延遲隊列(如RabbitMQ的rabbitmq_delayed_message_exchange插件):實現消息在指定延遲后送達處理隊列。優點是處理高效,異步執行,易于擴展,模塊化程度高。缺點是高度依賴消息隊列服務,配置復雜度增加,可能涉及消息丟失或延遲風險,以及消息隊列與數據庫操作一致性問題。
死信隊列:通過設置隊列TTL將超時訂單轉為死信,由監聽死信隊列的消費者處理。優點是能捕獲并隔離異常消息,實現業務邏輯分離,資源保護良好,方便追蹤和分析問題。缺點是相比延遲隊列,處理超時不夠精確,配置復雜,且同樣存在消息處理完整性及一致性問題。
不同方案各有優劣,實際應用中應根據系統的具體需求、資源狀況以及技術棧等因素綜合評估,選擇最適合的方案。在許多現代大型系統中,通常會選擇消息隊列的延遲隊列或死信隊列方案,以充分利用其異步處理、資源優化和擴展性方面的優勢。
本文鏈接:http://www.www897cc.com/showinfo-26-79986-0.html美團二面:如何設計一個訂單超時未支付關閉訂單的解決方案?我說使用Elastic-Job輪訓判斷。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 干貨必讀: 測試開發既然都這么厲害了!為啥不直接轉業務開發?
下一篇: 球盒模型:一切回溯窮舉,皆從此法出