pos機接收返回超時,100億訂單超時處理

 新聞資訊2  |   2023-05-21 11:49  |  投稿人:pos機之家

網(wǎng)上有很多關于pos機接收返回超時,100億訂單超時處理的知識,也有很多人為大家解答關于pos機接收返回超時的問題,今天pos機之家(www.shineka.com)為大家整理了關于這方面的知識,讓我們一起來看下吧!

本文目錄一覽:

1、pos機接收返回超時

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機接收返回超時的知識,希望能夠幫助到大家!

轉(zhuǎn)發(fā)請帶上網(wǎng)址:http://www.shineka.com/newsone/50996.html

你可能會喜歡:

版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關法律責任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 babsan@163.com 舉報,一經(jīng)查實,本站將立刻刪除。