網(wǎng)上有很多關于pos機接收返回超時,100億訂單超時處理的知識,也有很多人為大家解答關于pos機接收返回超時的問題,今天pos機之家(www.shineka.com)為大家整理了關于這方面的知識,讓我們一起來看下吧!
本文目錄一覽:
pos機接收返回超時
▌背景:
超時處理,是一個很有技術(shù)難度的問題。
所以很多的小伙伴,在寫簡歷的時候,喜歡把這個技術(shù)難題寫在簡歷里邊, 體現(xiàn)自己高超的技術(shù)水平。
在40歲老架構(gòu)師 尼恩的讀者社區(qū)(50+個)中,尼恩經(jīng)常指導大家 優(yōu)化簡歷。
最近,有小伙伴寫簡歷,就寫了這問題:
通過 定時任務+ 數(shù)據(jù)分片的方式,進行訂單的超時處理。
尼恩去問他,怎么進行分片、怎么進行調(diào)度的。
小伙伴就開始一半糊涂、一半清晰。也就是說,他也能講一些細節(jié),比如怎么分片,怎么調(diào)度,感覺好像是接觸過。
為啥說一半糊涂呢? 就是他的方案的 空白點太多, 隨便找?guī)讉€點發(fā)問, 就講不清楚了。
一半糊涂、一半清晰的方案,問題很嚴重,為啥呢? 從面試官的角度來說,這 就是沒有真正干過, 說明是盜用的其他人的方案。
這種案例,在尼恩的招人生涯中見得太多。面試過N多這種看上去牛逼轟轟,實際上 稀里糊涂的面試者,沒有一次讓他們過的。
那么問題來了,訂單的超時處理的方案,具體是什么樣的呢?
這里尼恩給大家做一下系統(tǒng)化、體系化的 訂單的超時處理的方案,使得大家可以充分展示一下大家雄厚的 “技術(shù)肌肉”,讓面試官愛到 “不能自已、口水直流”。
也一并把這個題目以及參考答案,收入咱們的《尼恩java面試寶典 PDF》,供后面的小伙伴參考,提升大家的 3高 架構(gòu)、設計、開發(fā)水平。
注:本文以 PDF 持續(xù)更新,最新尼恩 架構(gòu)筆記、面試題 的PDF文件,請到《技術(shù)自由圈》公號領取
▌100億訂單的超時處理難題
100億訂單的超時處理,是一個很有技術(shù)難度的問題。
比如,用戶下了一個訂單之后,需要在指定時間內(nèi)(例如30分鐘)進行支付,在到期之前可以發(fā)送一個消息提醒用戶進行支付。
下面是一個訂單的流程:
如上圖所示,實際的生產(chǎn)場景中,一個訂單流程中有許多環(huán)節(jié)要用到定時處理,比如:
買家超時未付款:超過15分鐘沒有支付,訂單自動取消。商家超時未發(fā)貨:商家超過1個月沒發(fā)貨,訂單自動取消。買家超時未收貨:商家發(fā)貨后,買家沒有在14天內(nèi)點擊確認收貨,則自動收貨。▌海量任務的定時處理方案
基于內(nèi)存的延遲隊列/優(yōu)先級隊列處理基于內(nèi)存的時間輪調(diào)度基于分布式隊列延遲消息的定時方案基于分布式K-V組件(如Redis)過期時間的定時方案一些消息中間件的Broker端內(nèi)置了延遲消息支持的能力
▌方案1:基于內(nèi)存的延時隊列
JDK中提供了一種延遲隊列數(shù)據(jù)結(jié)構(gòu)DelayQueue,其本質(zhì)是封裝了PriorityQueue,可以把元素進行排序。
Java 的Timer、JUC的延遲調(diào)度,最終都是基于 PriorityQueue。
基于內(nèi)存的延時隊列進行調(diào)度的邏輯,其實比較簡單,具體如下:
把訂單插入DelayQueue中,以超時時間作為排序條件,將訂單按照超時時間從小到大排序。起一個線程不停輪詢隊列的頭部,如果訂單的超時時間到了,就出隊進行超時處理,并更新訂單狀態(tài)到數(shù)據(jù)庫中。為了防止機器重啟導致內(nèi)存中的DelayQueue數(shù)據(jù)丟失,每次機器啟動的時候,需要從數(shù)據(jù)庫中初始化未結(jié)束的訂單,加入到DelayQueue中。基于內(nèi)存的延時隊列調(diào)度的優(yōu)點和缺點:
優(yōu)點:
簡單,不需要借助其他第三方組件,成本低。缺點:
所有超時處理訂單都要加入到DelayQueue中,占用內(nèi)存大。沒法做到分布式處理,只能在集群中選一臺leader專門處理,效率低。不適合訂單量比較大的場景。▌方案2:RocketMQ的定時消息
RocketMQ支持任意秒級的定時消息,如下圖所示:
使用門檻低,只需要在發(fā)送消息的時候設置延時時間即可,以 java 代碼為例:
MessageBuilder messageBuilder = null;Long deliverTimeStamp = System.currentTimeMillis() + 10L * 60 * 1000; //延遲10分鐘Message message = messageBuilder.setTopic("topic") //設置消息索引鍵,可根據(jù)關鍵字精確查找某條消息。 .setKeys("messageKey") //設置消息Tag,用于消費端根據(jù)指定Tag過濾消息。 .setTag("messageTag") //設置延時時間 .setDeliverytimestamp(deliverTimeStamp) //消息體 .setBody("messageBody".getBytes()) .build();SendReceipt sendReceipt = producer.send(message);System.out.println(sendReceipt.getMessageId());
▌RocketMQ的定時消息是如何實現(xiàn)的呢?
RocketMQ 定時消息的推送,主要分為 : 延遲消息、定時消息。
在 RocketMQ 4.x 版本,使用 延遲消息來實現(xiàn)消息的多個級別延遲消息——粗粒度延遲。在 RocketMQ 5.x 版本,使用定時消息來實現(xiàn)消息的更精準定時消息,——細粒度延遲。RocketMQ 4.x 版本只支持 延遲消息,有一些局限性。而 RocketMQ 5.x 版本引入了定時消息,彌補了 延遲消息的不足。
▌RocketMQ 4.x 粗粒度 延遲消息
RocketMQ 的 延遲消息是指 Producer 發(fā)送消息后,Consumer 不會立即消費,而是需要等待固定的時間才能消費。
在一些場景下, 延遲消息是很有用的,比如電商場景下關閉 30 分鐘內(nèi)未支付的訂單。
使用 延遲消息非常簡單,只需要給消息的 delayTimeLevel 屬性賦值就可以。
發(fā)送 分級定時消息,參考下面代碼:
Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());//第 3 個級別,10smessage.setDelayTimeLevel(3);producer.send(message);
延遲消息有 18 個級別,如下:
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
▌實現(xiàn)原理
延遲消息的實現(xiàn)原理如下圖:
Producer 把消息發(fā)送到 Broker 后,Broker 判斷是否 延遲消息,
如果是,首先會把消息投遞到延時隊列(Topic = SCHEDULE_TOPIC_XXXX,queueId = delayTimeLevel - 1)。
另外,由于18 個級別的延遲,所以定時任務線程池會有 18 個線程來對延時隊列進行調(diào)度,每個線程調(diào)度一個延時級別,
調(diào)度任務把 延遲消息再投遞到原始隊列,這樣 Consumer 就可以拉取到了到期的消息。
▌RocketMQ 4.x 粗粒度 延遲消息存在不足
RocketMQ 4.x 延遲消息存在著一些不足:
1、延時級別只有 18 個,粒度很粗,并不能滿足所有場景;
"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
2、可以通過修改 messageDelayLevel 配置來自定義延時級別,雖然可以修改,但是不靈活
比如一個在大規(guī)模的平臺上,延時級別成百上千,而且隨時可能增加新的延時時間;
3.延時時間不準確,后臺的定時線程可能會因為處理消息量大導致延時誤差大。
▌RocketMQ 5.x 細粒度定時消息
為了彌補 延遲消息的不足,RocketMQ 5.0 引入了細粒度定時消息。
經(jīng)典的時間輪算法如下:
時間輪類似map,key 為時間的刻度,value為此刻度所對應的任務列表。
一般來說,可以理解為一種環(huán)形結(jié)構(gòu),像鐘表一樣被分為多個 slot 槽位。
每個 slot 代表一個時間段,每個 slot 中可以存放多個任務,使用的是鏈表結(jié)構(gòu)保存該時間段到期的所有任務。
時間輪通過一個時針隨著時間一個個 slot 轉(zhuǎn)動,并執(zhí)行 slot 中的所有到期任務。
從內(nèi)部結(jié)構(gòu)來看,是一個Bucket 數(shù)組,每個 Bucket 表示時間輪中一個 slot。
從 Bucket 的結(jié)構(gòu)定義可以看出,Bucket 內(nèi)部是一個雙向鏈表結(jié)構(gòu),雙向鏈表的每個節(jié)點持有一個 task 對象,task 代表一個定時任務。每個 Bucket 都包含雙向鏈表 head 和 tail 兩個 task 節(jié)點,這樣就可以實現(xiàn)不同方向進行鏈表遍歷
時間輪的算法、以及時間輪的演進,非常重要
內(nèi)容太多,這里不做展開,具體請看尼恩的3高架構(gòu)筆記《徹底穿透Caffeine底層源碼和架構(gòu)》PDF,里邊介紹了三個超高并發(fā)組件:時間輪、 多級時間輪、條帶環(huán)狀結(jié)構(gòu)、MPSC隊列,大家一定認真看看。
▌RocketMQ 5.X的時間輪
RocketMQ 定時消息引入了秒級的時間輪算法。注意,是 秒級時間輪。
從源碼來看,RocketMQ 定義了一個 7 天的以秒為單位的時間輪
注意:時間刻度為1s,沒有再細,比如 10ms、100ms之類的 。
作為參考下面提供一個一分鐘的,以1s為刻度的時間輪,如下圖:
圖中是一個 60s 的時間輪,每一個槽位是一個鏈表,鏈表里邊的節(jié)點,通過TimerLog節(jié)點結(jié)構(gòu)來記錄不同時刻的消息。
所以,RocketMQ 使用 TimerWheel 來描述時間輪,TimerWheel 中每一個時間節(jié)點是一個 Slot,Slot 保存了這個延時時間的 TimerLog 信息的鏈表。
Slot 數(shù)據(jù)結(jié)構(gòu)如下圖:
參考下面代碼:
//類 TimerWheelpublic void putSlot(long timeMs, long firstPos, long lastPos, int num, int magic) { localBuffer.get().position(getSlotIndex(timeMs) * Slot.SIZE); localBuffer.get().putLong(timeMs / precisionMs); localBuffer.get().putLong(firstPos); localBuffer.get().putLong(lastPos); localBuffer.get().putInt(num); localBuffer.get().putInt(magic);}
▌綁定時間輪
時間通過TimerWheel來描述時間輪不同的時刻,
并且,對于所處于同一個刻度的的消息,組成一個槽位的鏈表,每一個定時消息,有一個TimerLog 描述定時相關的信息
TimerLog 有一個核心字段prevPos,同一個時間輪槽位里邊的TimerLog ,會通過prevPos串聯(lián)成一個鏈表.
首先看一下 TimerLog 保存的數(shù)據(jù)結(jié)構(gòu),如下圖:
參考下面代碼:
//TimerMessageStore類ByteBuffer tmpBuffer = timerLogBuffer;tmpBuffer.clear();tmpBuffer.putInt(TimerLog.UNIT_SIZE); //sizetmpBuffer.putLong(slot.lastPos); //prev postmpBuffer.putInt(magic); //magictmpBuffer.putLong(tmpWriteTimeMs); //currWriteTimetmpBuffer.putInt((int) (delayedTime - tmpWriteTimeMs)); //delayTimetmpBuffer.putLong(offsetPy); //offsettmpBuffer.putInt(sizePy); //sizetmpBuffer.putInt(hashTopicForMetrics(realTopic)); //hashcode of real topictmpBuffer.putLong(0); //reserved value, just set to 0 nowlong ret = timerLog.append(tmpBuffer.array(), 0, TimerLog.UNIT_SIZE);if (-1 != ret) { // If it's a delete message, then slot's total num -1 // TODO: check if the delete msg is in the same slot with "the msg to be deleted". timerWheel.putSlot(delayedTime, slot.firstPos == -1 ? ret : slot.firstPos, ret, isDelete ? slot.num - 1 : slot.num + 1, slot.magic);}
▌時間輪上的時間指針
時間輪上,會有一個指向當前時間的指針定時地移動到下一個時間(秒級)。
TimerWheel中的每一格代表著一個時刻,同時會有一個firstPos指向槽位鏈的首條TimerLog記錄的地址,一個lastPos指向這個槽位鏈最后一條TimerLog的記錄的地址。
當需要新增一條記錄的時候,
例如現(xiàn)在我們要新增一個 “1-4”。
那么就將新記錄的 prevPos 指向當前的 lastPos,即 “1-3”,然后修改 lastPos 指向 “1-4”。
這樣就將同一個刻度上面的 TimerLog 記錄全都串起來了。
內(nèi)容太多,這里不做展開,具體請看尼恩的3高架構(gòu)筆記《徹底穿透Caffeine底層源碼和架構(gòu)》PDF,里邊介紹了三個超高并發(fā)組件:時間輪、 多級時間輪、條帶環(huán)狀結(jié)構(gòu)、MPSC隊列,大家一定認真看看。
▌精準定時消息發(fā)送方式
使用 RocketMQ 定時消息時,客戶端定義精準定時消息的示例代碼如下:
//定義消息投遞時間// deliveryTime = 未來的一個時間戳org.apache.rocketmq.common.message.Message messageExt = this.sendMessageActivity.buildMessage(null, Lists.newArrayList( Message.newBuilder() .setTopic(Resource.newBuilder() .setName(TOPIC) .build()) .setSystemProperties(SystemProperties.newBuilder() .setMessageId(msgId) .setQueueId(0) .setMessageType(MessageType.DELAY) .setDeliveryTimestamp(Timestamps.fromMillis(deliveryTime)) .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) .setBornHost(StringUtils.defaultString(RemotingUtil.getLocalAddress(), "127.0.0.1:1234")) .build()) .setBody(ByteString.copyFromUtf8("123")) .build() ),Resource.newBuilder().setName(TOPIC).build()).get(0);
▌源碼分析:消息投遞原理
客戶端SDK代碼中,Producer 創(chuàng)建消息時給消息傳了一個系統(tǒng)屬性 deliveryTimestamp,
這個屬性指定了消息投遞的時間,并且封裝到消息的 TIMER_DELIVER_MS 屬性,代碼如下:
protected void fillDelayMessageProperty(apache.rocketmq.v2.Message message, org.apache.rocketmq.common.message.Message messageWithHeader) { if (message.getSystemProperties().hasDeliveryTimestamp()) { Timestamp deliveryTimestamp = message.getSystemProperties().getDeliveryTimestamp(); //delayTime 這個延時時間默認不能超過 1 天,可以配置 long deliveryTimestampMs = Timestamps.toMillis(deliveryTimestamp); validateDelayTime(deliveryTimestampMs); //... String timestampString = String.valueOf(deliveryTimestampMs); //MessageConst.PROPERTY_TIMER_DELIVER_MS="TIMER_DELIVER_MS" MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_TIMER_DELIVER_MS, timestampString); }}
服務端代碼中,Broker 收到這個消息后,判斷到 TIMER_DELIVER_MS 這個屬性是否有值,
如果有,就會把這個消息投遞到 Topic 是 rmq_sys_wheel_timer 的隊列中,這個只有一個分區(qū),queueId 是 0,作為中轉(zhuǎn)的隊列
中轉(zhuǎn)的時候,同時會保存原始消息的 Topic、queueId、投遞時間(TIMER_OUT_MS)。
TimerMessageStore 中有個定時任務 TimerEnqueueGetService 會從 rmq_sys_wheel_timer 這個 Topic 中讀取消息,然后封裝 TimerRequest 請求并放到內(nèi)存隊列 enqueuePutQueue。
一個異步任務TimerEnqueuePutService 從上面的 enqueuePutQueue 取出 TimerRequest 然后封裝成 TimerLog,然后綁定到時間輪
▌TimerLog 是怎么和時間輪關聯(lián)起來的呢?
RocketMQ 使用 TimerWheel 來描述時間輪,
從源碼上看,RocketMQ 定義了一個 7 天的以秒為單位的時間輪。TimerWheel 中每一個時間節(jié)點是一個 Slot,Slot 保存了這個延時時間的 TimerLog 信息。
數(shù)據(jù)結(jié)構(gòu)如下圖:
參考下面代碼:
//類 TimerWheelpublic void putSlot(long timeMs, long firstPos, long lastPos, int num, int magic) { localBuffer.get().position(getSlotIndex(timeMs) * Slot.SIZE); localBuffer.get().putLong(timeMs / precisionMs); localBuffer.get().putLong(firstPos); localBuffer.get().putLong(lastPos); localBuffer.get().putInt(num); localBuffer.get().putInt(magic);}
通過TimerWheel的 putSlot方法,TimerLog 跟 時間輪就綁定起來了,見下圖:
如果時間輪的一個時間節(jié)點(Slot)上有一條新的消息到來,那只要新建一個 TimerLog,然后把它的指針指向該時間節(jié)點的最后一個 TimerLog,然后把 Slot 的 lastPos 屬性指向新建的這個 TimerLog,如下圖:
▌時間輪中的 定時消息異步處理總流程
終于,咱們的定時消息進入到時間輪了。
那么,隨著時間刻度的步進, 上面的消息,怎么轉(zhuǎn)移到原始的topic的 分區(qū)呢?
由于 rocketmq的源碼是超高性能的,所以,這里有N個隊列做緩沖,有N個任務
這里用到 5 個定時任務和 3個隊列來實現(xiàn)。
定時消息的處理流程如下圖:
▌時間輪轉(zhuǎn)動
轉(zhuǎn)動時間輪時,TimerDequeueGetService 這個定時任務從當前時間節(jié)點(Slot)對應的 TimerLog 中取出數(shù)據(jù),封裝成 TimerRequest 放入 dequeueGetQueue 隊列。
▌CommitLog 中讀取消息
定時任務 TimerDequeueGetMessageService 從隊列 dequeueGetQueue 中拉取 TimerRequest 請求,然后根據(jù) TimerRequest 中的參數(shù)去 CommitLog(MessageExt) 中查找消息,查出后把消息封裝到 TimerRequest 中,然后把 TimerRequest 寫入 dequeuePutQueue 這個隊列。
▌寫入原隊列
定時任務 TimerDequeuePutMessageService 從 dequeuePutQueue 隊列中獲取消息,
把消息轉(zhuǎn)換成原始消息,投入到原始隊列中,這樣消費者就可以拉取到了。
▌RocketMQ的定時消息的優(yōu)點和不足
▌要注意的地方
對于定時時間的定義,客戶端、Broker 和時間輪的默認最大延時時間定義是不同的,使用的時候需要注意。
▌RocketMQ的定時消息優(yōu)點
精度高,支持任意時刻。使用門檻低,和使用普通消息一樣。▌RocketMQ的定時消息缺點
時長的使用限制:定時和延時消息的msg.setStartDeliverTime參數(shù)可設置40天內(nèi)的任何時刻(單位毫秒),超過40天消息發(fā)送將失敗。
設置定時和延時消息的投遞時間后,從中轉(zhuǎn)隊列調(diào)度到了原始的消息隊列之后,依然受3天的消息保存時長限制。例如,設置定時消息5天后才能被消費,如果第5天后一直沒被消費,那么這條消息將在第8天被刪除。
海量 消息場景,存儲成本高:在海量訂單場景中,如果每個訂單需要新增一個定時消息,且不會馬上消費,額外給MQ帶來很大的存儲成本。
同一個時刻大量消息會導致消息延遲:定時消息的實現(xiàn)邏輯需要先經(jīng)過定時存儲等待觸發(fā),定時時間到達后才會被投遞給消費者。
因此,如果將大量定時消息的定時時間設置為同一時刻,則到達該時刻后會有大量消息同時需要被處理,會造成系統(tǒng)壓力過大,導致消息分發(fā)延遲,影響定時精度。
▌方案3:Redis的過期監(jiān)聽
和RocketMQ定時消息一樣,Redis支持過期監(jiān)聽,也能達到的能力
▌通過Redis中key的過期事件,作為延遲消息
可以通過Redis中key的過期事件,作為延遲消息。使用Redis進行訂單超時處理的流程圖如下
具體步驟如下:
1.在服務器中 修改redis配置文件, 開啟"notify-keyspace-events Ex"
原來notify-keyspace-events 屬性是" " 空的,我們只需要填上“Ex”就行了
2.監(jiān)聽key的過期回調(diào)
創(chuàng)建一個Redis監(jiān)控類,用于監(jiān)控過期的key,該類需繼承KeyExpirationEventMessageListener
public class KeyExpiredListener extends KeyExpirationEventMessageListener { public KeyExpiredListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } @Override public void onMessage(Message message, byte[] pattern) { String keyExpira = message.toString(); System.out.println("監(jiān)聽到key:" + expiredKey + "已過期"); }}
3.創(chuàng)建Redis配置類 , 裝配這個 監(jiān)聽器
@Configurationpublic class RedisConfiguration { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public RedisMessageListenerContainer redisMessageListenerContainer() { RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory); return redisMessageListenerContainer; } @Bean public KeyExpiredListener keyExpiredListener() { return new KeyExpiredListener(this.redisMessageListenerContainer()); }}
▌Redis過期時間作為延遲消息的原理
每當一個key設置了過期時間,Redis就會把該key帶上過期時間,在redisDb中通過expires字段維護:
typedef struct redisDb { dict *dict; /* 維護所有key-value鍵值對 */ dict *expires; /* 過期字典,維護設置失效時間的鍵 */ ....} redisDb;
這個結(jié)構(gòu),通過一個 過期字典dict來維護
過期字典dict 本質(zhì)上是一個鏈表,每個節(jié)點的數(shù)據(jù)結(jié)構(gòu)結(jié)構(gòu)如下:
key是一個指針,指向某個鍵對象。value是一個long long類型的整數(shù),保存了key的過期時間。為了提升性能,Redis主要使用了定期刪除和惰性刪除策略來進行過期key的刪除
定期刪除:每隔一段時間(默認100ms)就隨機抽取一些設置了過期時間的key,檢查其是否過期,如果有過期就刪除。之所以這么做,是為了通過限制刪除操作的執(zhí)行時長和頻率來減少對cpu的影響。不然每隔100ms就要遍歷所有設置過期時間的key,會導致cpu負載太大。惰性刪除:不主動刪除過期的key,每次從數(shù)據(jù)庫訪問key時,都檢測key是否過期,如果過期則刪除該key。惰性刪除有一個問題,如果這個key已經(jīng)過期了,但是一直沒有被訪問,就會一直保存在數(shù)據(jù)庫中。從以上可以知道,Redis過期刪除是不精準的,
在訂單超時處理的場景下,訂單的數(shù)據(jù)如果沒有去訪問,那么惰性刪除基本上也用不到,
所以,無法保證key在過期的時候可以立即刪除,更不能保證能立即通知。
如果訂單量比較大,那么延遲幾分鐘也是有可能的。
總而言之,Redis過期通知也是不可靠的,Redis在過期通知的時候,
如果應用正好重啟了,那么就有可能通知事件就丟了,會導致訂單一直無法關閉,有穩(wěn)定性問題。
▌方案4:超大規(guī)模分布式定時批處理 架構(gòu)
海量訂單過期的 分布式定時批處理解決方案,分為兩步:
step1:通過分布式定時不停輪詢數(shù)據(jù)庫的訂單,將已經(jīng)超時的訂單撈出來
step2:分而治之,分發(fā)給不同的機器分布式處理
在阿里內(nèi)部,幾乎所有的業(yè)務都使用超大規(guī)模分布式定時批處理架構(gòu),大致的架構(gòu)圖如下:
▌超大規(guī)模分布式定時批處理 宏觀架構(gòu)
前段時間指導簡歷,小伙伴簡歷里邊寫了這個項目,但是對這個項目的思想和精髓沒有理解,導致漏洞百出。
接下來,尼恩首先給大家梳理一下 超大規(guī)模分布式定時批處理 宏觀架構(gòu)。
如何讓超時調(diào)度中心不同的節(jié)點協(xié)同工作,拉取不同的數(shù)據(jù)?宏觀的架構(gòu)如下:
▌調(diào)度中心:海量任務調(diào)度執(zhí)行系統(tǒng)
通常的解決方案如
自研分布式調(diào)度系統(tǒng),尼恩曾經(jīng)自研過 基于DB的分布式調(diào)度系統(tǒng)、基于zookeeper 的分布式調(diào)度系統(tǒng)開源的分布式調(diào)度系統(tǒng) 如 xxl-job,阿里巴巴分布式任務調(diào)度系統(tǒng)SchedulerX,不但兼容主流開源任務調(diào)度系統(tǒng),也兼容Spring @Scheduled注解,優(yōu)先推薦,是使用最新版本的、基于時間輪的xxl-job,或者基于xxl-job做定制開發(fā),后面估計尼恩可能會通過對 xxl-job的架構(gòu)和源碼進行介紹。
xxl-job基于 時間輪完成調(diào)度,關于時間輪和 高性能多級時間輪,非常重要,但是內(nèi)容太多,這里不做展開,
具體請看尼恩的3高架構(gòu)筆記《徹底穿透Caffeine底層源碼和架構(gòu)》PDF,里邊介紹了三個超高并發(fā)組件:時間輪、 多級時間輪、條帶環(huán)狀結(jié)構(gòu)、MPSC隊列,大家一定認真看看。
那么在100億級海量訂單超時處理場景中,雖然海量訂單,但是對于調(diào)度系統(tǒng)來說,不是海量的。
因為為了給DB降低壓力,訂單是批量處理的,不可能一個訂單一個延遲任務,而是一大批訂單一個延遲任務。
所以,任務的數(shù)量級,成數(shù)量級的下降。
所以00億級海量訂單超時處理場景的壓力不在調(diào)度,而在于 數(shù)據(jù)庫。
問題的關鍵是如何進行數(shù)據(jù)分片。
▌海量數(shù)據(jù)分片模型
首先來看 DB數(shù)據(jù)的數(shù)據(jù)是如何分片的。 DB數(shù)據(jù)分片模型的架構(gòu),本身場景復雜:
分庫分表場景一張大表場景海量數(shù)據(jù)存儲組件 hbase、hdfs 等等海量數(shù)據(jù)分片模型 要解決問題:
首先是復雜的分片模型的問題其次就是數(shù)據(jù)批處理的過程中,如何減少數(shù)據(jù)傳輸,提升性能的問題。第一個問題:復雜的分片模型的問題。
由于 分片模型與數(shù)據(jù)庫的存儲方案的選型,和分庫分表的設計有關,這里不做展開。
后面給大家講大數(shù)據(jù)的時候,再展開。
咱們的卷王目標是左手大數(shù)據(jù)、右手云原生, 大數(shù)據(jù)是咱們后面卷的重點。
▌輕量級MapReduce模型
第2個問題:如何減少數(shù)據(jù)傳輸。
注意,如果數(shù)據(jù)源在mysql這樣的結(jié)構(gòu)化db,修改訂單的狀態(tài),是可以直接通過結(jié)構(gòu)化sql,進行批量更新的。這個一條sql搞定,是非常簡單的。
但是,既然是海量數(shù)據(jù),就不一定在結(jié)構(gòu)化DB,而是在異構(gòu)的DB(NOSQL)中。
異構(gòu)DB(NOSQL)就沒有辦法通過結(jié)構(gòu)化sql,進行批量更新了。
很多的NOSQL DB的記錄修改,是需要兩步:
step1:把數(shù)據(jù)讀取出來,step2:改完之后,再寫入。所以,如果訂單的數(shù)據(jù)源,不一定是DB,而且在異構(gòu)的DB(NOSQL)中, 那么久存在大量的數(shù)據(jù)傳輸?shù)膯栴}。
100億級訂單規(guī)模,基本上會涉及到異構(gòu)DB,所以阿里的訂單狀態(tài)修改,要求既能兼容 結(jié)構(gòu)化DB,又能兼容異構(gòu) DB, 那么就存在大量的數(shù)據(jù)傳輸問題。
為了減少數(shù)據(jù)傳輸,提升批處理性能的問題。阿里技術(shù)團隊,使用了輕量級MapReduce模型。
▌重量級MapReduce模型
首先看看,什么是MapReduce模型。
傳統(tǒng)的基于hadoop的文件批處理,數(shù)據(jù)在hdfs文件系統(tǒng)中,讀取到內(nèi)存之后,再把結(jié)果寫入到hdfs,這種基于文件的批處理模式,存在大量的數(shù)據(jù)傳輸、磁盤IO,性能太低太低,根本不在目前的這個場景考慮之內(nèi)哈
基于內(nèi)存的批量數(shù)據(jù)處理,比如spark中的離線批量處理,采用的內(nèi)存處理方式,和基于文件的批量處理相比,速度有了質(zhì)的飛越
在spark基于內(nèi)存的批處理流程中(這里簡稱為基于內(nèi)存的MR),首先把數(shù)據(jù)加載到內(nèi)存, 進行map轉(zhuǎn)換、reduce 規(guī)約(或者聚合)之后,再把結(jié)果通過網(wǎng)絡傳輸?shù)较乱粋€環(huán)節(jié)。這里,存在著大家的數(shù)據(jù)傳輸。
所以,不論是內(nèi)存的批處理、還是基于文件批處理,都需要大量的傳輸數(shù)據(jù),
大量的數(shù)據(jù)傳輸,意味著性能太低, 不適用 海量訂單批處理場景。
如何縮減數(shù)據(jù)傳輸?shù)拈|蜜,避免數(shù)據(jù)的大規(guī)模傳輸呢?
▌輕量級MapReduce模型
阿里自研了輕量級MapReduce模型,可以簡稱為本地批處理。
所以輕量級MapReduce模型,就是減少數(shù)據(jù)傳輸,在原來的數(shù)據(jù)庫里邊進行數(shù)據(jù)的計算(比如通過SQL語句),不需要把數(shù)據(jù)加載到內(nèi)存進行計算。
輕量級MapReduce模型在執(zhí)行的過程中,計算節(jié)點主要管理的是 轉(zhuǎn)換、規(guī)約 的分片規(guī)則、執(zhí)行狀態(tài)和結(jié)果,對這些任務中的業(yè)務數(shù)據(jù),計算節(jié)點不去讀取計算的目標數(shù)據(jù),減少了海量業(yè)務數(shù)據(jù)的反復讀取、寫入。
轉(zhuǎn)換階段:
輕量級MapReduce模型,通過實現(xiàn)map函數(shù),通過代碼自行構(gòu)造分片,調(diào)度系統(tǒng)將分片平均分給超時中心的不同節(jié)點分布式執(zhí)行。
注意,在MR集群中,分片由Master主節(jié)點完成,保存在內(nèi)存數(shù)據(jù)庫H2中。工作節(jié)點的調(diào)度,也是Master完成。
▌規(guī)約階段
通過實現(xiàn)reduce函數(shù),可以做聚合,可以判斷這次跑批有哪些分片跑失敗了,從而通知下游處理。
注意,在MR集群中,work的分片任務執(zhí)行結(jié)果匯報到Master,這些結(jié)果保存在內(nèi)存數(shù)據(jù)庫H2中,由Master完成規(guī)約(或聚合)。
有了這個自研了輕量級MapReduce模型,阿里的超時調(diào)度中心可以針對任意異構(gòu)數(shù)據(jù)源,簡單幾行代碼就可以實現(xiàn)海量數(shù)據(jù)秒級別跑批。
▌定時任務分布式批處理的方案的優(yōu)勢:
使用定時任務分布式批處理的方案具有如下優(yōu)勢:
▌穩(wěn)定性強:
基于通知的方案(比如MQ和Redis),比較擔心在各種極端情況下導致通知的事件丟了。使用定時任務跑批,只需要保證業(yè)務冪等即可,如果這個批次有些訂單沒有撈出來,或者處理訂單的時候應用重啟了,下一個批次還是可以撈出來處理,穩(wěn)定性非常高。▌效率高:
基于MQ的方案,需要一個訂單一個定時消息,consumer處理定時消息的時候也需要一個訂單一個訂單更新,對數(shù)據(jù)庫tps很高。使用定時任務跑批方案,一次撈出一批訂單,處理完了,可以批量更新訂單狀態(tài),減少數(shù)據(jù)庫的tps。在海量訂單處理場景下,批量處理效率最高。▌可運維:
基于數(shù)據(jù)庫存儲,可以很方便的對訂單進行修改、暫停、取消等操作,所見即所得。如果業(yè)務跑失敗了,還可以直接通過sql修改數(shù)據(jù)庫來進行批量運維。▌成本低:
相對于其他解決方案要借助第三方存儲組件,復用數(shù)據(jù)庫的成本大大降低。▌定時任務分布式批處理的方案的缺點:
但是使用定時任務有個天然的缺點:沒法做到精度很高。
定時任務的延遲時間,由定時任務的調(diào)度周期決定。
如果把頻率設置很小,就會導致數(shù)據(jù)庫的qps比較高,容易造成數(shù)據(jù)庫壓力過大,從而影響線上的正常業(yè)務。
解決方案就是DB解耦:
阿里內(nèi)部,一般需要解耦單獨超時庫,單獨做訂單的超時調(diào)度,不會和業(yè)務庫在在一起操作。
同時也有獨立的超時中心,完成 數(shù)據(jù)的分片,以及跑批任務的調(diào)度。
▌訂單任務調(diào)度場景的選型:
(1)超時精度比較高、超時任務不會有峰值壓力的場景
如果對于超時精度比較高,不會有峰值壓力的場景,推薦使用RocketMQ的定時消息解決方案。
(2)超時精度比較低、超時任務不會有峰值壓力的場景(100億級訂單)
在電商業(yè)務下,許多訂單超時場景都在24小時以上,對于超時精度沒有那么敏感,并且有海量訂單需要批處理,推薦使用基于定時任務的跑批解決方案。
阿里的100億級訂單超時處理選型,選擇的是后面的方案。
▌架構(gòu)的魅力:
通過以上的梳理,大家如果需要把 海量任務定時調(diào)度的方案寫入簡歷,再也不會一半清晰、一半糊涂了。
如果還不清楚怎么寫入簡歷,可以來找尼恩進行簡歷指導。保證 脫胎換骨、金光閃閃、天衣無縫。
總之,架構(gòu)魅力,在于沒有最好的方案,只有更好的方案。
大家如果有疑問,或者更好的方案,可以多多交流,此題,后面的答案,也會不斷的完善和優(yōu)化。
注:本文以 PDF 持續(xù)更新,最新尼恩 架構(gòu)筆記、面試題 的PDF文件,請到《技術(shù)自由圈》公號領取
▌技術(shù)自由的實現(xiàn)路徑 PDF領?。?/strong>
▌實現(xiàn)你的架構(gòu)自由:
《吃透8圖1模板,人人可以做架構(gòu)》PDF《10Wqps評論中臺,如何架構(gòu)?B站是這么做的?。?!》PDF《阿里二面:千萬級、億級數(shù)據(jù),如何性能優(yōu)化? 教科書級 答案來了》PDF《峰值21WQps、億級DAU,小游戲《羊了個羊》是怎么架構(gòu)的?》PDF《100億級訂單怎么調(diào)度,來一個大廠的極品方案》PDF《2個大廠 100億級 超大流量 紅包 架構(gòu)方案》PDF… 更多架構(gòu)文章,正在添加中
▌實現(xiàn)你的 響應式 自由:
《響應式圣經(jīng):10W字,實現(xiàn)Spring響應式編程自由》PDF這是老版本 《Flux、Mono、Reactor 實戰(zhàn)(史上最全)》PDF▌實現(xiàn)你的 spring cloud 自由:
《Spring cloud Alibaba 學習圣經(jīng)》 PDF《分庫分表 Sharding-JDBC 底層原理、核心實戰(zhàn)(史上最全)》PDF《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關系(史上最全)》PDF▌實現(xiàn)你的 linux 自由:
《Linux命令大全:2W多字,一次實現(xiàn)Linux自由》PDF▌實現(xiàn)你的 網(wǎng)絡 自由:
《TCP協(xié)議詳解 (史上最全)》PDF《網(wǎng)絡三張表:ARP表, MAC表, 路由表,實現(xiàn)你的網(wǎng)絡自由!!》PDF▌實現(xiàn)你的 分布式鎖 自由:
《Redis分布式鎖(圖解 - 秒懂 - 史上最全)》PDF《Zookeeper 分布式鎖 - 圖解 - 秒懂》PDF▌實現(xiàn)你的 王者組件 自由:
《隊列之王: Disruptor 原理、架構(gòu)、源碼 一文穿透》PDF《緩存之王:Caffeine 源碼、架構(gòu)、原理(史上最全,10W字 超級長文)》PDF《緩存之王:Caffeine 的使用(史上最全)》PDF《Java Agent 探針、字節(jié)碼增強 ByteBuddy(史上最全)》PDF▌實現(xiàn)你的 面試題 自由:
4000頁《尼恩Java面試寶典》PDF 40個專題....
注:以上尼恩 架構(gòu)筆記、面試題 的PDF文件,請到《技術(shù)自由圈》公號領取
還需要啥自由,可以告訴尼恩。 尼恩幫你實現(xiàn).......
以上就是關于pos機接收返回超時,100億訂單超時處理的知識,后面我們會繼續(xù)為大家整理關于pos機接收返回超時的知識,希望能夠幫助到大家!
