打破「公平」,讓秒殺系統飛起來

坑爹的技術競賽

幾年前,公司組織了一次技術競賽。自由組隊,每組4人,36小時以內按要求完成設計、代碼實現和文檔。第一名獎金3萬元。前端

一開始不是很想參加,由於要通宵熬夜(如今想一想,不參加競賽是個明智之舉)!不事後來仍是接收了其餘部門的邀請,參賽了。可是,在比賽前一天,其餘三我的放我鴿子!!!放我鴿子!!!放我鴿子!!!沒辦法,只能從新組隊,最終兩個後端、兩個前端!可是,比賽當天,另外一個後端去參加歌唱比賽了!!!參加歌唱比賽了!!!參加歌唱比賽了!!!因此變成了一個後端+兩個前端。咱們成了當時參賽小組裏惟一的三人小組,惟一的有前端的小組,仍是兩個前端!!!node

而後比賽的題目是:實現一個秒殺系統!!!web

  • 不能使用Java和Spring技術體系外的技術(由於除了咱們組,其餘組都是Java後端,因此只對Java技術作了限制),若是使用要扣分
  • 吞吐量越高越好
  • 不能出現超賣或少賣的狀況
  • 要有完善的文檔
  • ......

當時咱們三個就想直接棄權了!可是轉念一想,都走到這一步了,仍是試試吧!數據庫

最終的結果是,咱們沒完成(意不意外?驚不驚喜?)!!!前端在最後時間點纔開發完成,測試的時間都沒有!後來測試的時候,修復了幾個bug,爲了公平起見,就沒有參與最後的比賽!後端

不過,在全部的參賽隊伍中,咱們的吞吐量是最高的,其餘隊伍的TPS基本在三四千左右!咱們的TPS基本在一萬七左右!若是一開始就完成了,基本秒殺其餘組!瀏覽器

咱們的吞吐量是其它組的四倍,究其緣由是由於咱們的人員組成與其餘組不一樣,致使咱們的設計思路與其餘組的設計思路也不一樣緩存

在以前的什麼是軟件架構一文中,我對軟件架構作了一個定義:「架構是特定約束下決策的結果」!服務器

打破「公平」,讓秒殺系統飛起來

此次技術競賽的經歷,正好能夠驗證這一觀點:即便是在技術不對口、人數不足等劣勢條件下,只要作出合適的決策,依然能作出超出預期的架構設計!markdown

有時候,對於合適的架構設計,劣勢有時候會成爲優點架構

咱們先來看下,對於秒殺系統來講,通常的設計思路。

秒殺系統通常設計思路

秒殺系統的特色是:

  • 瞬時請求量很高
  • 持續時間較短

因此秒殺系統須要解決的是「在高併發狀況下,用戶請求及數據更新的問題」!

通常的設計思路:

  • (變相)擴容
  • 提升性能

具體方式有:

動靜分離

對於通常的應用來講,請求流程大體以下:

  • 服務端接收到請求,從數據庫中查詢相應數據
  • 選擇對應的展現模板
  • 經過模板和數據渲染出最終頁面
  • 將頁面返回給客戶端

當訪問量很大的時候,服務器壓力會很是的大!解決方案就是動靜分離

作軟件開發的都知道要「將變化的內容和不變的內容隔離開」,以便於獨立進化。這裏其實也是同樣的思路。

模板是個靜態的內容,部署後通常是不會變化的;而數據是個相對動態的內容,根據請求參數的不一樣,數據可能不一樣。因此咱們須要將模板與數據分離。

之前的作法是後端事先生成渲染後的頁面,緩存起來或直接部署到靜態服務器或CDN,請求時直接從緩存(靜態服務器/CDN)中獲取頁面,而動態數據經過AJAX請求的方式獲取。服務器再也不須要渲染頁面,只須要返回少許的數據便可。既下降了服務器壓力,又減小了服務端數據的傳輸。

而如今很流行的先後端分離就能很容易的解決這個問題。頁面獨立部署,數據異步獲取,頁面渲染由瀏覽器負責。這裏和普通的先後端分離還有些差別,須要將相對靜態的數據都靜態化,以減小動態數據量。

分離後,靜態內容和動態內容就能夠獨立進化。例如靜態內容能夠部署到CDN上,用戶能夠從最近的服務器獲取到數據。相對熱點的動態數據能夠作緩存,下降數據庫壓力,進一步提升服務端響應。

獨立部署

「獨立部署」其實也能夠當作是一種「動靜分離」。將秒殺系統這個相對動態的系統,和相對靜態的業務系統分開部署

緣由很好理解,秒殺系統的請求量很大,可能會因爲預估不足或系統問題,致使了秒殺系統的負載太高、響應變慢。若是秒殺系統是業務系統的一部分,則會致使業務系統響應變慢,甚至致使系統沒有響應。且秒殺是個短時間活動也不是核心業務,而業務系統是須要長期穩定運行的。不能由於一個短時間非核心的活動,而影響了核心的業務系統。

因此秒殺系統最好和業務系統分開獨立部署。即便秒殺系統掛了,也不會影響業務系統的正常對外服務。

一樣的道理,秒殺系統的數據庫也須要和業務系統的數據庫獨立開。

限流削峯

動靜分離獨立部署能提升系統的響應能力和容量。可是可提供的訪問量是必定的,當超過了系統所能承受的容量,該怎麼辦呢?你可能會說,能夠擴容啊。的確是能夠,可是擴容也是有限度的。假設單機能承受10萬的請求量,預計有1億的請求量,你要擴容1000臺服務器?!這會致使嚴重的浪費。

首先,上面提到了,秒殺是短時間活動,爲了秒殺多部署1000臺服務器,秒殺結束後這些服務器再銷燬?既浪費硬件資源、又浪費人力資源。

其次,秒殺的商品數量其實並很少,可能秒殺賺的那點錢還不夠付服務器和帶寬的費用。真·花錢賺吆喝!

咱們該如何處理呢?

上面說了,秒殺的商品數量很少,也就是說,其實最後的真實成交量並不大。再進一步講,不少的請求都是沒用的。

其次,在秒殺前,買家會頻繁的刷頁面,這又額外增長了無用請求的數量。

咱們只要把這些無用的請求提早都過濾掉,最終到達服務端的請求就會少不少,也就不須要這麼多的服務器了。這就是限流削峯。具體作法有不少:

  • 秒殺時間未到時,秒殺按鈕置灰:也就是說在秒殺未到時間時,不可發送下單請求。前面咱們已經將頁面靜態化,分發到了CDN,因此用戶的刷新操做只會到CDN。這就削除了刷新操做致使的請求。
  • 秒殺按鈕點擊後置灰:即避免double-click,一個用戶只能點擊一次。限制用戶點擊次數,避免秒殺工具帶來過量無效請求。
  • 秒殺前先作題:即在秒殺前須要先作題目,相似驗證碼功能,實際上是下降了用戶的點擊頻率,也限制了秒殺工具的使用。不過體驗很差,不推薦使用。
  • 限制請求次數:能夠用js斷定,限制用戶多少時間間隔內,只能請求多少次。在代理層也能夠基於ip作次數限制,限制單ip的請求數量。
  • 直接跳轉:假設秒殺已結束或秒殺隊列已滿,對後續的請求,直接跳轉到秒殺結束頁面。請求再也不到達服務端。
  • 請求排隊:經過消息隊列、內存排隊等手段,對請求進行排隊。相似EDA、Reactor。當隊列滿了之後,可拒絕後續請求。

服務端優化

上面的「請求排隊」,能夠作在web服務層,也能夠在服務端處理,亦能夠兩處都處理。除了排隊,服務端的優化的核心手段就是緩存,儘可能減小到數據庫的數據訪問,將熱點數據緩存起來。

更極致的優化可能還涉及到:

  • 減小序列化:你們都知道Java序列化和反序列化都是比較耗時的操做,即便使用第三方的序列化工具,也是須要消耗時間的,儘可能減小序列化操做,能減小這部分的時間消耗
  • 不要使用框架:如今通常開發都會使用框架開發,例如SpringMVC。SpringMVC使用了前端控制器,還包括不少的Filter,攔截器等,額外的增長了請求時間。使用純Servlet,能下降此部分的時間消耗。由於畢竟秒殺邏輯簡單,用不用框架,開發效率影響不大。
  • 使用字節流:即便用InputStream、OutputStream,不要使用Writer,Reader。與「減小序列化」相似,編解碼也會消耗時間。

另外還有扣庫存邏輯處理:

  • 拍下減庫存:用戶搶到後即扣除庫存,可是若是用戶搶到了不付款,最後秒殺的商品可能實際並無賣出去。
  • 付款減庫存:到用戶付款後纔去扣庫存。這可能致使下單數量遠超商品數量。致使的問題是,要麼後付款的買家被提示付款失敗。要麼就是超賣。
  • 預扣庫存:用戶搶到即扣除庫存。規定時間內沒有付款則取消訂單,恢復庫存。這個是經常使用手段

上面說的秒殺系統的通常設計思路。下面來看看咱們是怎麼鑽漏洞的!

競賽漏洞

因爲競賽規定,只能使用Java和Spring來處理,對其餘隊來講,這就致使了一個比較棘手的問題,IO優化。

Java支持兩種IO,BIO和NIO。對於秒殺這種場景來講,確定不能使用BIO。可是,若是使用Java原生NIO的話,須要處理不少問題,例如半包問題。以最終結果來看,選用原生NIO本身手寫異步IO框架的,全都以失敗了結。

那就只有另一條路,寧肯扣分,也要使用第三方框架,例如Netty。這就是賭,賭使用JavaNIO的隊伍沒法完成系統了。

而對咱們隊來講,咱們原先的劣勢---前端、一會兒就變成了優點。由於前端有node啊(若是沒有node,咱們妥妥的棄賽了)。當時node剛火起來,node天生就是個異步IO框架,競賽規定裏,可沒有對前端技術作技術限制!這個漏洞,咱們怎麼能不鑽?!

公平?公平!

最終競賽評比時,我發現一個問題,其餘隊伍很看重公平!!!即先到先得原則,優先到達的請求,優先排隊下單。這就致使,在秒殺結束前或請求被處理前,都須要等待,直到服務器處理後纔有返回。

這明顯增長了服務端的壓力,這也是致使他們的吞吐量限制在4000左右的緣由。但不是根本緣由。

根本緣由是這樣作就真的公平嗎?!這就要看每一個人對公平的理解了!我認爲這世上「沒有絕對的公平,只有相對的公平」!

你在秒殺系統裏排隊,保證先到先得,這就是公平嗎?

  • 若是一個買家是1M帶寬,另外一個買家是100M光纖,他們同時秒殺,你能保證公平嗎?
  • 若是你的服務器在北京,北京的買家是否是比廣州的買家更容易秒殺到?你能保證公平嗎?
  • 若是一個買家是萬年死宅,手速奇快;另外一個買家手不太靈活。你能保證公平嗎?

既然不能,我爲何要在服務端保證公平呢?!

咱們的設計

秒殺就是拼個運氣,只要不暗箱操做,那就是公平的。因此咱們不保證先到達的請求就能先買到商品!客戶哪知道他是否是先到的呢(雖然這樣說,看起來不公平,但實際確實是這樣)。因此咱們放棄了所謂的公平

咱們使用了兩個隊列:

  • 前端node隊列
  • 後端下單隊列

大體請求流程以下:

  • 假設商品數量爲100,那能夠設定node隊列長度爲1000,下單隊列長度爲100
  • 秒殺開始後,node隊列接收前端請求,先到先進。當隊列滿了之後,直接響應後面的請求,秒殺失敗/結束。
  • node隊列中的數據批量傳遞給後端的下單隊列,由消費線程從下單隊列中獲取請求進行處理
  • 若是100個商品所有處理完成(下單後,規定時間內沒有付款,取消訂單,恢復庫存),則秒殺結束
  • 若是100個商品沒有處理結束,繼續從node隊列獲取下一批數據處理
  • 若是node隊列有空餘後,後續的請求繼續進入隊列
  • node隊列中的請求設置超時,規定時間內沒有獲得處理,直接返回秒殺失敗/結束

總結

人員、技術、考量點的不一樣都會影響架構設計。一個符合當前人員、技術以及適合考量點的架構,可能能獲得意想不到的效果。

相關文章
相關標籤/搜索