我們在電商平臺購物時,下單之后會有一個付款倒計時,如果在規定的時間內未付款,訂單就會自動關閉。
類似這樣的場景還有很多,比如優惠劵到期失效,下單后自動發消息等。
今天我們來討論一下,要實現諸如此類的功能,都有哪些技術方案,這些方案的優缺點是什么。需要說明的是,以下這些方案每一種都有其適用場景,并無絕對優劣之分。
1,定時任務
通過定時任務關閉訂單,是一種成本很低,實現也很容易的方案。通過簡單的幾行代碼,寫一個定時任務,定期掃描數據庫中的訂單,如果時間過期,就將其狀態更新為關閉即可。
優點:實現容易,成本低,基本不依賴其他組件。
缺點:
- 時間可能不夠精確。由于定時任務掃描的間隔是固定的,所以可能造成一些訂單已經過期了一段時間才被掃描到,訂單關閉的時間比正常時間晚一些。
- 增加了數據庫的壓力。隨著訂單的數量越來越多,掃描的成本也會越來越大,執行時間也會被拉長,可能導致某些應該被關閉的訂單遲遲沒有被關閉。
總結:采用定時任務的方案比較適合對時間要求不是很敏感,并且數據量不太多的業務場景。
2,JDK延遲隊列DelayQueue
DelayQueue是JDK提供的一個無界隊列,我們可以看到,DelayQueue隊列中的元素需要實現Delayed,它只提供了一個方法,就是獲取過期時間。
用戶的訂單生成以后,設置過期時間比如30分鐘,放入定義好的DelayQueue,然后創建一個線程,在線程中通過while(true)不斷的從DelayQueue中獲取過期的數據。
優點:不依賴任何第三方組件,連數據庫也不需要了,實現起來也方便。
缺點:
- 因為DelayQueue是一個無界隊列,如果放入的訂單過多,會造成JVM OOM。
- DelayQueue基于JVM內存,如果JVM重啟了,那所有數據就丟失了。
總結:DelayQueue適用于數據量較小,且丟失也不影響主業務的場景,比如內部系統的一些非重要通知,就算丟失,也不會有太大影響。
3,redis過期監聽
Redis是一個高性能的KV數據庫,除了用作緩存以外,其實還提供了過期監聽的功能。
在redis.conf中,配置notify-keyspace-events Ex即可開啟此功能。
然后在代碼中繼承KeyspaceEventMessageListener,實現onMessage就可以監聽過期的數據量。
public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean { private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*"); //...省略部分代碼 public void init() { if (StringUtils.hasText(keyspaceNotificationsConfigParameter)) { RedisConnection connection = listenerContainer.getConnectionFactory().getConnection(); try { Properties config = connection.getConfig("notify-keyspace-events"); if (!StringUtils.hasText(config.getProperty("notify-keyspace-events"))) { connection.setConfig("notify-keyspace-events", keyspaceNotificationsConfigParameter); } } finally { connection.close(); } } doRegister(listenerContainer); } protected void doRegister(RedisMessageListenerContainer container) { listenerContainer.addMessageListener(this, TOPIC_ALL_KEYEVENTS); } //...省略部分代碼 @Override public void afterPropertiesSet() throws Exception { init(); } }
通過以上源碼,我們可以發現,其本質也是注冊一個listener,利用redis的發布訂閱,當key過期時,發布過期消息(key)到Channel :__keyevent@*__:expired中。
在實際的業務中,我們可以將訂單的過期時間設置比如30分鐘,然后放入到redis。30分鐘之后,就可以消費這個key,然后做一些業務上的后置動作,比如檢查用戶是否支付。
優點:由于redis的高性能,所以我們在設置key,或者消費key時,速度上是可以保證的。
缺點:由于redis的key過期策略原因,當一個key過期時,redis無法保證立刻將其刪除,自然我們的監聽事件也無法第一時間消費到這個key,所以會存在一定的延遲。另外,在redis5.0之前,訂閱發布中的消息并沒有被持久化,自然也沒有所謂的確認機制。所以一旦消費消息的過程中我們的客戶端發生了宕機,這條消息就徹底丟失了。
總結:redis的過期訂閱相比于其他方案沒有太大的優勢,在實際生產環境中,用得相對較少。
4,Redisson分布式延遲隊列RDelayedQueue
Redisson是一個基于redis實現的JAVA 駐內存數據網格,它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務。
Redisson除了提供我們常用的分布式鎖外,還提供了一個分布式延遲隊列RDelayedQueue,他是一種基于zset結構實現的延遲隊列,其實現類是RedissonDelayedQueue。
優點:使用簡單,并且其實現類中大量使用lua腳本保證其原子性,不會有并發重復問題。
缺點:需要依賴redis(如果這算一種缺點的話)。
總結:Redisson是redis官方推薦的JAVA客戶端,提供了很多常用的功能,使用簡單、高效,推薦大家嘗試使用。
5,RocketMQ延遲消息
延遲消息,當消息寫入到Broker后,不會立刻被消費者消費,需要等待指定的時長后才可被消費處理的消息,稱為延時消息。
在訂單創建之后,我們就可以把訂單作為一條消息投遞到rocketmq,并將延遲時間設置為30分鐘,這樣,30分鐘后我們定義的consumer就可以消費到這條消息,然后檢查用戶是否支付了這個訂單。
通過延遲消息,我們就可以將業務解耦,極大地簡化我們的代碼邏輯。
優點:可以使代碼邏輯清晰,系統之間完全解耦,只需關注生產及消費消息即可。另外其吞吐量極高,最多可以支撐萬億級的數據量。
缺點:相對來說mq是重量級的組件,引入mq之后,隨之而來的消息丟失、冪等性問題等都加深了系統的復雜度。
總結:通過mq進行系統業務解耦,以及對系統性能削峰填谷已經是當前高性能系統的標配。
6,RabbitMQ死信隊列
除了RocketMQ的延遲隊列,RabbitMQ的死信隊列也可以實現消息延遲功能。
當RabbitMQ中的一條正常消息,因為過了存活時間(TTL過期)、隊列長度超限、被消費者拒絕等原因無法被消費時,就會被當成一條死信消息,投遞到死信隊列。
基于這樣的機制,我們可以給消息設置一個ttl,然后故意不消費消息,等消息過期就會進入死信隊列,我們再消費死信隊列即可。
通過這樣的方式,就可以達到同RocketMQ延遲消息一樣的效果。
優點:同RocketMQ一樣,RabbitMQ同樣可以使業務解耦,基于其集群的擴展性,也可以實現高可用、高性能的目標。
缺點:死信隊列本質還是一個隊列,隊列都是先進先出,如果隊頭的消息過期時間比較長,就會導致后面過期的消息無法得到及時消費,造成消息阻塞。
總結:除了增加系統復雜度之外,死信隊列的阻塞問題也是需要我們重點關注的。
最后
本文介紹了常見的6種實現訂單關閉的方案,不同的方案都有其適用的場景,各自的優缺點也不盡相同,大家可以根據自己的業務場景,選擇合適的方案。
如果本文中沒有提到你熟悉的技術方案,也歡迎在評論區分享給大家,期待共同學習進步。
寫文不易,朋友們幫忙點點贊和關注吧,謝謝。