互聯網面試必備之超時訂單自動關閉的優雅實現
任務場景
12306訂單,比如我們買了一張票,一般會給30分鐘的支付時間,如果30分鐘內沒有支付,則系統會自動取消訂單,然后釋放鎖定座位。

那么超時訂單有哪幾種實現方式呢?
超時訂單自動關閉的優雅實現
1、數據庫輪詢(30S)
訂單表(訂單ID,狀態,創建時間)

輪詢數據庫會帶來什么問題?
- 輪詢大部分時間其實是在做無用功,我們假設一張訂單是45分鐘過期,每分鐘我們掃描一次,對這張訂單來說,要掃描45次以后,才會檢查到這張訂單過期,這就意味著數據庫的資源(連接,IO)被白白浪費了;
- 處理上的不及時,一個待支付的電影票訂單我們假設是12:00:35過期,但是上次掃描的時間是12:00:30,那么這個訂單實際的過期時間是什么時候?12:01:30,和我本來的過期時間差了55秒鐘。放在業務上,會帶來什么問題?這張電影票,假設是最后一張,有個人12:00:55來買票,買得到嗎?當然買不到了。那么這張電影票很有可能就浪費了。如果縮短掃描的時間間隔,第一只能改善不能解決,第二,又會對數據庫造成更大的壓力。
2、使用DelayQueue 實現

DelayQueue: 阻塞隊列(先進先出)
- 支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿
- 支持阻塞的移除方法:意思是在隊列為空時,獲取元素的線程會等待隊列變為非空。
Delayed接口使對象成為延遲對象,它使存放在DelayQueue類中的對象具有了激活日期。該接口強制實現下列兩個方法。
應用重啟帶來的問題
- 保存在Queue 中的訂單會丟失
- 已過期的訂單不會被處理

解決之道

從系統伸縮性角度考慮:應用集群化了怎么辦?
集群化了會帶來什么問題?應用之間會相互搶奪訂單,特別是在應用重啟的時候,重新啟動的那個應用會把不屬于自己的訂單,也全部加載到自己的隊列里去,一是造成內存的浪費,二來會造成訂單的重復處理,而且加大了數據庫的壓力。
解決方案:
1、 給每臺服務器編號,然后在訂單表里登記每條訂單的服務器編號;2,更簡單的,在訂單表里登記每臺服務器的IP地址,修改相應的sql語句即可。
幾個問題:如果有一臺服務器掛了怎么辦?運維吃干飯的嗎?服務器掛了趕緊啟動啊。如果是某臺服務器下線或者宕機,起不來怎么辦?這個還是還是稍微有點麻煩,需要人工干預一下,手動把庫里的每條訂單數據的服務器編號改為目前正常的服務器的編號,不過也就是一條sql語句的事,然后想辦法讓正常的服務器進行處理(重啟正常的服務器)。
能不能同時解決伸縮性和擴展性問題?
用delayqueue是隊列,分布式情況我們何不直接引入消息中間件呢?一舉解決我們應用的伸縮性和擴展性問題
我們可以使用ActiveMQ的延遲和定時投遞
3、ActiveMQ延遲隊列
修改配置文件(activemq.xml),增加延遲和定時投遞支持
- <broker xmlns="http://activemq.apache.org/schema/core"
- brokerName="localhost" dataDirectory="${activemq.data}"
- schedulerSupport="true">
需要把幾個描述消息定時調度方式的參數作為屬性添加到消息,broker端的調度器就會按照我們想要的行為去處理消息。
一共有4個屬性:
- AMQ_SCHEDULED_DELAY :延遲投遞的時間
- AMQ_SCHEDULED_PERIOD :重復投遞的時間間隔
- AMQ_SCHEDULED_REPEAT:重復投遞次數
- AMQ_SCHEDULED_CRON:Cron表達式
ActiveMQ也提供了一個封裝的消息類型:org.apache.activemq.ScheduledMessage,可以使用這個類來輔助設置,使用例子如:延遲60秒
- MessageProducer producer = session.createProducer(destination);
- TextMessage message = session.createTextMessage("test msg");
- long time = 60 * 1000;
- message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, time);
- producer.send(message);
例子:延遲30秒,投遞10次,間隔10秒:
- TextMessage message = session.createTextMessage("test msg");
- long delay = 30 * 1000;
- long period = 10 * 1000;
- int repeat = 9;
- message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
- message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
- message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
- 也可使用 CRON 表達式,如message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");
也可使用 CRON 表達式,如message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");
4、Redis有序集合
Redis 有序集合
Redis sortedSet 集合(sorted set 也叫zset) 是一個有序集合,每個元素(member)都關聯了一個score,可以通過score排序獲取集合中的值。
zset常用命令:
- 添加元素:zadd key score member[[score member]]
- 按順序查詢元素:zrange key start stop [withscores]
- 查詢元素score:zscore key member
- 移除元素: zrem key member 【member ...】
將訂單超時時間戳(long)與訂單號分別設置為score與member,系統掃描第一個元素判斷定是否超時,拿到分數最小的,超時時間最早的。判斷與當前時間戳的關系

生產者

消費者

5、Redis Set 集合
將訂單延遲時間的秒級時間戳設置為set集合的key,value 為訂單ID
sadd set的key item的項值,item項可以有多個
按秒級的時間進行聚合,即 key為時間戳,里面可以由多個ID


總結
1、DB輪詢
- 優點: 實現簡單、無 技術難點、異常恢復、支持分布式/進群環境
- 缺點:影響數據庫性能、時效性差、效率低
2、DelayedQueue
- 優點: 實現簡單、性能較好
- 缺點: 異常恢復困難、分布式/集群實現坤丹
3、redis
優點 ; 解耦、異常恢復、支持分布式/集群環境
缺點
- 增加redis維護、占用寬帶
- 有序 集合缺點: 當sortedSet集合中元素過多時,插入性能降低
- Set集合缺點: 已經超時未處理的訂單不好處理