<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          《吊打面試官》系列 Node.js 全棧秒殺系統(tǒng)

          共 12888字,需瀏覽 26分鐘

           ·

          2020-03-18 23:23

          本文公眾號來源:接水怪作者:宗緯本文已收錄至我的GitHub

          前言

          Coding 應當是一生的事業(yè),而不僅僅是 30 歲的青春?

          ???,這篇文章接水怪很用心,也很硬核,相信能看完的都有點東西?。?!?


          作為一個在互聯(lián)網(wǎng)公司面一次拿一次 Offer 的面霸,打敗了無數(shù)競爭對手,每次都只能看到無數(shù)落寞的身影失望地離開,略感愧疚。

          在一個寂寞難耐的夜晚,我痛定思痛,決定開始寫面試相關(guān)的文章,希望能幫助各位讀者以后面試勢如破竹,對面試官進行 360° 的反擊,吊打問你的面試官,讓一同面試的競爭者瞠目結(jié)舌,瘋狂收割大廠Offer?。ㄕ堅试S我使用一下夸張的修辭手法?)

          文章的名字只是我的噱頭,我們應該有一顆謙遜的心,所以希望大家懷著空杯心態(tài)好好學,一起進步?。


          每篇文章都希望你能收獲到東西,這篇是根據(jù)搶口罩的實際場景出發(fā),逐層對一個高并發(fā)的系統(tǒng)進行分析,其中 Node 服務(wù)層會講的相對詳細一些,希望你看完,能夠有這些收獲:

          • Node 生態(tài)已經(jīng)越來越好,一些高性能的 Web 業(yè)務(wù)場景,是完全可以用 Node 來做的

          • 前端應該不止于前端,學習一些服務(wù)端的知識,不僅僅單方面的說是為了做一些全棧的系統(tǒng),更多的是讓現(xiàn)有的前端可以去做更多的事情,去嘗試更多的可能

          • 能夠獨立去設(shè)計一些東西,可以是一個微型全棧的系統(tǒng),也可以是前端工程化中某個環(huán)節(jié)的工具

          場景分析

          真的打心底對那些在一線的工作人員點贊,接水怪的父親目前仍在一線湖北提供生活物資的運輸,我想說,爸,你真的很棒,怪怪為您感到自豪與驕傲!

          想搶個口罩怎么就這么難!接水怪大學之前是湖北的,想給湖北的朋友搶個口罩快遞過去,結(jié)果顯而易見,沒有搶到…

          各大平臺也相繼開始了各種口罩秒殺活動,接水怪陸陸續(xù)續(xù)搶了快一個月了,還是沒搶到~

          于是,接水怪痛定思痛,下定決心對口罩秒殺系統(tǒng)架構(gòu)一探究竟,雖然業(yè)界大部分的這種場景應該都是基于 Java 實現(xiàn)的,但是怪怪我決定嘗試從 Node.js 的方向,配合業(yè)界一些成熟的中間件來分析一下整個系統(tǒng)的架構(gòu),以及一些常見的問題。

          關(guān)于 Node.js 如何實現(xiàn)高并發(fā)的原理,怪怪往期的文章有寫哈。?

          寫之前跟好兄弟丙丙也請教了不少后端側(cè)的東西,對,就是你們熟悉的那個男人,號稱自己在互聯(lián)網(wǎng)茍且偷生的那個敖丙

          怪怪在這里將前后端一并分享給大家,畢竟 Node 的生態(tài)環(huán)境日益增強,前端側(cè)能做的東西也越來越多。

          比如你想在公司自己做一個基于 Node 的前端發(fā)布系統(tǒng),也會涉及到 db,緩存,消息中間件這些東西。

          8cfcecde7003ba480f72691a103c4dd4.webp

          接下來,讓我們請出今天的主角兒,當當當當~~ 它就是低調(diào)奢華有內(nèi)涵的口罩秒殺系統(tǒng)架構(gòu)圖,后面的內(nèi)容會基于這個架構(gòu)圖來擺會兒龍門陣。

          擺龍門陣,怪怪方言,也就是怪怪想跟大家聊會兒天的意思啦~)

          5bf152d6560ca463e62f3a7fa7ac6cae.webp

          秒殺類型的業(yè)務(wù)為什么難做

          秒殺,秒殺,顧名思義就是一個短時間內(nèi)的高流量操作,是一個天然高并發(fā)的場景,換句話說,讀寫沖突十分嚴重。

          而通常秒殺類型的業(yè)務(wù),價格都比較誘人,這就引發(fā)了另外一個薅羊毛的問題,有不少黃牛會在這個時候趁機撈一筆,想必每年春節(jié)回家,大家都深有感觸吧,還沒來得及輸完 12306 的高智商驗證碼,票已經(jīng)沒有了,哦豁~~

          "哦豁",怪怪的方言,很無奈,很驚訝的意思

          6db04f141c562a0cef95ae8ad2d8cd5e.webp

          試想幾十萬,上百萬的流量直接打到 DB?

          世界突然安靜,不得不再一次發(fā)出熟悉的感慨,哦豁~

          ccc1c87f4f540ef578f7bfe3926ab3ba.webp

          因此,秒殺的核心就在于請求是真實請求的前提下,處理好高并發(fā)以及數(shù)據(jù)庫存扣減的問題。

          接下來,就讓我們逐層來看看是怎么做的~

          業(yè)務(wù)規(guī)則層面

          針對流量特別大的場景,可以分時分段開展活動,原來統(tǒng)一 10 點搶口罩,現(xiàn)在 6 點,6 點半,7 點,…每隔半個小時進行一次活動,達到流量攤勻的效果。

          前端層面

          APP / H5 / 小程序 / PC

          針對平民老百姓

          前端側(cè)會進行一個按鈕置灰的操作,當你點擊完一次之后,按鈕會變灰,防止用戶重復提交搶口罩的請求。

          針對程序員

          想必你一定會說,這還不簡單?抓個包,寫個定時腳本,一到時間,for 循環(huán)自動打請求不就好了?

          我只能說,小伙子,你很優(yōu)秀,有的場景確實是闊以滴,比如之前女孩子們瘋搶的九價 HPV 宮頸癌疫苗,想必有不少女孩子都讓程序員小哥哥幫忙寫腳本,搶疫苗,怪怪我親眼目睹,我們團隊一位前端小哥哥用 Node 定時腳本搶到啦~

          64469320ed6c8fb2b1fac8a8d9a1a155.webp

          別著急,剛剛就是讓你皮一下,哦豁~

          針對上面這種情況,后端 controller 層會進行處理,簡單來說就是對同一個用戶搶口罩的請求進行校驗,對于同一個用戶在短時間內(nèi)發(fā)送的大量請求進行次數(shù)限制,以及請求去重處理~

          但怪怪我咨詢了風控的朋友,現(xiàn)在企業(yè)中大部分場景,后端是不做這個特殊處理,而是讓風控團隊來處理,下面所謂黑客的部分會稍微細說一下。

          道高一尺,魔高一丈,接下來請出我們的終極大魔王,高端黑客??!

          針對更加高端的黑客們

          比如一些高端黑客,控制了 30w 個肉雞,也就是這些人手上有 30w 的 uid,如果他們同時發(fā)動手上 30w 的肉雞來搶口罩,咋辦?

          此時,再一次陷入了尷尬~ 哦豁~~

          這個問題,怪怪我咨詢了安全、以及風控的朋友,簡單跟大家分享一下

          首先,安全針對秒殺,沒做啥特殊的處理。

          一般來講,秒殺類工作主要在風控這邊,對于那種利用機器或者腳本瘋狂刷新的,QPS 異常高的,換句話說就是短時間內(nèi)流量異常高滴的用戶,會直接給他彈一個滑塊(滑動驗證,應該大家都遇到過這種情況),這樣可以大大提高那些刷請求批量操作的成本,甚至能夠遏制他們的行為。

          同時風控也會根據(jù)一系列的規(guī)則(通過這些規(guī)則來判定這個 uid 也就是這個用戶是否符合要求,不要問我具體規(guī)則,怪怪我還想繼續(xù)寫文章,不想進去~~),對于那些不符合平臺下單要求的肉雞,直接進行請求攔截,甚至有的會加入黑名單,直接取消掉這個用戶的相關(guān)權(quán)限。

          寫到這里,我想說,風控、安全團隊,你們還是有點東西?。?/p> b2b76f48c2afb0f58840c6775d477d62.webp

          Nginx 層

          實際上真正的企業(yè)架構(gòu)中,在 Nginx 的上面一層還會有一層 lvs ,這里不展開講,簡單解釋就是能夠在網(wǎng)絡(luò)第 4  層對 Nginx 服務(wù)的 ip 進行負載均衡

          雖然上面我們攔截了惡意請求,減少了部分流量,但秒殺真實的用戶也超多的啊,你想想這次有多少人在搶口罩!

          所以我們我們還是要搞負載均衡~~

          我們先簡單解釋一下集群跟負載均衡是個什么玩意兒,哦豁~

          集群嘛,簡單來講就是我們不是有秒殺的 Node 服務(wù)嘛,那一臺機器是不是有可能會掛掉,流量太大,單臺機器根本扛不住,咋辦?

          加機器唄,一臺會被打掛,那我們多搞幾臺,不就變強啦?俗話說,1 根牙簽一折就斷,10 根牙簽…… 咦,好像也是一折就斷,hhhh,哦豁~

          負載均衡是嘛玩意兒嘛?剛上面不是說了集群就是加機器嘛,那現(xiàn)在機器雖然變多了,來了一個請求,具體派哪個機器去處理,是不是得有一套規(guī)則嘛,并且這套規(guī)則要讓請求分發(fā)的比較合理,不然就失去了集群的意義了撒~

          Nginx 層下面是基于 Node 的 service 層,也就是業(yè)務(wù)邏輯會在這里進行處理,實際上 Nginx 在這里主要做了兩件事:

          • 對于前端打過來的真實的搶口罩請求,在 Nginx 這里進行請求的分發(fā),打到 Node 集群的某一個機器上

          • 健康檢測,Node 集群的機器同樣有可能掛掉,所以會利用 Nginx 進行檢測,發(fā)現(xiàn)掛了的機器,會干掉重啟,保證集群的高可用。檢測有兩種機制,被動檢測跟主動檢測,不展開說,后面會出一篇 Nginx 原理與實戰(zhàn)的文章,像跨域啊,項目部署啊,自己搞個代理啥的啊,都可以用 Nginx 來搞,哦豁~

          Node Service 層

          這一層我會說的細一些,讓我們挨個來擺下龍門陣吧~~

          首先,為什么選擇 Node 來做 service 層

          想必大家都知道服務(wù)模型是經(jīng)歷了 4 個階段滴,同步、復制進程、多線程、以及事件驅(qū)動。

          不清楚的同學需要補補作業(yè)啦~

          多線程目前業(yè)界以 Java 為首,性能啥的就不說了,各種雙十一已經(jīng)證明這位兄臺是個狠人。

          但是,怪怪我覺得,雖然多線程之間可以共享數(shù)據(jù),內(nèi)存可以充分利用,并且利用線程池可以較少創(chuàng)建和銷毀線程,節(jié)省開銷。但是,還是存在一些問題,比如:

          • 操作系統(tǒng)在切換線程的時候,同樣需要切換線程上下文,如果線程數(shù)量太多,切換線程會花費大量時間

          • 多線程之間的數(shù)據(jù)一致性問題,各種加鎖的神仙操作,也容易出問題

          那能不能讓我們?nèi)艘娙藧鄣涡¢煇?,Node 小朋友,來嘗試搞一下?

          31320b39a7d3fd36c42131fa663e6fea.webp

          Node 是基于事件驅(qū)動模型滴,相信前端同學都知道事件驅(qū)動,后端同學可能有點懵,但是作為后端同學,Node 你沒搞過,Nginx 你總用過哈,Node 跟 Nginx 都是基于事件驅(qū)動模型滴~

          并且,Node 是單線程,采用單線程可以避免不必要的內(nèi)存開銷,以及上下文切換。

          跟多線程 Java 比起來,好像 Node 也有自己的優(yōu)勢呢~

          我知道,已經(jīng)有不少同學迫不及待要接著問我了,尤其是后端同學心里肯定在想,你 TM 逗我呢,你一個單線程還能來搞高并發(fā),多核 CPU 怎么利用?

          畢竟隔行如隔山,后端大佬對我們 Node 有誤解,也是情有可原,就跟很多前端覺得 Java 用類型檢測是很麻煩的,不如我們 JS 靈活,是一個道理~

          如果我們 Node 解決了多核 CPU 的資源利用問題,再加上 Node 異步非阻塞的特性,帶來的性能上的提升應該是很不錯滴,并且也沒有復雜的多線程啊,加鎖這一類心累的問題。

          關(guān)于 Node 單線程如何實現(xiàn)高并發(fā),可以查看往期文章哦?

          Node Master-Worker 模式

          我們順著架構(gòu)圖,一步步來分析~

          e373b985fd3f9d794b18879d7f929fe0.webp

          先看這里,說好的單進程,單線程呢,這是個什么鬼?

          別著急,這是 Node 進程模型中著名滴 Master-Worker 模式哦~

          還不是因為單線程,無法利用多核 CPU 嘛,我們的小闊愛 Node 提供了 child_process 模塊,或者說 cluster 模塊也行,可以利用 child_process 模塊直接創(chuàng)建子進程。

          說到這里, HTML5 提出的 Web Worker ,方式大同小異,解決了 JavaScript 主線程與 UI 渲染共用一個線程所引發(fā)的兩者相互阻塞的問題。

          cluster 模塊:實際上就是 child_process 模塊跟其它模塊的組合
          另外申明一點:fork 線程開銷是比較大的,要謹慎使用,并且我們 fork 進程是為了利用 CPU 資源,跟高并發(fā)沒啥大關(guān)系

          這樣看來,多核 CPU 的資源利用問題,好像得到了解決,雖然看起來方式稍顯粗暴~

          e8ef40fb9b4d1b288491d08ea8ec436e.webp

          我們再次回歸到上面的主、子進程的架構(gòu)圖,接著談?wù)勚鬟M程與子進程通信的問題,現(xiàn)在一個口罩秒殺請求過來了,Node 主進程,子進程這里是怎么進行處理的呢?

          主-子進程通信

          想必大家都知道 IPC,也就是進程間通信,首先要申明的是實現(xiàn) IPC 的方式不止一種,比如共享內(nèi)存啊、信號量啊等等~~

          而 Node 是基于管道技術(shù)實現(xiàn)滴,管道是啥?

          在 Node 中管道只是屬于抽象層面的一個叫法而已,具體的實現(xiàn)都扔給一個叫 libuv 的家伙去搞啦~之前的文章有講到 Node 的 3 層架構(gòu),libuv 是在最下層滴哦,并且大家都知道 Node 可以跨平臺嘛,libuv 會針對不同的平臺,采用不同的方式實現(xiàn)進程間的通信。

          2ed90863c1d74249f3c76abe9ee656ba.webp

          ok,我們上面大致說完了理論部分,是不是要實戰(zhàn)一把了?

          客戶端請求代理轉(zhuǎn)發(fā)

          73e20e344889de607f7dba6eda1b100f.webp

          一把大叉叉什么鬼?說明通過代理請求轉(zhuǎn)發(fā)這種方式是不太友好滴~

          那我們具體分析一下,為什么不友好,怎么改進?

          首先我們需要明確一點,操作系統(tǒng)有一個叫文件描述符的東西,這個涉及到操作系統(tǒng)內(nèi)核的一些小知識點,并且操作系統(tǒng)的文件描述符是有限的,維護起來也是需要成本滴,因此不能鋪張浪費~

          那我們分析一下上面的流程,這些口罩請求打到主進程上面,主進程對這些請求進行代理,轉(zhuǎn)發(fā)到不同端口的子進程上,看起來一切都那么美好~

          并且在主進程這里,我們還可以繼續(xù)進行一層負載均衡,讓每個子進程的任務(wù)更加合理。

          646b4f4342d563f198a79c23c3c9eb79.webp

          but,but,接下來是重點?。?/p>

          前面我們說啦,操作系統(tǒng)的文件描述符不能鋪張浪費,我們來看看這個代理的方式,有沒有浪費~

          首先,需要明確一點。進程每收到一個連接,就會用到一個文件操作符,所以呢?來,怪怪給你整個當字開頭的排比句!

          當客戶端連接到主進程的時候,用掉一個操作符~

          當主進程連接到子進程,又用掉一個~

          所以嘛,從數(shù)量上來看,代理方案浪費掉了整整一倍,這好像不太科學,囊個搞內(nèi)?

          怪怪那兒的方言,“囊個搞”,就是怎么辦的意思咯~~

          魔高一尺,道高一丈~

          句柄傳遞-去除代理

          Node 在 v0.5.9 引入了進程間發(fā)送句柄的機制,簡單解釋一下,句柄實際上就是一種可以用來標識資源的引用。

          通過句柄傳遞,我們就可以去掉代理這種方案。使主進程收到客戶端的請求之后,將這個請求直接發(fā)送給工作進程,而不是重新與子進程之間建立新的連接來轉(zhuǎn)發(fā)數(shù)據(jù)。

          但實際上這一塊還涉及到很多知識點,比如句柄的發(fā)送與還原啊,端口的共同監(jiān)聽啊等系列問題。

          這一塊的具體實現(xiàn),可以參考 《深入淺出 Node.js 》9.2.3 句柄傳遞

          最終,通過句柄傳遞就可以得到我們想要的效果,即不經(jīng)過代理,子進程直接響應了客戶端的請求。

          46fceb98f07351c021bc9ab0ffc42a9b.webp

          怪怪我也去研究了一下 Egg.js 在多進程模型和進程間通信這塊是怎么做滴,大體架構(gòu)思路跟上面差不多,不同的點在于, Egg.js 搞了一個叫 Agent 的東西。

          對于一些后臺運行的重復邏輯,Egg.js 將它們放到了一個單獨的進程上去執(zhí)行,這個進程就叫 Agent Worker,簡稱 Agent,專門用來處理一些公共事務(wù),具體細節(jié)可以參考 Egg.js 官網(wǎng)。

          子進程服務(wù)高可用問題

          畢竟是秒殺服務(wù),fork 的子進程是可能掛滴,所以我們需要一種機制來保證子進程的高可用。

          我知道,你肯定會說,掛了,重啟一個繼續(xù)提供服務(wù)不就好了?

          對,你說的沒錯,我們就先來搞定重啟這一趴~

          假如現(xiàn)在某個子進程發(fā)生異常啦,哦豁~

          那么,此時這個子進程會立即停止接受新的請求連接,直到所有連接斷開,當這個子進程退出的時候主進程會監(jiān)聽到一個  exit() 事件,然后主進程重啟一個子進程來進行補償。

          也就是這樣一個流程。

          2b43142d86814b0d24c1973c5b9837e0.webp

          小伙子,你很不錯,但是極端情況我們是不是也要考慮一下子?

          假如有一天,出現(xiàn)極端情況,你所有的女朋友一夜之間都要跟你分手,是不是開始慌了?(壞笑~~)

          如果所有的子進程同時異常了,按照我們上面的處理方式,所有的子進程都停止接收新的請求,整個服務(wù)豈不是就會出現(xiàn)瞬時癱瘓的現(xiàn)象了?大部分的新請求就會白白丟掉,想想如果是雙十一,損失得有多大,哦豁~

          既然如此,我們就來改進一下。

          有的小伙伴可能會說,我知道怎么搞了?。。?/p>

          既然不能一直等待著,那就直接暴力 kill 干掉這個進程,然后立馬重啟一個?。?/p>

          小伙子,你有點激動,不過可以理解,畢竟能想到這里,你還是有點東西~~ 不過可以更加全面一點

          暴力 kill 掉,會導致已連接的用戶直接中斷,那用戶也就自然離開啦~ 哦豁~

          所以,總結(jié)起來我們要解決的就是,如何在不丟失現(xiàn)有用戶請求的基礎(chǔ)上,重啟新的子進程,從而保證業(yè)務(wù)跟系統(tǒng)的雙重高可用?。。?/p> 01a7a20280d89f519d3e391fb50c3815.webp

          忽然靈光一現(xiàn),我們可以這樣搞撒~

          上面分析到主進程要等到子進程退出的時候,才會去重啟新的子進程,這就有點意思啦??!同志,不要等到分手了再去反思自己哪里做的不對!??!,只要女朋友一生氣,即使響應,立馬認錯,沒毛病~~~

          因此,我們可以在子進程剛出異常的時候,就給主進程發(fā)一個自殺信號,主進程收到信號后,立即創(chuàng)建新的子進程,這樣上面的問題完美解決??!

          從此跟女朋友過上了幸福美好滴生活~

          1a4f47cf0454c98689de9062e15359f9.webp
          Node 集群

          搞定了上述秒殺服務(wù) Node 主進程,子進程高可用的問題之后,我們就可以搭建 Node 集群了,進一步提高整個系統(tǒng)的高可用,Node 集群的負載均衡,文章一開始的 Nginx 層已經(jīng)講過啦~

          至于集群做多大,用多少臺機器,這些需要根據(jù)具體的業(yè)務(wù)場景來決策。并且一般大型的活動上線之前,企業(yè)內(nèi)部會進行幾輪壓測,簡單來講,就是模擬用戶高并發(fā)的流量請求,然后看一下小伙子你搞的這個系統(tǒng)抗不扛得住,靠不靠譜~

          但真正的壓測,遠比我說的要復雜,比如壓測中某個環(huán)節(jié)出了問題,扛不住啦,是直接重啟機器,還是擴容,又或者是降低處理等等,這都是需要綜合考慮滴~

          當然,最終的目的也是在保證服務(wù)高可用的前提下能給企業(yè)節(jié)約成本,畢竟加機器擴容是需要成本滴~

          同樣的需求,別人要 5 臺機器,你只要 4 臺就能搞定,小伙子,那你就真的有點東西啦,升職加薪指日可待!!~

          1599072fb4188219acd46b2d3277a605.webp
          數(shù)據(jù)一致性

          搞定了上面 Node 服務(wù)的各個環(huán)節(jié)之后,還有一個很重要的問題要解決,我丟!??!咋還有問題~~

          首先,明確一個知識點,Node 多個進程之間是不允許共享數(shù)據(jù)滴~

          這就是最后一個非常重要的問題,數(shù)據(jù)共享。

          實際業(yè)務(wù)中,就拿我們今天的口罩系統(tǒng)來說,庫存量就是一個典型數(shù)據(jù)共享的場景,會有多個進程需要知道現(xiàn)在還有多少庫存,不解決好這個問題,就很有可能發(fā)生口罩超賣的問題。

          超賣,簡單來講就是,怪怪家一共有 200 只口罩等待出售,然后怪怪我在某平臺上發(fā)起了一個秒殺搶口罩的活動,由于平臺的數(shù)據(jù)沒處理好,怪怪我在后臺驚喜的收到了 500 個訂單,超出了 300 個訂單,這就是超賣現(xiàn)象啦~

          那這個問題,我們應該如何避免呢,怪怪我有點心累,買個口罩怎么這么復雜!

          既然 Node 本身不支持數(shù)據(jù)共享,ok,那我們就用三方的嘛,比如今天的第二個主角~ 當當當當~~ 它就是一位名叫 Redis 的神秘女子,身穿紅色外套,看著還有點內(nèi)斂~

          7026d6f739a59651270f14419ac06194.webp

          至于 Redis 這里如何保持數(shù)據(jù)一致,放到下面 Redis 的部分一起說。

          33e4c24886570a7f9145aa0e3ac00282.webp

          Redis 層

          我們使用 Redis 解決上述遺留的 Node 進程間數(shù)據(jù)不能共享的問題。

          解決問題之前,先簡單介紹一下 Redis ,畢竟,都不認識,怎么開始戀愛?

          Redis 還是有很多知識點滴,具體可查看 Redis 官網(wǎng)

          相信大家在大學的時候都接觸過 SQL Server ,相信你也曾經(jīng)因為 SQL Server 那詭異的環(huán)境曾頭痛~

          但大部分學校,應該都是沒有 Redis 這門課滴,如果有,怪怪我只能說,朋友,你的學校有點東西??!

          步入正題,Redis 是一個在內(nèi)存中進行數(shù)據(jù)結(jié)構(gòu)存儲的系統(tǒng),可以用作數(shù)據(jù)庫、緩存和消息中間件,是 key-value 的形式來進行數(shù)據(jù)的讀寫。簡單理解就是,我們可以利用 Redis 加緩存減輕 db 的壓力,提升用戶讀寫的速度。

          大家要明確的是,秒殺的本質(zhì)就是對庫存的搶奪,如果每個從上層 Node 服務(wù)打過來的請求,都直接去 db 進行處理,

          拋開最大的性能問題先不說,你不覺得這樣好繁瑣,對開發(fā)人員以及后期的維護都很不友好嘛?

          那咋辦

          上面已經(jīng)說啦,直接搞到 MySQL 扛不住,你可以找她閨蜜 Redis 嘛,寫一個定時腳本,在秒殺還未開始之前,就把當前口罩的庫存數(shù)量寫入 Redis,讓 Node 服務(wù)的請求從 Redis 這里拿數(shù)據(jù)進行處理,然后異步的往 kafka 消息隊列里面寫入搶到口罩用戶的訂單信息(這一塊具體的放到后面 kafka 消息隊列部分分析)。

          那這里就關(guān)聯(lián)上了我們前面提到的問題,數(shù)據(jù)一致性問題,如何保證 Node 服務(wù)從你 Redis 拿到的庫存量是沒有問題滴?

          如果直接按正常的邏輯去寫,搶到口罩,Redis 中庫存 count -1,這種方式看起來是沒有問題滴,但是我們來思考這樣一個場景。

          比如倉庫里面還有最后 1 個口燥,現(xiàn)在光頭強發(fā)了一個請求過來,讀了一下 Redis 的數(shù)據(jù),count 為 1,然后點擊下單,count - 1 。但如果此時 count 正在執(zhí)行 -1 的操作的時候(此時 count 依然是 1 ),熊大哥哥一看這形式不對,也開始搶口罩,一個查庫存量的請求再一次打了過來,發(fā)現(xiàn) Redis 中庫存量仍然為 1 ,然后點擊下單。

          按上述的流程,就會發(fā)生文章一開頭提到的超賣現(xiàn)象,1 只口罩,你給我下了兩個單??。?!

          這又咋辦
          9ff4454b2710f1e3f86b3e2cc0dc1965.webp

          我們可以加事務(wù)嘛?。?/p>

          事務(wù)可以一次執(zhí)行多個命令,并且有兩個很重要的特性~

          • 事務(wù)中的所有命令都會序列化、按順序地執(zhí)行。事務(wù)在執(zhí)行的過程中,不會被其他客戶端發(fā)送來的命令請求所打斷

          • 事務(wù)是一個原子操作:事務(wù)中的命令要么全部被執(zhí)行,要么全部都不執(zhí)行。

          這里直接引用 Redis 的官方示例來解決超賣問題,官方示例講的是執(zhí)行 +1 的操作,我們這里只需要修改成 -1 就 ok。具體可參考官方文檔。

          4265a25e933dec02a40135cf3a5adf69.webp

          這樣一來,Node 數(shù)據(jù)共享,庫存減叩的問題就搞定了,Redis 小姐姐,你有點東西!!~

          那如果單臺 Redis 扛不住怎么辦,我們可以對 Redis 進行集群嘛,主從同步這一系列的操作,然后再搞點哨兵持久化也搞上,那你這個秒殺直接無敵?。。?/p>

          后續(xù)有時間,會寫一個基于 Node.js 的 Redis 實戰(zhàn)與原理剖析?,之前怪怪寫過一個可視化頁面搭建系統(tǒng),其中各個模塊在后臺進行渲染的時候,為了提高渲染速度,就用到了 Redis 進行高速緩存。

          緩存這一趴告一段落,下面接著說說消息隊列~

          kafka 消息隊列層

          如果將訂單數(shù)據(jù)一次性全部寫入 db,性能不好,所以會先往消息隊列里面存一下,當然消息隊列不僅僅是起到了這個作用,像什么應用的解耦,也可以基于 kafka 來做一層封裝處理。

          先簡單說一下消息隊列,不光秒殺,其它場景的使用大同小異。

          kafka 消息隊列就是基于生產(chǎn)者-消設(shè)計模式滴,按具體場景與規(guī)則,針對上層請求讓生產(chǎn)者往隊列的末尾添加數(shù)據(jù),讓多個消費者從隊列里面依次讀取數(shù)據(jù)然后自行處理。

          結(jié)合到我們的秒殺場景就是,對訂單進行分組存儲管理,然后讓多個消費者來進行消費,也就是把訂單信息寫入 db。

          這里附上一張經(jīng)典的消息隊列的圖,有興趣的小伙伴可以深入去了解 kafka,還是有點東西。

          225783751c5e32273639e8dd6774924b.webpkafka

          MySQL 數(shù)據(jù)庫層

          數(shù)據(jù)庫這塊不多說,數(shù)據(jù)存儲的地方,最終的訂單信息會寫到 MySQL 中進行持久化存儲。

          簡單模擬這個系統(tǒng)的話,表結(jié)構(gòu)就搞到最簡化就行。

          CREATE TABLE IF NOT EXISTS `seckill`(
             `id` INT UNSIGNED AUTO_INCREMENT,
             `date` TIMESTAMP,
             `stock` INT UNSIGNED,
             PRIMARY KEY ( `id` )
          )ENGINE=InnoDB DEFAULT CHARSET=utf8;

          深入學習 MySQL,還是有些東西滴,查詢優(yōu)化啊,索引怎么用啊,底層數(shù)據(jù)結(jié)構(gòu)啊,怎么去設(shè)計表啊等等一些列問題都可以去探究~

          相信前段時間搞的沸沸揚揚滴刪庫跑路事件,大家也都聽說了~~

          順便提一嘴,很多小伙伴說,我想搞一下前端工程化,我想搞幾個減少開發(fā)流程的系統(tǒng),我想…

          就舉個很簡單的例子,企業(yè)都有線下、預發(fā)、線上這一套環(huán)境,你想開發(fā)一款 host 切換工具來切各個不同的環(huán)境,兩種方式:

          • 直接基于 Electron 寫一個本地 host 文件(etc/hosts)讀寫的工具,看似也沒什么毛病

          • 但既然是整個公司用,那思維就不要太局限啦,如果僅僅開發(fā)一個本地 host 文件讀寫的工具,那沒什么意思,大家直接用開源的不就好了,還節(jié)省成本,不用開發(fā)。

          • 做成可配置滴,公共的 host 配置文件存到 db,并且支持本地自定義配置,這就很棒啊,全公司統(tǒng)一。假如哪天預發(fā)的機器 ip 變了,那么只用去 host 工具管理后臺更新一下,全公司所有的小伙伴就可以一鍵更新啊,是不是很爽??

          怪怪之前是維護我們公司的這個 host 小工具滴,后面計劃自己寫一套開源出來。

          什么,這么個破玩意兒你還要開源?

          3be9172d37e021d2f33ac6a2c8615fec.webp

          舉上面的例子,就是想說,Node 生態(tài)越來越好,我們前端能做的事情也越來越多,我們有義務(wù)去接觸更多的東西,尋找更多前端的突破口~

          狼叔那一篇在 cnode 社區(qū)置頂?shù)奈恼拢蛯懙姆浅Y潱?!~~~

          踩過的坑-經(jīng)驗分享

          能夠看到這里,說明你有點東西,整個系統(tǒng)的各個層都講了一下,我估計現(xiàn)在讓你自己來搞一個秒殺系統(tǒng),也問題不大了~

          怪怪我基于文章開頭的架構(gòu)圖,自己搞了一個低配版的 Node 秒殺系統(tǒng),在這里把踩過的一些小坑跟大家分享一下,畢竟踩過的坑,不希望大家踩第二次!

          首先,如果大家自己在本地搞這個系統(tǒng),可能乍一看,又有 Node,又有 Nginx,又有 Redis ,又有 kafka,而且管理 kafka 還要用 zookeeper,感覺這也太麻煩了,可能看到這個環(huán)境搭建都望而卻步了~。

          淡定,淡定!!

          33e4c24886570a7f9145aa0e3ac00282.webp

          我們可以用 docker 來搞嘛,這樣就很輕量并且很方便管理,沒用過的小伙伴強烈建議去學習一下~
          使用 docker 來搞,上面說的幾個環(huán)境,一個配置文件就搞定,簡單示意一下:

          services:
            zk:
              image: wurstmeister/zookeeper
              ports:
                - "2181:2181"
            kfk:
              image: wurstmeister/kafka
              ports:
                - "9092:9092"
              environment:
                KAFKA_ADVERTISED_HOST_NAME: 172.17.36.250
                KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
              volumes:
                - ./docker.sock:/var/run/docker.sock
            redis:
              image: redis
              ports:
                - "6379:6379"
            mysql:
              image: mysql
              ports:
                - "3306:3306"
              environment:
                MYSQL_ROOT_PASSWORD: 'password'
          遇到過坑的小總結(jié)
          • 用 docker 搭建環(huán)境,避免自己去裝那些不必要的環(huán)境,比如 MySQL。

          • 使用 Docker Compose 定義和運行多容器的 Docker 應用程序,便于容器的集中配置管理。

          • 刪除 Docker 容器,容器里面的數(shù)據(jù)還是會在你物理機上面,除非你手動去清理。

          • kafka-node 這個 npm 包,最新的版本用法相比老版本有一些更新,比如老版本創(chuàng)建一個 kafkaClient 的寫法是 new kafka.Client(); 但新版本現(xiàn)在已經(jīng)是 new kafka.KafkaClient(); 這種寫法了

          • kafka 的 docker 鏡像,官方提供的 docker-compose 示例未指定端口映射關(guān)系,需要自行處理一下映射關(guān)系

          • Egg.js 首次在連接 MySQL 的 docker 容器的時候,會出現(xiàn) Client does not support authentication protocol requested by server; 的異常,修改一下密碼就 ok。

            ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_new_password';
            FLUSH PRIVILEGES;

          被后端朋友靈魂 3 問的小分享

          如果是一家很小的公司,由于資金問題機器不夠用來集群怎么辦

          萬一機器不夠,只能放棄部分請求,原則上還是要已保護系統(tǒng)為核心,不然所有的請求都失敗,就更不理想


          kafka 隊列這邊如果發(fā)生異常處理失敗怎么反饋給用戶

          那就給用戶及時反饋說下單失敗,讓用戶重試,都已經(jīng)搶到口罩了,重新下個單應該問題不大的,再者,這是少數(shù)情況~


          搶到口罩之后未及時支付或取消這筆訂單,如何對剩余庫存做及時的處理呢

          首先,搶到口罩之后,支付界面會有一個支付的時間提醒,例如,超過 20 分鐘未支出,這筆訂單將被取消。關(guān)聯(lián)到數(shù)據(jù)庫里就會有一個未支出的狀態(tài)。如果超過時間,庫存將會重新恢復。

          前端側(cè)也會給到用戶對應的提示,比如 20 分鐘之后再試試看,說不定又有口燥了喲~

          總結(jié)

          回過頭再去看看文章開頭的系統(tǒng)架構(gòu)圖,相信你會有不一樣的收獲~

          怪怪上面寫的也不全,真正的企業(yè)實戰(zhàn),整個鏈路會復雜很多,環(huán)環(huán)相扣。比如 Node 服務(wù)的監(jiān)控報警啊,各種異常處理啊等等~~

          戳:百萬字長文帶你學習「Java」

          
           

          掃碼或者微信搜Java3y 免費領(lǐng)取原創(chuàng)思維導圖、精美PDF。在公眾號回復「888」領(lǐng)取,PDF內(nèi)容純手打有任何不懂歡迎來問我。

          1133c541feff3babfd32784ab927a010.webp

          bc347d7643cbcb286934d80de887f1de.webp


          df5d405c041ba54fb63b4cd58b223a32.webp

          efca9d9d813f9385dd455e301ce9b454.webp

          efca9d9d813f9385dd455e301ce9b454.webp

          瀏覽 31
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美影院屄 | 青娱乐国产盛宴 | 在线午夜福利 | 亚洲高清有码无码视频 | 亚洲有码在线播放 |