面試官問我:什麼是高併發下的請求合併?

這是why哥的第

76

篇原創文章

從一道面試題說起

前段時間一個在深圳的,兩年經驗的小夥伴出去面試了一圈,收割了幾個大廠 offer 的同時,還總結了一下面試的過程中遇到的面試題,面試題有很多,文末的時候我會分享給大家。

這次的文章主要分享他面試過程中遇到的一個場景題:

面試官問我:什麼是高併發下的請求合併?

他說對於這個場景題,面試的時候沒有什麼思路。

說真的,請求合併我知道,高併發無非就是快速的請求合併。

但是在我有限的認知裡面,如果類似於秒殺的高併發扣庫存這個場景,用請求合併的方式來做,我個人感覺是有點怪怪的不夠傳統。

在傳統的,或者說是業界常用的秒殺解決方案中,從前端到後臺,你也找不到請求合併的字樣。

我理解請求合併更加適用的場景是查詢類的,或者說是數值增加類的需求,對於庫存扣減這種,你稍不留神,就會出現超賣的情況。

當然也有可能是我理解錯題意了,看到高併發扣庫存就想到秒殺場景了。

但是不重要,我們也不能直接和麵試官硬剛。

面試官問我:什麼是高併發下的請求合併?

我會重新給個我覺得合理的場景,告訴大家我理解的請求合併和高併發下的請求合併是什麼玩意。

請求合併

現在我們拋開秒殺這個場景。

換一個更加合適,大家可能更容易理解的場景來聊聊什麼是請求合併。

就是熱點賬戶。

什麼是熱點賬戶呢?

在第三方支付系統或者銀行這類交易機構中,每產生一筆轉入或者轉出的交易,就需要對交易涉及的賬戶進行記賬操作。

記賬一般來說涉及到兩個部分。

交易系統記錄這一筆交易的資訊。

賬戶系統需要增加或減少對應的賬戶餘額。

如果對於某個賬戶操作非常的頻繁,那麼當我們對賬戶餘額進行操作的時候,就會涉及到併發處理的問題。

併發了怎麼辦?

是的,我們可以對賬戶進行加鎖處理。這樣一來,這個賬戶就涉及到頻繁的加鎖解鎖操作。

這樣我們可以保證資料不出問題,但是隨之帶來的問題是隨著併發的提高,賬戶系統性能下降。

這個賬戶,就是熱點賬戶,就是效能瓶頸點。

熱點賬戶是業界的一個非常常見的問題。

我所瞭解到的常規解決方案大概可以分為三種:

非同步緩衝記賬。

設立影子賬戶。

多筆合一記賬。

本小節主要是介紹“多筆合一記賬”解決方案,從而引出請求合併的機率。

對於另外兩個解決方案,就先簡單的說一下。

首先非同步緩衝記賬。

我先不解釋,你就看著這個名字,想著這個場景,你覺得你會想到什麼?

非同步,是不是想到了 MQ?

那麼請問你係統裡面為什麼要引入 MQ 呢?

來,面試八股文背起來:

非同步處理、系統解耦、削峰填谷。

你說我們當前的這個場景下屬於哪一種情況?

肯定是為了做削峰填谷呀。

假設賬務系統的 TPS 是 200 筆每秒,當請求低於 200 筆每秒的時候,賬務服務基本上能夠及時處理馬上返回。

從使用者的角度來說就是:啪的一下,很快啊。我就收到了記賬成功的通知了,也看到賬戶餘額發生了變化。

但是在業務高峰期的時候,流量直接翻倍,每秒過來了 400 筆請求,這個時候對於賬務系統來說就是流量洪峰,需要進行削峰了,佇列裡面開始堆積著請求,開始排隊處理了。

在流量低谷的時候,就可以把這部分資料消費完成。

相當於資料扔到佇列裡面之後,就可以告訴使用者記賬成功了,錢馬上就到。

但是這個方案帶來的問題也是很明顯的,如果流量真的爆了,一天都沒有谷讓你填,佇列裡面堆積著大量的請求還沒來得及處理,你怎麼辦?

這對於使用者而言就是:你明明告訴我記賬成功了,為什麼我的賬戶餘額遲遲沒有變化呢?是不是想陰我錢,我反手就是一波投訴。

另外一個風險點就是對於支出類的請求,如果被削峰,很明顯,我們提前就告訴了使用者操作成功,但是真正動賬戶餘額的時候已經延遲了,所以可能會出現賬戶透支的情況。

另外一個設立影子賬戶的方案,其實和我們本次的請求合併的主題是另外一個不同的方向。

它的思想是拆分。

熱點賬戶說到底還是一個單點問題,那麼對於單點問題,我們用微服務的思想去解決的話是什麼方案?

就是拆分。

假設這個熱點賬戶上有 100w,我設立 10 個影子賬戶,每個賬戶 10w ,那麼是不是我們的流量就分散了?從一個賬戶變成了 10 個賬戶。

壓力也就進行了分攤。

這個方案就有點類似於秒殺場景中的庫存了,庫存我們也可以拆多份。

但是帶來的問題也很明顯。

一是獲取賬戶餘額的時候需要進行彙總操作。

二是假設使用者要扣 11w 呢?我們總餘額是夠的,但是每個影子賬戶上的錢是不夠的。

三是你的影子賬戶選擇的演算法是很重要的,是用隨機?輪訓?加權?這些對於賬務成功率都是有比較大的影響的。

另外這個思想,我在之前的文章中也提到過,有興趣的可以看看其在 JDK 原始碼中的應用:我從LongAdder中窺探到了高併發的秘籍,上面只寫了兩個字。。。

好了,回到本次的主題:多筆合一筆記賬。

有個網紅店,生意非常的好,每天很多人在店裡面消費。

當用戶掃碼支付後,請求會發送到這個店對接的第三方支付公司。

當支付公司收到請求,並完成記賬操作後才會告知商戶使用者支付成功。可以給使用者商品了。

面試官問我:什麼是高併發下的請求合併?

隨著店裡生意越來越好,帶來的問題是第三方支付公司的系統壓力增加,扛不住這麼大的併發了。導致使用者支付成功率的下降或者使用者支付成功後很長時間才通知到商戶。

那麼針對這個商戶的賬戶,我們就可以做多筆合一筆處理。

當記錄進入緩衝流水記錄表之後,我們就可以通知商戶使用者支付成功了,至於錢,你放心,我有定時任務,一會就到賬:

面試官問我:什麼是高併發下的請求合併?

所以當用戶下單之後,我們只是先記錄資料,並不去實際動賬戶。等著定時任務去觸發記賬,進行多筆合併一筆的操作。

比如下面的這個示意圖:

面試官問我:什麼是高併發下的請求合併?

商戶實際有 5 個使用者支付記錄,但是這 5 筆記錄對應著一條賬戶流水。我們拿著賬戶流水,也是可以追溯到這 5 筆交易記錄的。

這樣的好處是吞吐量上來了,通知及時,使用者體驗也好了。但是帶來的弊端是餘額並不是一個準確的值。

假設我們的定時任務是一小時彙總一次,那麼商戶在後端看到的交易金額可能是一小時之前的資料。

而且這種方案對於賬戶收錢的場景非常的適合,但是減錢的場景,也是有可能會出現金額為負的情況。

不知道你有沒有看出多筆合一筆處理方案的秘密。

如果我們把緩衝流水記錄表看作是一個佇列。那麼這個方案抽象出來就是佇列加上定時任務。

所以,

請求合併的關鍵點也是佇列加上定時任務

文章看到現在,請求合併我們應該是大概的瞭解到了,也確實是有真實的應用場景。

除了我上面的例子外,比如還有 redis裡面的 mget,資料庫裡面的批次插入,這玩意不就是一個請求合併的真實場景嗎?

比如 redis 把多個 get 合併起來,然後呼叫 mget。多次請求合併成一次請求,節約的是網路傳輸時間。

還有真實的案例是轉賬的場景,有的轉賬渠道是按次收費的,那麼作為第三方公司,我們就可以把使用者的請求先放到表裡記錄著,等一小時之後,一起彙總發起,假設這一小時內發生了 10 次轉賬,那麼 10 次收費就變成了 1 次收費,雖然讓客戶等的稍微久了點,但還是在可以接受的範圍內,這操作節約的就是真金白銀了。

高併發的請求合併

理解了請求合併,那我們再來說說當他前面加上高併發這三個字之後,會發生什麼變化。

首先不論是在請求合併的前面加上多麼狂拽炫酷吊炸天的形容詞,說的多麼的天花亂墜,它也還是一個請求合併。

那麼佇列和定時任務的這個基礎結構肯定是不會變的。

高併發的情況下,就是請求量非常的大嘛,那我們把定時任務的頻率調高一點不就行了?

以前 100ms 內就會過來 50 筆請求,我每收到一筆就是立即處理了。

現在我們把請求先放到佇列裡面快取著,然後每 100ms 就執行一次定時任務。

100ms 到了之後,就會有定時任務把這 100ms 內的所有請求取走,統一處理。

同時,我們還可以控制佇列的長度,比如只要 50ms 佇列的長度就達到了 50,這個時候我也進行合併處理。不需要等待到 100ms 之後。

其實寫到這裡,高併發的請求合併的答案已經出來了。關鍵點就三個:

一是需要藉助佇列加定時任務實現。

二是控制定時任務的執行時間。

三是控制緩衝佇列的任務長度。

方案都想到了,把程式碼寫出來豈不是很容易的事情。而且對於這種面試的場景圖,一般都是討論技術方案,而不太會去討論具體的程式碼。

當討論到具體的程式碼的時候,要麼是對你的方案存疑,想具體的探討一下落地的可行性。要麼就是你答對了,他要準備從程式碼的交易開始衍生另外的面試題了。

總之,大部分情況下,不會在你給了一個面試官覺得錯誤的方案之後,他還和你討論程式碼細節。你們都不在一個頻道了,趕緊換題吧,還聊啥啊。

實在要往程式碼實現上聊,那麼大機率他是在等著你說出一個框架:Hystrix。

Hystrix框架

其實這題,你要是知道 Hystrix,很容易就能給出一個比較完美的回答。

因為 Hystrix 就有請求合併的功能。給大家演示一下。

假設我們有一個學生資訊查詢介面,呼叫頻率非常的高。對於這個介面我們需要做請求合併處理。

做請求合併,我們至少對應著兩個介面,一個是接收單個請求的介面,一個處理把單個請求彙總之後的請求介面。

所以我們需要先提供兩個 service:

面試官問我:什麼是高併發下的請求合併?

其中根據指定 id 查詢的介面,對應的 Controller 是這樣的:

面試官問我:什麼是高併發下的請求合併?

服務啟動起來後,我們用執行緒池結合 CountDownLatch 模擬 20 個併發請求:

面試官問我:什麼是高併發下的請求合併?

從控制檯可以看到,瞬間接受到了 20 個請求,執行了 20 次查詢 sql:

面試官問我:什麼是高併發下的請求合併?

很明顯,這個時候我們就可以做請求合併。每收到 10 次請求,合併為一次處理,結合 Hystrix 程式碼就是這樣的,為了程式碼的簡潔性,我採用的是註解方式:

面試官問我:什麼是高併發下的請求合併?

在上面的圖片中,有兩個方法,一個是 getUserId,直接返回的是null,因為這個方法體不重要,根本就不會執行。

在 @HystrixCollapser 裡面可以看到有一個 batchMethod 的屬性,其值是 getUserBatchById。

也就是說這個方法對應的批次處理方法就是 getUserBatchById。當我們請求 getUserById 方法的時候,Hystrix 會透過一定的邏輯,幫我們轉發到 getUserBatchById 上。

所以我們呼叫的還是 getUserById 方法:

面試官問我:什麼是高併發下的請求合併?

同樣,我們用執行緒池結合 CountDownLatch 模擬 20 個併發請求,只是變換了請求地址:

面試官問我:什麼是高併發下的請求合併?

呼叫之後,神奇的事情就出現了,我們看看日誌:

面試官問我:什麼是高併發下的請求合併?

同樣是接受到了 20 個請求,但是每 10 個一批,只執行了兩個sql語句。

從 20 個 sql 到 2 個 sql,這就是請求合併的威力。請求合併的處理速度甚至比單個處理還快,這也是效能的提升。

那假設我們只有 5 個請求過來,不滿足 10 個這個條件呢?

別忘了,我們還有定時任務呢。

在 Hystrix 中,定時任務預設是每 10ms 執行一次:

面試官問我:什麼是高併發下的請求合併?

同時我們可以看到,如果不設定 maxRequestsInBatch,那麼預設是 Integer。MAX_VALUE。

也就是說,在 Hystrix 中做請求合併,它更加側重的是時間方面。

功能演示,其實就這麼簡單,程式碼量也不多,有興趣的朋友可以直接搭個 Demo 跑跑看。看看 Hystrix 的原始碼。

我這裡只是給大家指幾個關鍵點吧。

第一個肯定是我們需要找到方法入口。

你想,我們的 getUserById 方法的方法體裡面直接是 return null,也就是說這個方法體是什麼根本就不重要,因為不會去執行方法體中的程式碼。它只需要攔截到方法入參,並快取起來,然後轉發到批次方法中去即可。

然後方法體上面有一個 @HystrixCollapser 註解。

那麼其對應的實現方式你能想到什麼?

肯定是 AOP 了嘛。

所以,我們拿著這個註解的全路徑,進行搜尋,啪的一下,很快啊,就能找到方法的入口:

com。netflix。hystrix。contrib。javanica。aop。aspectj。HystrixCommandAspect#methodsAnnotatedWithHystrixCommand

面試官問我:什麼是高併發下的請求合併?

在入口處打上斷點,就可以開始除錯了:

面試官問我:什麼是高併發下的請求合併?

第二個我們看看定時任務是在哪兒進行註冊的。

這個就很好找了。我們已經知道預設引數是 10ms 了,只需要順著鏈路看一下,哪裡的程式碼呼叫了其對應的 get 方法即可:

面試官問我:什麼是高併發下的請求合併?

同時,我們可以看到,其定時功能是基於java。util。concurrent。ScheduledThreadPoolExecutor#scheduleAtFixedRate實現的。

第三個我們看看是怎麼控制超過指定數量後,就不等待定時任務執行,而是直接發起彙總操作的:

面試官問我:什麼是高併發下的請求合併?

可以看到,在com。netflix。hystrix。collapser。RequestBatch#offer方法中,當 argumentMap 的 size 大於我們指定的 maxBatchSize 的時候返回了 null。

如果,返回為 null ,那麼說明已經不能接受請求了,需要立即處理,程式碼裡面的註釋也說的很清楚了:

面試官問我:什麼是高併發下的請求合併?

以上就是三個關鍵的地方,Hystrix 的原始碼讀起來,需要下點功夫,大家自己研究的時候需要做好心理準備。

最後再貼一個官方的請求合併工作流程圖:

面試官問我:什麼是高併發下的請求合併?

打完收工。

面試題

前面說的深圳的,兩年經驗的小夥伴把面試題彙總了一份給我,我也分享給大家吧。

Java基礎

volatile關鍵字底層原理

執行緒池各個引數含義

lock、synchronized區別

ReentrantLock鎖公平與非公平實現、重入原理

HashMap擴容時機(容量初始化為1000和10000是否觸發擴容)、機制、1。7與1。8的差異

ConcurrentHashMap1。7、1。8的最佳化與差異,size方法實現差異

ThreadLocal原理與風險、為什麼會記憶體洩露

阻塞佇列的用途、區別

LinkedBlockingQueue佇列的add、put區別,實際過程中如何使用

悲觀鎖、樂觀鎖、自旋鎖的使用場景、實現方式、優缺點

Class。forName、loadClass區別;

執行緒生命週期、死鎖條件與死鎖避免、狀態轉換關係(原始碼級別);

String intern方法;

cas的優缺點與解決方案、ABA問題;

JVM相關

CMS垃圾回收的碎片解決方式

常用的垃圾回收器

JVM垃圾回收器CMS的優缺點、與G1的區別、進入老年代的時機

JVM記憶體模型

JVM調優思路

GC Root、ModUnionTable

偏向鎖、輕量級鎖、重量級鎖底層原理、升級過程

jmap、jstat、top、MAT

CMS與G1對別

GC Root、ModUnionTable;

Redis相關

Redis高效能原因

Redis的部署模式

RedisCluster底層原理

Redis持久化機制

快取淘汰機制

快取穿透、快取雪崩、快取擊穿發生場景與解決方案

SQL相關

MyBatis攔截器的用途

MyBatis動態SQL原理

分庫分表方案設計

MySQL怎麼解決幻讀、原理(原始碼級別)

Gap鎖的作用域原理

RR、RC區別

MySQL預設的事務隔離級別、Oracle預設的事務隔離級別

MySQL為啥使用B+樹索引

redo log、binlog、undo log寫入順序、分別保證了ACID的什麼特性

資料庫樂觀鎖

MySQL最佳化

MySQL底層原理

Spring相關

@Bean註解、@Component註解區別

Spring Aop原理

@Aspect和普通AOP區別

自定義攔截器和Aop那個先執行

web 攔截器

DispatchServlet原理

Dubbo相關

Dubbo負載均衡、叢集容錯

Dubbo SPI機制、Route重寫使用場景

Dubbo RPC底層原理

全鏈路監控實現原理

分散式相關

分散式鎖的實現方式

漏斗演算法、令牌桶演算法

事務最終一致性解決方案

SLA

分散式事務實現方式與區別

Tcc Confirm失敗怎麼辦?

分散式鎖的各種實現方式、對比

分散式ID的各種實現方式、對比

雪花演算法時鐘回撥問題與應對方案

紅鎖演算法

設計模式

常用的設計模式

狀態模式

責任鏈模式解決了什麼問題

餓漢式、懶漢式優缺點、使用場景

模板方法模式、策略模式、單例模式、責任鏈模式

Zookeeper

Zookeeper底層架構設計

zk一致性

MQ

Kafka順序訊息

MQ訊息冪等

Kafka高效能秘訣

Kafka高吞吐原理

Rocket事務訊息、延時佇列

計算機網路

瀏覽器輸入一個url發生了什麼

Http 1。0、1。1、2。0差異

IO多路複用

TCP四次揮手過程、狀態切換

XSS、CRSF攻擊與預防

301、302區別

Tomcat

Tomcat大概原理

程式碼

手寫釋出訂閱模式

大數(兩個String)相加

場景問題

打賞排行榜實現

高併發下的請求合併

CPU 100%處理經驗

短鏈系統設計

附近的人專案實現

10w個紅包秒級傳送方案

延時任務的實現方案與優缺點對比

說來慚愧,有些題我也答不上來,所以和大家一起查漏補缺吧。

哦,對了,那個小夥子最終收割了好幾個大廠 offer,跑來問我哪個 offer 好。

你說這問題對我來說那不是超綱了嗎?我也沒在大廠體驗過啊。所以我懷疑他不講武德,來騙,來偷襲我這個老實巴交的小號主,我希望他能耗子尾汁,在鵝廠好好發展:

面試官問我:什麼是高併發下的請求合併?

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,可以提出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被程式碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。

還有,歡迎關注我呀。