秒殺搶購系統優化思路詳解

前幾年,火車票購票網站12306,每到放假高峰期,在線票刷不出來,購買困難,甚至出現了各種插件支持搶票,這樣的場景,對於每個買過票的人,應該印象深刻。小米手機的搶購活動,同樣異常火爆,在幾分鐘的時間內,賣出幾十萬部手機。當一個Web系統,在一秒鐘內收到數以萬計甚至更多請求時,系統的優化和穩定至關重要。

在面試中,面試官提出這樣的問題,應該從哪些角度分析。在工作中,也許沒有這麼龐大複雜的應用場景,但是,針對網站的優化思路是一致的。本文從技術度度,分析下應如何設計優化系統,才能保障如此大規模的併發訪問。

秒殺系統主要解決三大問題:

一、瞬時的高併發訪問。搶購和普通的電商銷售有所不同,普通的電商銷售,流量是比較平均的,雖然有波峰波谷,但不會特別突出。而搶購是在特定時間點進行的推銷活動,搶購開始前,用戶不斷刷新頁面,以獲得購買按鈕;搶購開始的一瞬間,集中併發購買。

二、數據正確性。搶購畢竟是一種購買行為,需要購買、扣減庫存、支付等複雜的流程,在此過程中,要保證數據的正確性,防止超賣(賣出量超過庫存)的發生。

三、防作弊。無論是火車票的購買,還是低價商品的促銷,肯定不希望某些客戶買到所有的商品,應儘量保證公平性。通過購票插件購買火車票,阿里巴巴搶月餅事件等,需要限制技術性用戶繞過網站的限制,通過技術手段獲得不良收益。

解決上述問題,主要有如下的三個思路:訪問攔截,分流,限流。

主流的Web站點採用分層的架構設計,如果你的應用還沒有采用分層的架構,那麼先做分層設計吧。一般來說,瀏覽器採用了html/js/css技術,負責數據的展示;反向代理一般採用nginx,負責負載均衡;Web層是指Php,Tomcat等應用服務器,負責用戶狀態的維護,http協議處理等;service層一般是rpc調用,當然也有用http的,例如spring cloud;數據庫存儲一般是mongodb,mysql等持久化數據方案。用戶的一次數據訪問,例如查詢商品庫存,數據是從上層依次調用到DB,逐層返回數據。

所謂訪問攔截,是指儘量把訪問攔截在上層,減輕下一層的壓力,即離用戶訪問更近的那一層。下面將從每一層講解如何做訪問攔截。

秒殺搶購系統優化思路詳解

瀏覽器訪問攔截:產品層面,當用戶點擊查詢或購買按鈕後,按鈕置灰,防止用戶重複提交數據。js層面,限制用戶在限定時間內的接口調用次數,或者返回相同的值。例如,用戶重複刷新,每秒訪問10次接口,變成5秒鐘訪問一次,併發量將會降低50倍。此種方法,可以攔截90%的小白用戶的訪問,但是技術型的用戶可以繞過js,通過腳本或其他自動化方式調用接口,當年出現的刷票神器,就屬於這類範疇。用戶量雖小,但是訪問量很大。關於防作弊的問題,後續討論。

CDN加速:CDN的全稱是Content Delivery Network,即內容分發網絡。其基本思路是儘可能避開互聯網上有可能影響數據傳輸速度和穩定性的瓶頸和環節,使內容傳輸的更快、更穩定。簡單的來說,就是把原服務器上數據複製到其他服務器上,用戶訪問時,那臺服務器近訪問到的就是那臺服務器上的數據。CDN的劣勢是內容的變更生效慢,所以僅適用於“幾乎不變”的資源,例如引用的js包,圖片等。

動靜分離與反向代理層訪問攔截:動態頁面是指根據實時數據渲染的,需要組織數據、渲染頁面;靜態頁面是存儲在文件系統的文件,不會根據數據變化而變化,讀取速度很快。為了提升效率,應儘可能的靜態化,用靜態頁面,替換動態頁面。例如,商品信息頁,商品信息在發佈後,是不會變化的,如果採用動態的方式,訪問數據庫讀取數據,service組裝數據,web渲染數據;如果發佈商品信息時,就保存下商品信息的靜態頁面,訪問時只需要讀取一個文件就夠了。

做了動靜分離,靜態文件的訪問應在哪一層返回?無論是tomcat,還是apache,都支持靜態文件的訪問,很多時候我們也是這麼做的,把靜態文件作為web項目的一部分進行發佈。Nginx也支持靜態文件的訪問,更高效的做法是,把靜態文件交由nginx管理,訪問nginx直接返回靜態數據,減輕Web服務的壓力。

Web層和Service層訪問攔截:通過上述的訪問攔截,進入到web層的,都是動態數據訪問。這部分的訪問攔截,主要採用緩存的策略,減少對下一層的數據訪問。緩存又可分為本地緩存和redis、memcache等緩存中間件。關於緩存,重點關注緩存的淘汰策略。一般有三種方式:超時更新,定時更新,通知更新。

訪問攔截,除了減少向下一層的訪問,還大幅提高系統的支持用戶數。訪問攔截,大大減少了每次請求的處理時間,假設:每個請求原來需要200ms時間,10W的併發量,每秒鐘可處理50W的請求;通過訪問攔截,每個請求的處理時間下降到100ms,同樣的併發量,每秒鐘可處理100W的請求。

通過上述的分析,各層通過訪問攔截,系統架構演變成如下的結構。

秒殺搶購系統優化思路詳解

在併發量巨大的場景下,通過上述的優化遠遠不夠的,因為單臺服務器的處理能力是有限的,即便在當前硬件設備越來越便宜,也不可能無限擴容。分流就是指通過多臺服務器,併發的處理請求,減輕單臺服務的負載。

DNS輪詢:Nginx的處理能力是有限的,單臺服務器支持10W左右的併發訪問,沒有問題。如果更大的負載怎麼辦?Nginx是應用服務的入口,不能再應用服務這個層次增加服務器,提高併發處理能力。

秒殺搶購系統優化思路詳解

通過瀏覽器輸入域名訪問某個服務,其過程如圖所示。DNS輪詢是ISP提供的一個服務,不同的用戶訪問同一個域名,獲取到不同的IP地址。例如:給www.example.com配置4個IP地址,如果有40W的併發訪問,每個IP將會獲得10W的併發訪問。當然,域名的IP地址配置,可以支持不同的策略,例如按照電信運營商分配,按照地域分配等。

Nginx負載均衡:Nginx可以支持10W的併發訪問,而應用服務器卻達不到這個水準,tomcat一般支持1W的併發訪問就很好了。Nginx支持配置請求的代理策略,把請求路由到多個Web服務器處理。Nginx支持的負載均衡策略包括:輪詢,權重,ip_hash,fair,url_hash等。

分佈式架構的負載策略:Web層調用service,以及service之間的調用,每個service都需要部署多份。目前最常用的兩個框架技術,spring cloud和dubbo,都採用客戶端負載均衡策略,路由到service的不同實例。

Redis負載:redis是內存的緩存結構,非常高效,瓶頸在於網絡IO,支持幾十萬的QPS。redis分流,可考慮分片的設計,把數據分配到多臺服務器上,減輕每臺機器的負載。一般情況下,分片策略多用戶redis數據擴容方案。

Mysql讀寫分離:對寫請求,不適合做分流,因為分流後的數據同步是大問題,導致數據不一致。對於寫請求,一般採用讀寫分離的策略,並且可以多臺讀庫。讀庫應用MyIsam引擎,單獨設置合適的索引,提高讀性能。從庫並不是越多越好,因為從庫越多,數據延遲越嚴重,要保持好平衡。

通過上述的分析,各層通過分流策略,系統架構演變成如下的結構。

秒殺搶購系統優化思路詳解

訪問攔截和分流的策略,主要作用還是解決併發讀的問題。購買、支付等這類“寫請求”,不能像讀緩存一樣,寫緩存提高效率,數據持久化成功,才算交易成功。尤其搶購這種模式下,商品數量少,如果多臺服務同事寫數據,將造成mysql嚴重的行鎖衝突,執行效率遠遠不如順序執行。並且大量的所等待,延長單個操作的時長,佔用工作線程,產生服務雪崩現象,短時間內不能對外提供服務。解決此問題的思路是限流,限制寫操作的流量,使其正常運行,不影響業務。

計數器:假設總共100個商品庫存,供大家搶貨,併發訪問極大。可以在Web層做一個計數器,搶單一次計數器加1,計數器到達100後,直接返回搶購失敗。同樣的道理,計數器亦可在service層實現。這種情況下,假設有10臺web服務器,也只會放行100 * 10 = 1000次搶購。

按商品路由:在Web層,把對同一品類商品的搶購路由到一臺service處理。在service內,自定義mysql連接池,使對同一個商品的操作,使用同一個連接。這樣就實現了對同一商品的順序處理,避免了鎖競爭。

異步化:是指把購買請求的接受和處理異步化。購買請求先放到隊列中,這個過程非常高效,返回客戶信息。搶購服務訂閱消息隊列,異步處理購買請求,處理成功給用戶發消息。異步化主要解決成產和消費的速度不匹配問題,由此類場景都可以採用。

秒殺搶購系統優化思路詳解

對於防作弊問題,是比較容易處理的。因為所有的購買,都是登陸用戶的行為,可以很方便的根據用戶ID進行過濾,只允許一個客戶購買一次。在分佈式環境下,要解決如何記錄用戶ID的問題,因為同一個用戶可能被不同的web,不同的service處理。

全局Cache:在redis中開闢一個空間,記錄所有用戶的商品購買,處理用戶購買請求是,校驗緩存中是否已記錄此商品的購買,如果已經購買,則不允許。要解決重複提交的問題,可考慮分佈式鎖。

用戶ID路由:參考上一節的按商品路由,我們同樣可以把對一個用戶的處理,路由到同一個Service處理,只需要做本地緩存就夠了。此種方案最大的問題是,如果服務掛了,數據就錯亂了。


分享到:


相關文章: