最近很火的京東搶購飛天茅臺是怎么回事?從架構原理來分析一波
背景
大家好,這篇文章給大家介紹一個非常經典的去大廠面試經常被問的一個問題,就是瞬時高并發搶購問題。
通常來說,大廠開發的系統經常會遇到一些類似電商秒殺搶購、景點門票高并發搶購、特殊商品(比如口罩)高并發搶購、類似 12306 的高并發搶票類的系統。
所以經常會問這一類高并發搶購類的問題,這個時候,小伙伴們如果不能有理有據的給出一整套高并發場景下系統可能遇到的各種問題,以及你對應的架構設計和解決方案,那基本面試可能就會涼掉。
所以今天就手把手帶著大家來分析一下,假設在特殊物品庫存緊缺的場景下,1 分鐘內要搶購 10w 個口罩這類特殊物品,此時可能有數十萬人這個量級瞬時涌入來進行搶購,這個時候系統可能會遇到哪些問題,我們應該如何來設計架構解決這類問題呢?
業務架構設計
首先在分析這一類問題的時候,我們先不要考慮這個瞬時高并發到底有多高,先得把實現購買這類特殊商品的一個基礎業務架構圖畫出來,同時把業務流程分析清楚。
大家看下圖,如果你要搞一個商品搶購的系統,肯定得有一個搶購系統,這個搶購系統你得依賴商品系統吧,畢竟搶購過程中需要對商品數據進行讀寫,你還得依賴庫存系統進行庫存扣減,同時你還得依賴價格系統來計算當前商品的購買價格,還得依賴營銷系統來驗證商品購買的優惠。
最后還得依賴鑒權認證、風控攔截類的基礎系統來確定本次搶購是否可以執行,所以說,一次搶購涉及到的各種系統其實是很多的,完整的基礎高并發搶購系統基礎業務架構圖。
如下圖 1 所示:
網絡拓撲架構設計
另外的話,大家還得對你的搶購請求是如何一步一步到達你的搶購系統的,這個事情流程大家也是要畫出來的。
一般來說,我們的 APP 移動端對后端訪問都是通過一個域名來發起請求的,這個域名會經過 DNS 進行解析得到我們的 SLB 負載均衡系統的 ip 地址。
然后請求會發送到我們的 SLB 負載均衡系統上去,接著 SLB 負載均衡系統會把請求均勻分發給我們后端的 API 網關系統,然后 API 網關系統再把流量分發給我們的搶購系統。
所以大致如下圖 2 所示:
好的,當大家能當著面試官的面,麻溜兒的把上面那套業務架構圖和生產部署網絡拓撲圖大致畫出來以后,我們可以跟大家保證,雖然這個時候面試官看起來面無表情,但是心里的真實反映應該是這樣的:小兄弟可以啊,一般人聽到這個問題就直接懵逼了,這小子居然知道先從業務架構和網絡拓撲架構入手進行分析。
但是大家別高興的太早,距離你圓滿的完成這個問題的分析,大致是才剛剛走完了西游記十萬八千里中的八千里而已,剩下的十萬還要繼續走呢!這一路上大家馬上要遇到各種妖魔鬼怪了!打起精神,接著一起來往下看。
秒殺業務流量洪峰
往往到這里,我們下一步應該分析的,就是日常流量和搶購流量的區別了,什么意思呢?
先來說說日常流量,這個意思就是說,平時沒有搶購的時候,就是別人正常來買各種商品,系統的大致流量應該是每秒會有多少請求。
這個問題的話,不大好說,因為不同的公司其實是不太一樣的,但是我們可以取一個較為中間的值,整個系統日常的話每秒也就 1000 次請求,這個是比較中肯的一個值,不高也不低。
如下圖 3 所示:
一般來說,但凡你的搶購系統以及他依賴的每個系統部署在 2 臺機器以上,每秒 1000 次請求這種常規流量,各個系統兄弟們同心協力,一起扛一抗,還是沒太大問題的。
但是如果說搞這么一個活動,某個特殊商品,限量 10w 份,大家又特別需要他,然后呢,限定就是每天上午 10:00 開搶,每次都有幾十萬人眼睛放出紅光盯著手機屏幕準備搶他,志在必得,這個時候,流量會搞成什么樣子呢?
注意,重頭戲來了,大體上來說,根據一般的搶購經驗,往往你的 10w 件商品會在 1 分鐘內搶光,而且根據二八法則,80% 的商品會在 20% 的時間內被搶光。
也就是說 8w 件商品可能會在 10s 內被搶購,而且參與搶購這 8w 件商品的流量達到了 80% 的人群數量,假設一共有 50w 人參與搶購,就是有 40w 人在 10s 內發起搶購請求,搶光了 8w 件商品。
這個時候,每秒的請求數量應該是 40w/10s = 4w/s 的 QPS,大家看下圖 4:
不知道大家看到上圖是何感想?腦子別發蒙啊,面試官聽得津津有味,咱們趕緊繼續往下講啊,不然你這時候停下來,你們會大眼瞪小眼的!那這個時候如果對你的搶購系統發起的請求量達到了每秒 4w,大家覺得會如何呢?
很簡單,系統絕對會被打死,網絡帶寬打滿、CPU 使用率達到 90% 多、數據庫負載過高、下游依賴頻繁超時,這一切問題都可能會發生,你要問為什么?
那就是因為你的系統常規化部署下,就是抗每秒 1000 的請求的,他們又不是設計來抗你每秒 4w 請求的。
架構設計優化
所以這個時候問題就牽扯到了一個點,那就是怎么才能讓你的搶購系統可以抗下來每秒 4w 請求呢?
為了解決這個問題,就得趁著面試官打瞌睡的時候,咱兄弟偷偷給你傳授一點武林秘籍了。
正常情況下,一臺 4 核 8G 的機器,開 200 個線程處理請求,如果他要調用別的服務,或者是訪問數據庫,基本上每秒單臺機器也就抗個 1000 的請求量。
并發搶購系統性能瓶頸分析
但是,注意,敲黑板劃重點了,不是說你的 4 核 8G 機器就菜雞到了只能抗每秒 1000 個請求,他的關鍵問題在于,他要調用別的服務,而且他還要訪問數據庫,就是因為這種通過網絡去訪問外部系統,才導致了他每秒抗的請求量比較菜雞一些。
大家看下圖 5:
大家要知道一點,類似 Redis、RocketMQ 這種中間件系統,經過深度優化之后,往往單臺抗個上萬甚至幾萬 QPS 都沒問題,所謂的深度優化是什么意思?
簡而言之就一點,你最好就是每次請求過來,完全就基于自己的內存來讀寫數據,然后就直接返回了。
不要隨便通過網絡去訪問外部的系統,這種情況下,往往你的并發量可以提升幾個數量級。
如下圖 6 所示:
并發搶購系統架構優化
所以說,一般這種場景下,有三個非常強悍的優化手段,那就是大幅度減少對外部服務的依賴調用嗎;寫數據盡量直接寫緩存,然后異步寫 DB;讀數據盡量優先把數據緩存在系統 JVM 內存里,本地讀取返回。
這里可以給大家舉一些例子,比如說,對于特殊商品固定價格搶購,那么對價格系統、營銷系統的調用是否就可以省略了,畢竟價格固定,也沒有優惠這一說。
對于風控和鑒權類的通用操作,是否可以前置到 API 網關層面讓他去執行,從我們的業務系統里移除這類通用邏輯?這不就一下子減少了對 4 個系統的調用了。
再比如說,對庫存的扣減,是否可以讓庫存系統把數據同步到 Redis 里,我們直接同步扣 Redis 里的庫存,然后發 MQ 消息異步去庫存系統的 DB 里扣庫存?
還有比如對商品數據的大量查詢,是否可以將商品數據緩存到 Redis 里,同時對熱門商品數據全部提前加載到搶購系統的 JVM 內存里本地緩存?
經過優化后的搶購系統大致看起來是下面圖 7 這樣子的:
大家看上圖,這個時候經過一通優化之后,我們的搶購系統已經不再直接調用任何服務了。
他在讀商品數據的時候,優先都是從自己的 JVM 本地緩存里讀取預緩存的數據,幾乎就是純內存操作,然后扣減庫存是去寫 Redis 的,對于庫存系統甚至是訂單系統的數據庫中的扣減庫存和下單,都是通過 MQ 異步化執行的。
基本上系統優化到這個水準,主要給搶購系統多部署幾臺機器,就可以抗下每秒幾萬高并發的請求了。
但是這個時候完了嗎?當然沒有,這個時候系統里存在的問題還非常的多,我們得繼續往下分析,進一步一步一步的優化。
①高并發搶購系統緩存擊穿問題分析與解決方案
首先,分析第一個問題,就是商品數據緩存在搶購系統 JVM 本地緩存時的擊穿問題,我們在搶購系統的 JVM 本地緩存中放的數據,一般都是要設置一個過期時間的,因為如果你一直緩存在 JVM 里,會導致商品數據有變化了,你也不知道。
所以假設我們設置一個 30min 的過期時間,每隔 30min 過期下,過期之后,搶購系統就得去 Redis 里查商品數據緩存,如果沒查到,那就得去調用商品系統的接口從數據庫里查了。
如下圖 8:
那么當你的搶購系統里的本地緩存過期了,此時本地緩存沒數據了,然后 Redis 里緩存可能此時也沒有的時候,就在這個非常要緊的關頭,偏偏就進來了大量的請求,此時這大量請求在本地緩存都沒找到,去 Redis 里也沒找到,然后呢?
然后當然就是完犢子了,因為這些請求都會涌入到商品系統里去,讓商品系統從數據庫里查詢,直接把商品系統擊穿。
如下圖 9:
所以這個時候,我們往往需要對這種本地緩存做一個特殊的方案設計,那就是對于本地緩存不要采取這種讓他自動過期然后請求過來的時候讀取不到再去商品系統那里查找的模式,而是采取搶購系統針對本地緩存自動定時刷新。
也就是說,搶購系統內可以開一個后臺線程,然后讓他每隔 30min 自動去 Redis 里查最新緩存數據,或者去商品系統查最新緩存數據,然后刷新本地緩存,這樣就可以避免說自動過期后突然大量請求查不到緩存都涌入商品系統了。
如下圖 10:
②高并發搶購系統數據不一致問題分析與解決方案
再來看下一個比較常見的問題,就是扣庫存的緩存與 DB 不一致問題,這個問題的場景可能發生在如下情況。
就是說你在 Redis 里扣完了庫存之后,通過 MQ 發送了一個消息異步讓那個庫存系統在 DB 里扣庫存,可是人家庫存系統還沒在 DB 里扣減呢,這個時候你突然因為異常回滾了這次庫存扣減,此時 Redis 里把扣的庫存恢復了,然后發了一個消息到 MQ 去恢復庫存扣減。
如下圖 11:
但是這個時候 Redis 里的庫存是恢復了,可是庫存系統 DB 那里就是未必了,因為庫存系統從 MQ 里獲取消息的時候,很有可能是亂序獲取的,就是先獲取到恢復庫存的消息。
此時庫存系統一般會判斷一下,之前是否對這次搶購有過庫存扣減日志,如果沒有,他就不會去恢復庫存,然后接著再獲取到扣減庫存的消息,此時他就扣減了庫存,可是恢復庫存的消息再也沒機會處理了。
如下圖 12:
那么上面會導致什么呢?會導致 Redis 里扣減了庫存,又恢復了庫存,可是庫存系統的 DB 里先獲取了恢復庫存指令,結果什么都沒干,然后又獲取了扣減庫存指令,反而把庫存給扣了,此時緩存和 DB 里的庫存是不一致的。
所以針對這個問題,通常都會實現 MQ 順序消息,也就是說,把同一個搶購訂單的多個庫存操作指令發送到 MQ 的一個分區里去,讓他們實現有序,強制要求庫存系統必須按照順序依次獲取后執行,這樣就會先執行扣減庫存指令,再執行恢復庫存指令了。
如下圖 13:
總結
好了,今天這篇文章到這里為止,就給大家講了一下大廠里我們經常遇到的高并發搶購類系統的架構設計和優化過程,以及緩存擊穿與數據亂序不一致問題的分析和解決方案。