前幾年,火車票購票網站12306,每到放假高峯期,在線票刷不出來,購買困難,甚至出現了各類插件支持搶票,這樣的場景,對於每一個買過票的人,應該印象深入。小米手機的搶購活動,一樣異常火爆,在幾分鐘的時間內,賣出幾十萬部手機。當一個Web系統,在一秒鐘內收到數以萬計甚至更多請求時,系統的優化和穩定相當重要。
在面試中,面試官提出這樣的問題,應該從哪些角度分析。在工做中,也許沒有這麼龐大複雜的應用場景,可是,針對網站的優化思路是一致的。本文從技術的角度,分析下應如何設計優化系統,才能保障如此大規模的併發訪問。
秒殺系統主要解決三大問題:
1、瞬時的高併發訪問。搶購和普通的電商銷售有所不一樣,普通的電商銷售,流量是比較平均的,雖然有波峯波谷,但不會特別突出。而搶購是在特定時間點進行的推銷活動,搶購開始前,用戶不斷刷新頁面,以得到購買按鈕;搶購開始的一瞬間,集中併發購買。
2、數據正確性。搶購畢竟是一種購買行爲,須要購買、扣減庫存、支付等複雜的流程,在此過程當中,要保證數據的正確性,防止超賣(賣出量超過庫存)的發生。
3、防做弊。不管是火車票的購買,仍是低價商品的促銷,確定不但願某些客戶買到全部的商品,應儘可能保證公平性。經過購票插件購買火車票,阿里巴巴搶月餅事件等,須要限制技術性用戶繞過網站的限制,經過技術手段得到不良收益。
解決上述問題,主要有以下的三個思路:訪問攔截,分流,限流。
主流的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處理,只須要作本地緩存就夠了。此種方案最大的問題是,若是服務掛了,數據就錯亂了。