一個大型網(wǎng)站應(yīng)用一般都是從最初小規(guī)模網(wǎng)站甚至是單機應(yīng)用發(fā)展而來的闲擦,為了讓系統(tǒng)能夠支持足夠大的業(yè)務(wù)量,從前端到后端也采用了各種各樣技術(shù)场梆,前端靜態(tài)資源壓縮整合墅冷、使用CDN、分布式SOA架構(gòu)或油、緩存寞忿、數(shù)據(jù)庫加索引、讀寫分離等等顶岸。 這些技術(shù)是高并發(fā)系統(tǒng)所必須的腔彰,但是今天先不細(xì)說叫编,而先談?wù)勗谶@些架構(gòu)既定的情況下,一些高并發(fā)業(yè)務(wù)/接口實現(xiàn)時應(yīng)該注意的原則霹抛,以及通過工作中一個6萬QPS的秒殺活動搓逾,來介紹一下秒殺業(yè)務(wù)的特點以及如何優(yōu)化。
高并發(fā)系統(tǒng)設(shè)計原則
高并發(fā)的接口/系統(tǒng)有一個共同的特性杯拐,那就是”快”霞篡。
在系統(tǒng)其它條件既定的情況下,系統(tǒng)處理請求越快端逼,用戶得到反饋的時間就越短朗兵,單位時間內(nèi)服務(wù)器能夠處理請求的數(shù)量就會越多。所以”快”幾乎可以算是高并發(fā)系統(tǒng)的要滿足的必要條件顶滩,要評估一個系統(tǒng)性能如何余掖,某次優(yōu)化是否提高系統(tǒng)的容量,”快”是一個很直觀的衡量標(biāo)準(zhǔn)诲祸。
那么浊吏,如何才能做得快呢而昨?有兩個需要注意的原則 :
做得少救氯,一方面是指在功能特性上有所為,有所不為歌憨,另一方面是指一次處理的信息量要少着憨。
做得巧,根據(jù)業(yè)務(wù)自身的特點务嫡,選擇合理的業(yè)務(wù)實現(xiàn)方式甲抖,選擇合理的緩存類型和緩存調(diào)用時機。
做得少
世界上最快的程序心铃,是什么都不做的程序准谚。
一個接口負(fù)責(zé)的功能越少,讀取信息量越少去扣,速度越快柱衔。
功能特性有選擇
對于一個需要承受高并發(fā)的接口,在功能上愉棱,盡量不涉及一些難以緩存和預(yù)熱的數(shù)據(jù)唆铐。 一個典型的例子,用戶維度個性化的數(shù)據(jù)奔滑,用戶和用戶的信息不同艾岂,userId數(shù)量又很多,即使加上緩存朋其,緩存命中率依然很低王浴,壓力還是會打到數(shù)據(jù)庫脆炎,不光接口快不了,高并發(fā)的sql也會給數(shù)據(jù)庫帶來風(fēng)險叼耙。
舉一個例子腕窥,在點評電影早期的秒殺活動頁上,展示了一個用戶當(dāng)前秒殺資格的信息筛婉,由于不同用戶搶到秒殺資格的時間簇爆、優(yōu)惠不同,每次都需要讀數(shù)據(jù)庫的來取爽撒,也就是每個用戶進入主頁都會產(chǎn)生一條sql入蛆。
還有一個例子,一般電商搞大促的時候硕勿,比如同時有多個優(yōu)惠活動可以降低商品的價格哨毁,而一般只展示最低價的優(yōu)惠,同時用戶一個優(yōu)惠只能參與一次源武,這樣不同用戶參與了不同活動之后可以享受的最低價就會隨之改變扼褪,如果要在商品頁面上展示這個動態(tài)價格,就免不了取到各個用戶參加這些在線優(yōu)惠的信息粱栖。
如果遇到這樣的數(shù)據(jù)话浇,要怎么解決呢?
一個辦法是嘗試轉(zhuǎn)移數(shù)據(jù)的維度:剛才說的秒殺活動資格信息闹究,如果以用戶userId為key幔崖,會出現(xiàn)緩存命中率低,仍要sql讀的情況渣淤,但是能夠秒到的用戶數(shù)量其實很少赏寇,所以如果以這次秒殺活動id為key,存儲一個成功秒到用戶的userid的list价认,就能夠解決緩存命中率低的問題嗅定。
還有一個辦法是可以把這些需要個性化數(shù)據(jù)的功能在業(yè)務(wù)流程上后移,流量漏斗用踩,越往后流量越少渠退,創(chuàng)建訂單級的sql查詢是可接受的。 剛才說的第二個例子捶箱,商品最優(yōu)惠的價格智什,可以排除用戶相關(guān)信息,只在商品列表/詳情上展示只和優(yōu)惠相關(guān)的最低價丁屎,而在提交訂單的時候才真正去取用戶參加活動情況荠锭,如果用戶已經(jīng)參加過給出提示并選擇次優(yōu)的優(yōu)惠。商品的列表/詳情頁都在用戶路徑上相對靠前的位置晨川,排除了用戶個性化信息可以讓商品列表/詳情更容易緩存证九,響應(yīng)速度更快删豺,系統(tǒng)可承受的高并發(fā)量更高。
處理信息量要少
我們寫業(yè)務(wù)代碼的時候都有對應(yīng)的業(yè)務(wù)對象愧怜,它們都存在一定的業(yè)務(wù)范圍之內(nèi)呀页,比如類目、地區(qū)拥坛、日期等自身相關(guān)的維度蓬蝶。 一個系統(tǒng)中的業(yè)務(wù)對象,在多個維度的細(xì)分下猜惋,對應(yīng)的量并不多丸氛,但如果一次全部都展示在一個頁面/接口下,即使覆蓋上了緩存著摔,也會由于緩存占用空間過大或者緩存key數(shù)目過多缓窜、網(wǎng)絡(luò)傳輸耗時、對象序列化反序列耗時等拖慢接口/頁面響應(yīng)速度谍咆。一般只要看一下這個頁面/接口給出的業(yè)務(wù)對象的數(shù)量級禾锤,就能大致知道這個接口的性能了。
大家在做設(shè)計的時候摹察,一般會估算一個接口的量級恩掷,如果一看就有幾千幾萬個業(yè)務(wù)對象,就不會這樣設(shè)計了港粱,但是需要警惕的是業(yè)務(wù)對象數(shù)量級可變的情況螃成,比如隨著業(yè)務(wù)發(fā)展數(shù)量會快速增長旦签,或者某些特殊維度下業(yè)務(wù)對象特別多查坪。設(shè)計的時候要按照預(yù)估的最大量級來,并且對接口/頁面做出數(shù)量的限制宁炫,如果發(fā)現(xiàn)當(dāng)前返回的業(yè)務(wù)對象過多偿曙,可以繼續(xù)根據(jù)業(yè)務(wù)維度來拆分,分次分批來處理羔巢。
舉一個例子望忆,比如一個影院下所有的活動場次,開始的時候一家影院下的場次有限竿秆,幾十一百場启摄,很好展示,后來隨著業(yè)務(wù)發(fā)展幽钢,一個影院下各個影院下場次數(shù)到了幾百一千歉备,一次全部拿完,在高并發(fā)時匪燕,memcached緩存的multi get會出現(xiàn)很多超時蕾羊,請求會打到mysql數(shù)據(jù)庫喧笔,給系統(tǒng)很大壓力。之后我們做了改造項目龟再,每次根據(jù)用戶的交互按照影片书闸、日期、影院的維度來分批取利凑,一次只有十幾個場次浆劲,接口響應(yīng)變快了,服務(wù)的壓力也小的多哀澈。
做得巧
根據(jù)業(yè)務(wù)特性選擇實現(xiàn)方式
平時涉及到的業(yè)務(wù)梳侨,總有屬于它的特性,比如實時性要求多高日丹,數(shù)據(jù)一致性要求多高走哺,涉及什么維度的數(shù)據(jù),量有多大等等哲虾,我們要根據(jù)這些特性來選擇實現(xiàn)的方案丙躏,比如一些統(tǒng)計數(shù)據(jù),如某類目下所有商品的最低價束凑,按照邏輯需要遍歷商品來獲取晒旅,但這樣每次實時讀取所有的對象,涉及讀取緩存數(shù)據(jù)庫操作汪诉,接口會很耗時废恋,但如果選擇作業(yè)離線計算,把計算結(jié)果寫表扒寄,加上緩存鱼鼓,搜索直接讀取,顯然會快很多了该编。
涉及到業(yè)務(wù)各階段特性的例子就是秒殺系統(tǒng)迄本,在第二部分秒殺實踐中我會詳細(xì)介紹。
合適選擇和調(diào)用緩存
除了業(yè)務(wù)特性方面课竣,緩存是業(yè)務(wù)對抗高并發(fā)非常重要的一個環(huán)節(jié)嘉赎,合理選擇緩存的類型和調(diào)用緩存的時機非常重要。
我們知道內(nèi)存運算速度快于遠(yuǎn)程連接于樟,所以存儲上來說效率如下 內(nèi)存 <= ehcache < redis <= memcached < mysql 可以看出公条,盡量少的遠(yuǎn)程連接,常規(guī)覆蓋數(shù)據(jù)庫訪問的緩存迂曲,都能提高程序的性能靶橱。
要根據(jù)不同緩存的特性和原理,才能根據(jù)業(yè)務(wù)選出最合適的,來看看幾種常用的緩存 :
varnish抓韩,可以作為反向代理纠永,緩存一些資源,例如可以把struts谒拴,freemarker動態(tài)生成的頁面存儲起來尝江,達(dá)到直接擋掉到達(dá)web服務(wù)器的請求。
ehcache英上,主要存儲在當(dāng)前機器內(nèi)存中炭序,存取非常快苍日,缺點是內(nèi)存有限惭聂,各臺機器內(nèi)存中各存一份,失效時間不一致相恃,數(shù)據(jù)就會出現(xiàn)不一致辜纲,一般用來緩存不常變化,且緩存?zhèn)€數(shù)較少的數(shù)據(jù)拦耐。
memcached緩存耕腾,kv分布式緩存集群,可擴展性好杀糯,可以存儲個數(shù)較多的緩存對象扫俺,也可以承接高流量的訪問,讀取緩存時遠(yuǎn)程連接固翰,一般耗時也在零點幾到幾ms不等狼纬。
redis,nosql骂际,是內(nèi)存的kv存儲疗琉,可以做為緩存使用,也可以持久化方援,它的性能和memcached相近没炒。而redis最大的特點是一個data-structure store涛癌,這時redis官網(wǎng)首頁介紹redis的第一句話犯戏,它可以保存list,hash拳话,set先匪,sorted set等數(shù)據(jù)結(jié)構(gòu),使用時和memcached區(qū)別是弃衍,它不用將數(shù)據(jù)取到客戶端再做邏輯判斷呀非,而是可以直接在redis服務(wù)器上完成操作,比如查看某個元素是不是一個范圍內(nèi),隊列的長度有多長等岸裙。redis可以用來做分布式服務(wù)器的進程間的通信猖败,比如我們經(jīng)常有需要分布式鎖的場景,控制同一個用戶發(fā)券的并發(fā)等降允。
根據(jù)業(yè)務(wù)需要選擇了合適類型的緩存后恩闻,還要合理去使用。 雖然說緩存是為了抵擋數(shù)據(jù)庫的流量而生剧董,本身性能非常強大幢尚,但仍然是受到緩存服務(wù)器性能甚至服務(wù)器網(wǎng)卡流量的限制的,不合理的使用比如單個key對應(yīng)的緩存對象過大翅楼、一次讀取中緩存key數(shù)量過多尉剩、短時間內(nèi)頻繁更新緩存等都是系統(tǒng)的隱患、并發(fā)越高時就越能體現(xiàn)毅臊。
秒殺實踐
秒殺業(yè)務(wù)分析
秒殺業(yè)務(wù)的典型特點有:
瞬時流量大
參與用戶多理茎,可秒殺商品數(shù)量少
請求讀多寫少
秒殺狀態(tài)轉(zhuǎn)換實時性要求高
一次秒殺的流程可以分為三個階段:
活動未開始
活動開始前,用戶進入活動頁管嬉,這個階段有兩種請求功蜓,一種是加載活動頁信息,一個是查詢活動狀態(tài)得到未開始的結(jié)果宠蚂, 一個用戶進入頁面兩個請求各發(fā)起一次式撼,這兩種請求占比各半。
活動進行中
這個階段持續(xù)時間非常短求厕,看到搶購按鈕的用戶大量發(fā)起秒殺請求著隆,瞬時秒殺請求占比增高,能不能抗住秒殺請求就是秒殺系統(tǒng)是否能抗住高并發(fā)的關(guān)鍵呀癣。
活動結(jié)束
當(dāng)商品被搶購?fù)昝榔郑M入結(jié)束狀態(tài),請求情況同活動開始前
各階段流量圖
其實貫穿整個活動的只有三種請求项栏,加載活動頁請求浦辨,讀取活動狀態(tài)請求,秒殺請求加載活動頁請求
主要是展示活動相關(guān)配置信息沼沈,活動背景圖片流酬,優(yōu)惠力度,活動規(guī)則等相對靜態(tài)的內(nèi)容列另,通過web項目渲染成頁面芽腾。
對于這樣的請求,我們可以使用varnish反向代理页衙,以頁面相關(guān)的參數(shù)比如本次秒殺的活動ID和城市ID的hash為key把整個頁面緩存在varnish機器上摊滔,而秒殺活動的狀態(tài)等動態(tài)信息通過ajax來刷新阴绢。
varnish作用機制
達(dá)到的效果是活動期間,加載頁面請求都會打到varnish機器直接返回艰躺,而不會給web和service帶來任何壓力呻袭。
查詢活動狀態(tài)
秒殺狀態(tài)就三種,未開始腺兴,可搶棒妨,已搶完,由兩個因素共同決定
活動開始時間
剩余庫存
讀取秒殺狀態(tài)的請求數(shù)并發(fā)也是非常高的含长,對于這個接口也要加上合適的緩存來處理券腔。 對于活動開始時間,是一個較固定且不會發(fā)生變化的屬性拘泞,并且纷纫,同時在線的秒殺活動數(shù)目并不多,所以把它也作為discount相關(guān)的信息陪腌,選擇用響應(yīng)快的ehcache來緩存辱魁。
對于庫存,剩余庫存?zhèn)€數(shù)诗鸭,一般來說是全局需要一致的染簇,可以用memcached來緩存,在秒殺的過程中强岸,庫存變化的非扯凸快,如果直接對庫存?zhèn)€數(shù)進行緩存蝌箍,那么秒殺期間就需要頻繁的更新緩存青灼,像之前說的,雖然緩存是用來扛并發(fā)的妓盲,但要調(diào)用緩存的時機也要合理杂拨,memcached處理的并發(fā)請求越少,相對成功率就會越高悯衬。 其實對于秒殺活動來說弹沽,當(dāng)時的剩余庫存數(shù)在秒殺期間變化非常快筋粗,某個時間點上的庫存?zhèn)€數(shù)并沒有太大的意義策橘,而用戶更關(guān)心的是 能不能搶,true or false亏狰。如果緩存true or false的話役纹,這個值在秒殺期間是相對穩(wěn)定的,只需要在庫存耗盡的時候更新一次暇唾,而且為了防止這一次的更新失敗,可以重復(fù)更新,利用memcached的cas操作策州,最后memcached也只會真正執(zhí)行一次set寫操作瘸味。 因為秒殺期間查詢活動狀態(tài)的請求都打在memcached上,減少寫的頻率可以明顯減輕memcached的負(fù)擔(dān)够挂。
其實活動狀態(tài)除了活動時間和庫存之外旁仿,還有第三個因素來決定,下面說到秒殺請求的優(yōu)化時會詳細(xì)來說
秒殺請求
秒殺請求分析
秒殺請求是一個秒殺系統(tǒng)能不能抗住高并發(fā)的關(guān)鍵 因為秒殺請求和之前兩個請求不同孽糖,它是寫請求枯冈,不能緩存,而且是活動峰值的主力办悟。
一個用戶從發(fā)出秒殺請求到成功秒殺簡單地說需要兩個步驟: 1. 扣庫存 2. 發(fā)送秒殺商品 這是至少兩條數(shù)據(jù)庫操作尘奏,而且扣庫存的這一步,在mysql的innodb引擎行鎖機制下病蛉,update的sql到了數(shù)據(jù)庫就開始排隊炫加,期間數(shù)據(jù)庫連接是被占用的,當(dāng)請求足夠多時就會造成數(shù)據(jù)庫的擁堵铺然。 可以看出俗孝,秒殺請求接口是一個耗時相對長的接口,而且并發(fā)越高耗時越長魄健,所以首先赋铝,一定要限制能夠真正進行秒殺的人數(shù)。
秒殺流程圖
上面說了沽瘦,秒殺業(yè)務(wù)的一個特點是參與人數(shù)多柬甥,但是可供秒殺的商品少,也就是說只有極少部分的用戶最終能夠秒殺成功 比如有2500個名額其垄,理論上來說先發(fā)送請求的2500個用戶能夠秒殺成功苛蒲,這2500個用戶扣庫存的sql在數(shù)據(jù)庫排隊的時候,庫存還沒有消耗完绿满,比如2500個請求臂外,全部排隊更新完是需要時間的,就比如說0.5s 在這個時間內(nèi)喇颁,用戶會看到當(dāng)前仍然是可搶狀態(tài)漏健,所以這段時間內(nèi)持續(xù)會有秒殺請求進入,秒殺的高峰期橘霎,0.5秒也有幾萬的請求蔫浆,讓幾萬條sql來競爭是沒有意義的,所以要限制這些參與到扣庫存這一步的人數(shù)姐叁。
秒殺隊列校驗
可搶狀態(tài)需要第三個因素來決定瓦盛,那就是當(dāng)前秒殺的排隊人數(shù)洗显。 加在判斷庫存剩余之前,擋上一層排隊人數(shù)的校驗原环, 即有庫存 并且 排隊人數(shù) < 限制請求數(shù) = 可搶挠唆,有庫存 并且 排隊人數(shù) >= 限制請求數(shù) = 搶完
比如2500個名額秒殺名額,目標(biāo)放過去3000個秒殺請求
那么排隊人數(shù)記在哪里嘱吗? 這個可以有所選擇玄组,如果只記請求個數(shù),可以用memcached的計數(shù)谒麦,一個用戶進入秒殺流程increase一次俄讹,判斷庫存之前先判斷隊列長度,這樣就限制了可參與秒殺的用戶數(shù)量绕德。
排隊秒殺流程圖
發(fā)起秒殺先去問排隊隊列是不是已滿患膛,滿了直接秒殺失敗,同時可以去更新之前緩存了是否可搶 true or false的緩存迁匠,直接把前臺可搶的狀態(tài)變?yōu)椴豢蓳屖F俊]滿繼續(xù)查詢庫存等后續(xù)流程,開始扣庫存的時候城丧,把當(dāng)前用戶id入隊延曙。 這樣,就限制了真正進入秒殺的人數(shù)亡哄。
這種方法枝缔,可能會有一個問題,既然限制了請求數(shù)蚊惯,那就必須要保證放過去的用戶能夠秒完商品愿卸,假設(shè)有重復(fù)提交的用戶,如果重復(fù)提交的量大截型,比如放過去的請求中有一半都是重復(fù)提交趴荸,就會造成最后沒秒完的情況,怎么屏蔽重復(fù)用戶呢宦焦? 就要有個地方來記參與的用戶id发钝,可以使用redis的set結(jié)構(gòu)來保存,這個時候set的size代表當(dāng)前排隊的用戶數(shù)波闹,扣庫存之前add當(dāng)前用戶id到set酝豪,根據(jù)add是否成功的結(jié)果,來判斷是否繼續(xù)處理請求精堕。
最終孵淘,把實際上幾萬個參與數(shù)據(jù)庫操作的用戶從減少到秒殺商品的級別,這是一個數(shù)據(jù)庫可控制的范圍歹篓,即使參與的用戶再多瘫证,實際上也只處理了秒殺商品數(shù)量級的請求揉阎。
更多的優(yōu)化
1.分庫存 一般這樣做就已經(jīng)能夠滿足常規(guī)秒殺的需求了,但有一個問題依然沒有解決痛悯,那就是加鎖扣庫存依然很慢 假設(shè)的活動秒殺的商品量能夠再上一個量級余黎,像小米賣個手機重窟,一次有幾W到幾十萬的時候载萌,數(shù)據(jù)庫也是扛不住這個量的,可以先把庫存數(shù)放在redis上巡扇,然而單一庫存加鎖排隊依然存在扭仁,庫存這個熱點數(shù)據(jù)會成為扣庫存的瓶頸。
一個解決的辦法是 分庫存厅翔,比如總共有50000個秒殺名額乖坠,可以分50份,放在redis上的50個不同的key刀闷,那么每份上1000個庫存熊泵,用戶進入秒殺流程后隨機到其中一個庫存來修改,這樣有50個庫存數(shù)來競爭甸昏,縮短請求的排隊時間顽分。
這樣專門為高并發(fā)設(shè)計的系統(tǒng)最大的敵人 是低流量,在大部分庫存都好近施蜜,而有幾個剩余庫存時卒蘸, 用戶會看到明明還能搶卻總是搶不到,而在高并發(fā)下翻默,用戶根本就覺察不到缸沃。
2.異步消息 如果有必要繼續(xù)優(yōu)化,就是扣庫存和發(fā)貨這兩個費時的流程修械,可以改為異步趾牧,得到秒殺結(jié)果后通過短信/push異步通知用戶。 主要是利用消息系統(tǒng)削峰填谷的特性 來增加系統(tǒng)的容量肯污。
秒殺總結(jié)
流量圖
先用varnish擋掉了所有的讀取狀態(tài)請求 然后用ehcache緩存活動時間翘单,擋掉活動未開始時查詢活動狀態(tài)的請求 memcached緩存是否可搶的狀態(tài),擋掉活動開始后到結(jié)束狀態(tài)的活動查詢請求 redis隊列擋掉了活動進行中仇箱,過量的秒殺請求 到最后只留下了秒殺商品數(shù)量級的請求到數(shù)據(jù)庫中县恕。
其實做為一個開發(fā)者,有一個學(xué)習(xí)的氛圍跟一個交流圈子特別重要剂桥,這里我推薦一個Java交流群730379855忠烛,不管你是小白還是大牛歡迎入駐,大家一起交流成長。