推薦:《PHP影片教學》
一、秒殺業務為什麼難做
1)im系統,例如qq或微博,每個人都讀自己的資料(好友列表、群列表、個人資訊);
2)微博系統,每個人讀你關注的人的數據,一個人讀多個人的數據;
#3)秒殺系統,庫存只有一份,所有人會在集中的時間讀取和寫這些數據,多個人讀一個數據。
例如:小米手機每週二的秒殺,可能手機只有1萬部,但瞬時進入的流量可能是幾百幾千萬。
又例如:12306搶票,票是有限的,庫存一份,瞬時流量非常多,都讀相同的庫存。 讀寫衝突,鎖非常嚴重,這是秒殺業務困難的地方。那我們要怎麼優化秒殺業務的架構呢?
二、最佳化方向
最佳化方向有兩個(今天就講這兩個點):
(1)將請求盡量攔截在系統上游(不要讓鎖定衝突落到資料庫上去)。傳統秒殺系統之所以掛,請求都壓倒了後端資料層,資料讀寫鎖定衝突嚴重,並發高回應慢,幾乎所有請求都超時,流量雖大,下單成功的有效流量甚小。以12306為例,一班火車其實只有2000張票,200w個人來買,基本上沒有人能買成功,請求有效率為0。
(2)充分利用快取,秒殺買票,這是一個典型的讀多些少的應用場景,大部分請求是車次查詢,票查詢,下單和支付才是寫請求。一班火車其實只有2000張票,200w個人來買,最多2000個人下單成功,其他人都是查詢庫存,寫比例只有0.1%,讀比例佔99.9%,非常適合使用緩存來優化。好,後續講講怎麼個「將請求盡量攔截在系統上游」法,以及怎麼個「快取」法,講講細節。
三、常見秒殺架構
常見的網站架構基本上是這樣的(絕對不畫忽悠類別的架構圖)
(1)瀏覽器端,最上層,會執行到一些JS程式碼
(2)服務端,這一層會存取後端數據,拼html頁面回傳給瀏覽器
(3)服務層(web伺服器),向上游屏蔽底層資料細節,提供資料存取
(4)資料層,最終的庫存是存在這裡的,mysql是一個典型(當然還有會緩存)
這個圖雖然簡單,但能形象的說明大流量高並發的秒殺業務架構,大家要記得這張圖。
後面要細解析各層級怎麼優化。
四、各層次優化細節
第一層,客戶端怎麼優化(瀏覽器層,APP層)
問大家一個問題,大家都玩過微信的搖一搖搶紅包對吧,每次搖一搖,就會往後端發送請求麼?回顧我們下單搶票的場景,點擊了“查詢”按鈕之後,系統那個卡呀,進度條漲的慢呀,作為用戶,我會不自覺的再去點擊“查詢”,對麼?繼續點,繼續點,點點點。 。 。有用麼?平白無故的增加了系統負載,一個用戶點5次,80%的請求是這麼多出來的,怎麼整?
(a)產品層面,使用者點選「查詢」或「購票」後,按鈕置灰,禁止使用者重複提交請求;
(b)JS層面,限制使用者在x秒之內只能提交一次請求;
APP層面,可以做類似的事情,雖然你瘋狂的在搖微信,其實x秒才向後端發起一次請求。這就是所謂的“將請求盡量攔截在系統上游”,越上游越好,瀏覽器層,APP層就給攔住,這樣就能擋住80% 的請求,這種辦法只能攔住普通用戶(但99%的用戶是普通用戶)對於群組內的高階程式設計師是攔不住的。 firebug一抓包,http長啥樣都知道,js是萬萬攔不住程式設計師寫for循環,呼叫http介面的,這部分請求怎麼處理?
第二層,服務端層面的請求攔截
怎麼攔截? 怎麼防止程式設計師寫for循環呼叫,有去重依據麼? ip? cookie-id? …想複雜了,這類業務都需要登錄,用uid即可。在服務端層面,對uid進行請求計數和去重,甚至不需要統一儲存計數,直接服務端記憶體儲存(這樣計數會不准,但最簡單)。一個uid,5秒只準透過1個請求,這樣又能攔住99%的for循環請求。
5s只透過一個請求,其餘的請求怎麼辦?緩存,頁面緩存,同一個uid,限制訪問頻度,做頁面緩存,x秒內到達服務端的請求,均返回同一頁。同一個item的查詢,例如車次,做頁面緩存,x秒內到達服務端的請求,均返回同一頁。如此限流,既能保證用戶有良好的用戶體驗(沒有返回404)又能保證系統的健壯性(利用頁面緩存,把請求攔截在服務端了) 。
頁面快取不一定要保證所有服務端傳回一致的頁面,直接放在每個網站的記憶體也是可以的。優點是簡單,壞處是http請求落到不同的服務端,回傳的車票資料可能不一樣,這是服務端的請求攔截與快取優化。
好,這個方式攔住了寫for循環發http請求的程式設計師,有些高階程式設計師(駭客)控制了10w個肉雞,手裡有10w個uid,同時發請求(先不考慮實名制的問題,小米搶手機不需要實名制),這下怎麼辦,服務端按照uid限流攔不住了。
第三層 服務層來攔截(反正就是不要讓請求落到資料庫上去)
服務層怎麼攔截?大哥,我是服務層,我清楚的知道小米只有1萬支手機,我清楚的知道一列火車只有2000張車票,我透10w個請求去數據庫有什麼意義呢?沒錯,請求隊列!
對於寫入請求,做請求隊列,每次只透有限的寫入請求去資料層(下訂單,支付這樣的寫業務)
1w部手機,只透1w個下單請求去db
3k張火車票,只透3k個下單請求去db
如果均成功再放下一批,如果庫存不夠則隊列裡的寫請求全部返回「已售完」。
對於讀取請求,怎麼優化? cache抗,不管是memcached還是redis,單機抗個每秒10w應該都是沒什麼問題的。如此限流,只有非常少的寫入請求,和非常少的讀取快取mis的請求會透到資料層去,又有99.9%的請求被攔住了。
當然,還有業務規則上的一些最佳化。回想12306所做的,分時分段售票,原來統一10點賣票,現在8點,8點半,9點,...每隔半小時放出一批:將流量攤勻。
其次,資料粒度的最佳化:你去購票,對於餘票查詢這個業務,票剩了58張,還是26張,你真的關注麼,其實我們只關心有票和無票?流量大的時候,做一個粗粒度的「有票」「無票」快取即可。
第三,一些業務邏輯的非同步:例如下單業務與 支付業務的分離。這些最佳化都是結合 業務 來的,我之前分享過一個觀點「一切脫離業務的架構設計都是耍流氓」架構的最佳化也要針對業務。
好了,最後是資料庫層
瀏覽器攔截了80%,服務端攔截了99.9%並做了頁面緩存,服務層又做了寫請求佇列與資料緩存,每次透到資料庫層的請求都是可控的。 db基本上就沒什麼壓力了,閒庭信步,單機也能扛得住,還是那句話,庫存是有限的,小米的產能有限,透這麼多請求來數據庫沒有意義。
全部透到資料庫,100w個下單,0個成功,請求有效率0%。透3k個到數據,全部成功,請求有效率100%。
五、總結
上文應該描述的非常清楚了,沒什麼總結了,對於秒殺系統,再次重複下我個人經驗的兩個架構優化思路:
(1)盡量將請求攔截在系統上游(越上游越好);
(2)讀多寫少的常用多使用快取(快取抗讀壓力);
瀏覽器與APP:做限速
服務端:依照uid做限速,做頁面快取
服務層(網頁伺服器):依照業務做寫入要求佇列控制流量,做資料快取
資料層:閒庭訊號步驟
並且:結合業務做最佳化
六、Q&A
問題1、按你的架構,其實壓力最大的反而是服務端,假設真實有效的請求數有1000萬,不太可能限制請求連接數吧,那麼這部分的壓力怎麼處理?
答:每秒鐘的並發可能沒有1kw,假設有1kw,解決方案2個:
(1)服務層(web伺服器)是可以透過加機器擴容的,最不濟1k台機器來嗆。
(2)如果機器不夠,拋棄請求,拋棄50%(50%直接回傳稍後再試),原則是要保護系統,不能讓所有使用者都失敗。
問題2、「控制了10w個肉雞,手上有10w個uid,同時發請求」 這個問題怎麼解決哈?
答:上面說了,服務層(web伺服器)寫入請求佇列控制
問題3:限制存取頻次的緩存,是否也可以用來搜尋?例如A用戶搜尋了“手機”,B用戶搜尋“手機”,優先使用A搜尋後產生的快取頁面?
答:這個是可以的,這個方法也常用在「動態」營運活動頁,例如短時間推送4kw用戶app-push營運活動,做頁面快取。
問題4:如果佇列處理失敗,該如何處理?肉雞把隊列被撐爆了怎麼辦?
答案:處理失敗回傳下單失敗,讓使用者再試一次。隊列成本很低,爆了很難吧。在最壞的情況下,快取了若干請求之後,後續請求都直接回傳「無票」(佇列裡已經有100w請求了,都等著,再接受請求也沒有意義了)
問題5 :服務端過濾的話,是把uid請求數單獨儲存到各個網站的記憶體中麼?如果是這樣的話,怎麼處理多台伺服器叢集經過負載平衡器將相同使用者的回應分散到不同伺服器的情況呢?還是說將服務端的過濾放到負載平衡前?
答:可以放在內存,這樣的話看似一台伺服器限制了5s一個請求,全局來說(假設有10台機器),其實是限制了5s 10個請求,解決辦法:
1)加大限制(這是建議的方案,最簡單)
2)在nginx層做7層均衡,讓一個uid的請求盡量落到同一個機器上
問題6:服務層(網頁伺服器)過濾的話,佇列是服務層(網頁伺服器)統一的一個佇列?還是每個提供服務的伺服器各一個佇列?如果是統一的一個佇列的話,需不需要在各伺服器提交的請求入佇列前進行鎖定控制?
答:可以不用統一一個佇列,這樣的話每個服務透過更少量的請求(總票數/服務個數),這樣簡單。統一一個隊列又複雜了。
問題7:秒殺後的支付完成,以及未支付取消佔位,如何對剩餘庫存做及時的控制更新?
答案:資料庫裡一個狀態,未支付。如果超過時間,例如45分鐘,庫存會重新會恢復(大家熟知的「回倉」),給我們搶票的啟示是,開動秒殺後,45分鐘之後再試試看,說不定又有票喲~
問題8:不同的使用者瀏覽同一個商品 落在不同的快取實例顯示的庫存完全不一樣 請問老師怎麼做快取資料一致或是允許髒讀?
答:目前的架構設計,請求落在不同的網站上,資料可能不一致(頁面快取不一樣),這個業務場景能接受。但資料庫層面真實資料是沒問題的。
問題9:就算處於業務把優化考慮「3k張火車票,只透3k個下單請求去db」那這3K個訂單就不會發生擁堵了嗎?
答:(1)資料庫抗3k個寫請求還是ok的;(2)可以資料拆分;(3)如果3k扛不住,服務層(web伺服器)可以控制透過去的並發數量,根據壓測情況來吧,3k只是舉例;
問題10;如果在服務端或服務層(web伺服器)處理後台失敗的話,需不需要考慮對這批次失敗的請求做重播?還是就直接丟棄?
答:別重播了,回傳使用者查詢失敗或下單失敗吧,架構設計原則之一是「fail fast」。
問題11.對於大型系統的秒殺,例如12306,同時進行的秒殺活動很多,如何分流?
答案:垂直拆分
問題12、額外又想到一個問題。這套流程做成同步還是非同步的?如果是同步的話,應該還存在會有回應回饋慢的情況。但如果是異步的話,如何控制能夠將回應結果傳回正確的請求方?
答:使用者層面肯定是同步的(使用者的http請求是夯住的),服務層(web伺服器)面可以同步可以非同步。
問題13、秒殺群問題:減庫存是在那個階段減呢?如果是下單鎖庫存的話,大量惡意用戶下單鎖庫存而不支付如何處理呢?
答:資料庫層面寫請求量很低,還好,下單不支付,等時間過完再“回倉”,之前提過了。
技巧:值的我們注意的地方
1、脫離原始網站部署(秒殺功能用的伺服器和商城伺服器不要放在同一個伺服器,防止秒殺崩咯,商城也不能訪問...)
2、多監控、注意監控、找個人盯著看
秒殺的關鍵點:
1、高可用:雙活
2、高並發:負載平衡、安全過濾
設計想法:
1、靜態頁面:cdn(使用各大廠商現成的)、網址隱藏、頁面壓縮、快取機制
2、動態頁面:排隊、非同步、資格搶購
其他建議:
##1、百度的建議:opcode快取、cdn 、更大的伺服器實例2、阿里的建議:雲端監控、雲端盾牌、ecs、oss、rds、cdncdn的使用思路:##1、靜態資源(圖片、js、css等)上傳到cdn
2、缺點:但要注意,更新時cdn更新不及時,這時就要推了
#1環境、形式:
1、使用者:超大量、正常/壞人(cdn加速也是一種分流,因為他就近訪問cdn的節點)
2、地理:全國各地(延遲1、2s也不行,因為延遲1s可能秒殺就結束了,需要cdn讓用戶就近選擇節點)
3、業務流程:前台商品展示、登記。後台資料存取、資料處理
在秒殺前加個頁面,可以分流、推廣其他商品
##商品展示層架構
頁面的3個狀態:
1、商品展示:倒數計時頁#2、秒殺進行中:點選進入秒殺頁面3、秒殺活動結束:提示活動已結束新想法:以時間線來思考問題
假設我們看見的就是秒殺進行中,往前推就是倒數計時,往後推就是結束了
下圖是從使用者的角度看問題:
## : 靜態資源,存到oss裡部署 #先刪除倒數頁面,馬上給秒殺頁面複製過來然後用coretab定時執行這個腳本
#總結:從倒數計時到搶購中:是用linux的定時任務和shell腳本來做
搶購中倒搶購結束:php來做,查表沒了就結束
用戶登錄層架構#程式碼: #$.cookie的封裝
資料存取層
程式碼:
第2層到第3層的時候要怎麼計算? ?
第2層發送資料到第3層網路傳輸dns解析等因素總共造成的延遲大概是多久,這樣就可以評估,需要配置伺服器的數量
#總結為2句話:1、在關鍵點上不通過,直接回到上一層(使用者登記層)
2、在關鍵點通過,就會通知其他層
效驗:類似微軟序號的加密解密
佇列:使用redis的有序集合
上限:技術標誌位元
資料處理層#
#以上是分享php秒殺功能實現的思路的詳細內容。更多資訊請關注PHP中文網其他相關文章!