高并發(fā)秒殺系統(tǒng)總結(jié)
大家也許開發(fā)過(guò)高并發(fā)的系統(tǒng)或者秒殺程序,但肯定都有接觸過(guò),像電商平臺(tái)的秒殺、搶購(gòu)等活動(dòng),還有12306春運(yùn)搶票。
活動(dòng)周期短,瞬間流量大(高并發(fā)),技術(shù)在這種情況下,會(huì)發(fā)生和要做的事。
第一:高并發(fā)
技術(shù)要做的事,一方面優(yōu)化程序,讓程序性能最優(yōu),單次請(qǐng)求時(shí)間能從50ms優(yōu)化到25ms,那就可以在一秒鐘內(nèi)成功響應(yīng)翻倍的請(qǐng)求了。
另一方面就是增加服務(wù)器,用更大的集群來(lái)處理用戶請(qǐng)求,設(shè)計(jì)好一個(gè)可靠且靈活擴(kuò)充的分布式方案就更加重要了。
第二:時(shí)間短
火熱的秒殺活動(dòng),真的是一秒鐘以內(nèi)就會(huì)把商品搶購(gòu)一空,而大部分用戶的感受是,提交訂單的過(guò)程卻要等待好幾秒、甚至十幾秒,更糟糕的當(dāng)然是請(qǐng)求報(bào)錯(cuò)。
那么一個(gè)好的秒殺體驗(yàn),當(dāng)然希望盡可能減少用戶等待時(shí)間,準(zhǔn)確的提示用戶當(dāng)前是否還有商品庫(kù)存。而這些,也是需要有優(yōu)秀的程序設(shè)計(jì)來(lái)保證的。
第三:系統(tǒng)容量預(yù)估
系統(tǒng)設(shè)計(jì)的時(shí)候,都需要有一個(gè)容量預(yù)估,那就是要提前計(jì)算好,我們?cè)O(shè)計(jì)的系統(tǒng),要承載多大的數(shù)量級(jí)。
假如線上前端服務(wù)器規(guī)格是8核16G內(nèi)存的服務(wù)器,而提交訂單的處理程序耗時(shí)100ms,那么可以簡(jiǎn)單計(jì)算一下:
每秒可以處理的訂單請(qǐng)求數(shù)=1000ms/100ms*8=80qps |
上面這個(gè)結(jié)果,對(duì)于秒殺系統(tǒng)來(lái)說(shuō),肯定是非常不理想的。
如果能將處理程序耗時(shí)優(yōu)化后,降低到10ms,那么就可以達(dá)到800qps。
如果我們可以把程序繼續(xù)優(yōu)化,能快速區(qū)分開有庫(kù)存和無(wú)庫(kù)存處理,那么無(wú)庫(kù)存時(shí)處理就有可能做到1ms甚至更低的耗時(shí)。這樣無(wú)庫(kù)存時(shí)就能有更好的性能,上萬(wàn)的qps也是可以達(dá)到的。
上面的預(yù)估,都是針對(duì)單機(jī),那么簡(jiǎn)單的增加前端服務(wù)器,是不是就能有更好的并發(fā)處理量呢?
肯定沒這么簡(jiǎn)單,因?yàn)閿?shù)據(jù)庫(kù)、緩存系統(tǒng)甚至機(jī)房網(wǎng)絡(luò)帶寬都會(huì)成為瓶頸。
于是就要有一個(gè)更好的分布式方案。
第四:好的分布式方案
一個(gè)好的分布式方案,首先當(dāng)然是穩(wěn)定可靠,不要出亂子,然后就是方便擴(kuò)充,最好的效果當(dāng)然是增加一臺(tái)服務(wù)器,并發(fā)處理量可以1:1線性增長(zhǎng)。
比如:?jiǎn)螜C(jī)qps是1k,那么10臺(tái)服務(wù)器可以做到1w,100臺(tái)可以做到10w每秒。
要做到這樣的線性增長(zhǎng)效果,就要杜絕出現(xiàn)瓶頸,否則還是會(huì)代價(jià)太大。
拒絕假的分布式尤其重要,比如:前端服務(wù)器是可以獨(dú)立存在的,但是都依賴集中的一個(gè)數(shù)據(jù)庫(kù)或者緩存系統(tǒng),那最后,一定是集中的那個(gè)數(shù)據(jù)庫(kù)或者緩存系統(tǒng)受不了,同樣無(wú)法做到一個(gè)好的分布式。
第五:關(guān)注系統(tǒng)的瓶頸
大家先有幾個(gè)基本的共識(shí),系統(tǒng)的處理速度
- 程序內(nèi)數(shù)據(jù)讀寫 > redis > mysql > 磁盤
- 單機(jī)網(wǎng)絡(luò)請(qǐng)求 > 局域網(wǎng)內(nèi)請(qǐng)求 > 跨機(jī)房請(qǐng)求
我們優(yōu)化程序的時(shí)候,盡量用最快的方式,盡量用最簡(jiǎn)短的邏輯。
用redis替代mysql來(lái)保存訂單處理中依賴的數(shù)據(jù),用程序中的提交的數(shù)據(jù)代替從redis中二次獲取數(shù)據(jù),比如:商品庫(kù)存信息,用戶訂單信息。
邏輯處理中,把速度快且提前中斷的邏輯放在最前面,比如:驗(yàn)證登錄,驗(yàn)證問答。
我們做分布式方案的時(shí)候,盡量把資源調(diào)用放在最近的地方。
前端服務(wù)器依賴的數(shù)據(jù)盡量就在局域網(wǎng)內(nèi),如果能在單機(jī)都有讀的redis服務(wù)當(dāng)然更好,程序維護(hù)數(shù)據(jù)響應(yīng)會(huì)復(fù)雜些。
不要出現(xiàn)跨機(jī)房網(wǎng)絡(luò)請(qǐng)求,不要出現(xiàn)跨機(jī)房網(wǎng)絡(luò)請(qǐng)求,不要出現(xiàn)跨機(jī)房網(wǎng)絡(luò)請(qǐng)求,重要的事情說(shuō)三遍。
第六:什么語(yǔ)言更適合這類系統(tǒng)
課程中用的是PHP語(yǔ)言,開發(fā)這類系統(tǒng)也是沒問題的。
當(dāng)然,像是用golang, ngx_lua可能在高并發(fā)和性能方面會(huì)更有優(yōu)勢(shì)。
如果使用java、.net當(dāng)然也是可以的,作為一個(gè)系統(tǒng),語(yǔ)言只是工具,更好的設(shè)計(jì)和優(yōu)化,才能達(dá)到最終想要的效果。
有了上面的基本概念,我們接下來(lái)再來(lái)看看,具體運(yùn)行時(shí),會(huì)出現(xiàn)什么狀況。
下面是一些具體的問題:
問題1:庫(kù)存超賣
只有10個(gè)庫(kù)存,但是一秒鐘有1k個(gè)訂單,怎么能不超賣呢?
核心思想就是保證庫(kù)存遞減是原子性操作,10--返回9,9--返回8,8--返回7。
而不能是讀取出來(lái)庫(kù)存10,10-1=9再更新回去。因?yàn)檫@個(gè)讀取和更新是并發(fā)執(zhí)行的,很可能就會(huì)有1k個(gè)訂單都成功了,而庫(kù)存實(shí)際只有10。
那么,怎么保證原子性操作呢?
1. 數(shù)據(jù)庫(kù):
- update product set left_numleft_num=left_num-1 where left_num>0;
這里用到的是left_num=left_num-1,如果left_num>0才能執(zhí)行成功,數(shù)據(jù)庫(kù)查詢、更新的時(shí)候有用到鎖,是可以保證更新操作的原子性的。
數(shù)據(jù)庫(kù)性能較差,不建議使用。
2. 分布式鎖
用redis來(lái)做一個(gè)分布式鎖,reids->setnx('lock', 1) 設(shè)置一個(gè)鎖,程序執(zhí)行完成再del這個(gè)鎖。
鎖定的過(guò)程,不利于并發(fā)執(zhí)行,大家都在等待鎖解開,不建議使用。
3. 消息隊(duì)列
將訂單請(qǐng)求全部放入消息隊(duì)列,然后另外一個(gè)后臺(tái)程序一個(gè)個(gè)處理隊(duì)列中的訂單請(qǐng)求。
并發(fā)不受影響,但是用戶等待的時(shí)間較長(zhǎng),進(jìn)入隊(duì)列的訂單也會(huì)很多,體驗(yàn)上并不好,也不建議使用。
4. redis遞減
通過(guò) redis->incrby('product', -1) 得到遞減之后的庫(kù)存數(shù)。
性能方面很好,同時(shí)體驗(yàn)上也很好,在PHP秒殺課程中,優(yōu)化后就是用的這種方法,而沒有使用上述其他方法,大家應(yīng)該也能對(duì)比了解啦。
問題2:集群怎么來(lái)規(guī)劃
前端服務(wù)器因?yàn)闆]有相互間關(guān)聯(lián),集群的數(shù)量不受影響。
redis的性能可以達(dá)到每秒幾萬(wàn)次響應(yīng),所以一個(gè)集群的規(guī)模,也就是redis服務(wù)可以承載的數(shù)量。
比如:一臺(tái)前端服務(wù)器是1~2k的qps(有庫(kù)存時(shí)),那么10臺(tái)+1臺(tái)redis就可以是一個(gè)獨(dú)立的集群,可以支撐1~2w每秒訂單量。
10個(gè)上述的集群就可以做到一秒鐘處理10w~20w的有效訂單。
如果秒殺活動(dòng)的庫(kù)存量在1w以內(nèi),預(yù)計(jì)參與的人數(shù)在百萬(wàn)左右,那么有一個(gè)集群也就可以搞定。
如果秒殺參與的人數(shù)超過(guò)千萬(wàn),那么就要用到不止一個(gè)集群了。
問題3:多個(gè)集群的數(shù)據(jù)怎么保持一致性
不要做多集群的數(shù)據(jù)同步,而是用散列,每個(gè)集群的數(shù)據(jù)是獨(dú)立存在的。
假設(shè),有10個(gè)商品,每個(gè)商品有1w庫(kù)存,規(guī)劃用10個(gè)集群,那么每個(gè)集群有10個(gè)商品,每個(gè)商品是1k庫(kù)存。
每個(gè)集群只需要負(fù)責(zé)把自己的庫(kù)存賣掉即可,至于說(shuō),會(huì)不會(huì)有用戶知道有10個(gè)集群,然后每個(gè)集群都去搶。
這種情況就不要用程序來(lái)處理了,利用運(yùn)營(yíng)規(guī)則,活動(dòng)結(jié)束后匯總訂單的時(shí)候再去處理就好了。
如果擔(dān)心散列的不合理,比如:某個(gè)集群用戶訪問量特別少,那么可以引入一個(gè)中控服務(wù),來(lái)監(jiān)控各個(gè)集群的庫(kù)存,然后再做平衡。
問題4:機(jī)器人搶購(gòu)怎么辦:
沒什么太好的辦法,類似DDOS攻擊,只能是讓自身更強(qiáng)大才是王道。
運(yùn)營(yíng)策略上,可以嚴(yán)格控制用戶注冊(cè),必須登錄,提交訂單的時(shí)候引入圖像驗(yàn)證碼,問答,交互式驗(yàn)證等。