這是why哥的第 76 篇原創文章前端
前段時間一個在深圳的,兩年經驗的小夥伴出去面試了一圈,收割了幾個大廠 offer 的同時,還總結了一下面試的過程當中遇到的面試題,面試題有不少,文末的時候我會分享給你們。java
此次的文章主要分享他面試過程當中遇到的一個場景題:web
他說對於這個場景題,面試的時候沒有什麼思路。面試
說真的,請求合併我知道,高併發無非就是快速的請求合併。redis
可是在我有限的認知裏面,若是相似於秒殺的高併發扣庫存這個場景,用請求合併的方式來作,我我的感受是有點怪怪的不夠傳統。算法
在傳統的,或者說是業界經常使用的秒殺解決方案中,從前端到後臺,你也找不到請求合併的字樣。sql
我理解請求合併更加適用的場景是查詢類的,或者說是數值增長類的需求,對於庫存扣減這種,你稍不留神,就會出現超賣的狀況。數據庫
固然也有多是我理解錯題意了,看到高併發扣庫存就想到秒殺場景了。後端
可是不重要,咱們也不能直接和麪試官硬剛。設計模式
我會從新給個我以爲合理的場景,告訴你們我理解的請求合併和高併發下的請求合併是什麼玩意。
如今咱們拋開秒殺這個場景。
換一個更加合適,你們可能更容易理解的場景來聊聊什麼是請求合併。
就是熱點帳戶。
什麼是熱點帳戶呢?
在第三方支付系統或者銀行這類交易機構中,每產生一筆轉入或者轉出的交易,就須要對交易涉及的帳戶進行記帳操做。
記帳通常來講涉及到兩個部分。
若是對於某個帳戶操做很是的頻繁,那麼當咱們對帳戶餘額進行操做的時候,就會涉及到併發處理的問題。
併發了怎麼辦?
是的,咱們能夠對帳戶進行加鎖處理。這樣一來,這個帳戶就涉及到頻繁的加鎖解鎖操做。
這樣咱們能夠保證數據不出問題,可是隨之帶來的問題是隨着併發的提升,帳戶系統性能降低。
這個帳戶,就是熱點帳戶,就是性能瓶頸點。
熱點帳戶是業界的一個很是常見的問題。
我所瞭解到的常規解決方案大概能夠分爲三種:
本小節主要是介紹「多筆合一記帳」解決方案,從而引出請求合併的機率。
對於另外兩個解決方案,就先簡單的說一下。
首先異步緩衝記帳。
我先不解釋,你就看着這個名字,想着這個場景,你以爲你會想到什麼?
異步,是否是想到了 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 就有請求合併的功能。給你們演示一下。
假設咱們有一個學生信息查詢接口,調用頻率很是的高。對於這個接口咱們須要作請求合併處理。
作請求合併,咱們至少對應着兩個接口,一個是接收單個請求的接口,一個處理把單個請求彙總以後的請求接口。
因此咱們須要先提供兩個 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基礎
JVM相關
Redis相關
SQL相關
Spring相關
Dubbo相關
分佈式相關
設計模式
Zookeeper
MQ
計算機網絡
Tomcat
代碼
場景問題
說來慚愧,有些題我也答不上來,因此和你們一塊兒查漏補缺吧。
哦,對了,那個小夥子最終收割了好幾個大廠 offer,跑來問我哪一個 offer 好。
你說這問題對我來講那不是超綱了嗎?我也沒在大廠體驗過啊。因此我懷疑他不講武德,來騙,來偷襲我這個老實巴交的小號主,我但願他能耗子尾汁,在鵝廠好好發展:
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠提出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。
還有,歡迎關注我呀。