如何設(shè)計(jì)百萬(wàn)人抽獎(jiǎng)系統(tǒng)……
把快樂(lè)放進(jìn)重點(diǎn)反復(fù)背誦。
思維導(dǎo)圖
導(dǎo)圖源文件:鏈接: https://pan.baidu.com/s/1FrUMGAdrbvzlDLdftnLV1A 提取碼: nw57

導(dǎo)圖按照由淺入深的方式進(jìn)行講解,架構(gòu)從來(lái)不是設(shè)計(jì)出來(lái)的,而是演進(jìn)而來(lái)的
從一個(gè)幾百人的抽獎(jiǎng)系統(tǒng)到幾萬(wàn)人,再到百萬(wàn)人,不斷增加新的東西。
最后總結(jié)歸納一套設(shè)計(jì)思想,也是萬(wàn)能模板,這樣面試官問(wèn)任何高并發(fā)系統(tǒng),只需從這幾個(gè)方向去考慮就可以了。
V0——單體架構(gòu)
如果現(xiàn)在讓你實(shí)現(xiàn)幾十人的抽獎(jiǎng)系統(tǒng),簡(jiǎn)單死了吧,直接重拳出擊!
兩貓一豚走江湖,中獎(jiǎng)入庫(kù),調(diào)通知服務(wù),查庫(kù)通知,完美!

相信大家學(xué)java時(shí)可能都做過(guò)這種案例,思考??一下存在什么問(wèn)題?
單體服務(wù),一著不慎滿盤皆輸 抽了再抽,一個(gè)人就是一支軍隊(duì) 惡意腳本,沒(méi)有程序員中不了的獎(jiǎng)
接下來(lái)就聊聊怎么解決這些問(wèn)題?
V1——負(fù)載均衡
當(dāng)一臺(tái)服務(wù)器的單位時(shí)間內(nèi)的訪問(wèn)量越大時(shí),服務(wù)器壓力就越大,大到超過(guò)自身承受能力時(shí),服務(wù)器就會(huì)崩潰。
為了避免服務(wù)器崩潰,讓用戶有更好的體驗(yàn),我們通過(guò)負(fù)載均衡的方式來(lái)分擔(dān)服務(wù)器壓力。
負(fù)載均衡就是建立很多很多服務(wù)器,組成一個(gè)服務(wù)器集群,當(dāng)用戶訪問(wèn)網(wǎng)站時(shí),先訪問(wèn)一個(gè)中間服務(wù)器,好比管家,由他在服務(wù)器集群中選擇一個(gè)壓力較小的服務(wù)器,然后將該訪問(wèn)請(qǐng)求引入該服務(wù)器。
如此以來(lái),用戶的每次訪問(wèn),都會(huì)保證服務(wù)器集群中的每個(gè)服務(wù)器壓力趨于平衡,分擔(dān)了服務(wù)器壓力,避免了服務(wù)器崩潰的情況。
負(fù)載均衡是用「反向代理」的原理實(shí)現(xiàn)的。具體負(fù)載均衡算法及其實(shí)現(xiàn)方式我們下文再續(xù)。

負(fù)載均衡雖然解決了單體架構(gòu)一著不慎滿盤皆輸?shù)膯?wèn)題,但服務(wù)器成本依然不能保護(hù)系統(tǒng)周全,我們必須想好一旦服務(wù)器宕機(jī),如何保證用戶的體驗(yàn)。
即如何緩解開(kāi)獎(jiǎng)一瞬間時(shí)的大量請(qǐng)求。
V2——服務(wù)限流
限流主要的作用是保護(hù)服務(wù)節(jié)點(diǎn)或者集群后面的數(shù)據(jù)節(jié)點(diǎn),防止瞬時(shí)流量過(guò)大使服務(wù)和數(shù)據(jù)崩潰(如前端緩存大量實(shí)效),造成不可用。
還可用于平滑請(qǐng)求。
在上一小節(jié)我們做好了負(fù)載均衡來(lái)保證集群的可用性,但公司需要需要考慮服務(wù)器的成本,不可能無(wú)限制的增加服務(wù)器數(shù)量,一般會(huì)經(jīng)過(guò)計(jì)算保證日常的使用沒(méi)問(wèn)題。
限流的意義就在于我們無(wú)法預(yù)測(cè)未知流量,比如剛提到的抽獎(jiǎng)可能遇到的:
重復(fù)抽獎(jiǎng) 惡意腳本
其他一些場(chǎng)景:
熱點(diǎn)事件(微博) 大量爬蟲(chóng)
這些情況都是無(wú)法預(yù)知的,不知道什么時(shí)候會(huì)有10倍甚至20倍的流量打進(jìn)來(lái),如果真碰上這種情況,擴(kuò)容是根本來(lái)不及的(彈性擴(kuò)容都是虛談,一秒鐘你給我擴(kuò)一下試試)
明確了限流的意義,我們?cè)賮?lái)看看如何實(shí)現(xiàn)限流
防止用戶重復(fù)抽獎(jiǎng)
重復(fù)抽獎(jiǎng)和惡意腳本可以歸在一起,同時(shí)幾十萬(wàn)的用戶可能發(fā)出幾百萬(wàn)的請(qǐng)求。

如果同一個(gè)用戶在1分鐘之內(nèi)多次發(fā)送請(qǐng)求來(lái)進(jìn)行抽獎(jiǎng),就認(rèn)為是惡意重復(fù)抽獎(jiǎng)或者是腳本在刷獎(jiǎng),這種流量是不應(yīng)該再繼續(xù)往下請(qǐng)求的,在負(fù)載均衡層給直接屏蔽掉。
可以通過(guò)nginx配置ip的訪問(wèn)頻率,或者在在網(wǎng)關(guān)層結(jié)合sentinel配置限流策略。
用戶的抽獎(jiǎng)狀態(tài)可以通過(guò)redis來(lái)存儲(chǔ),后面會(huì)說(shuō)。
攔截?zé)o效流量
無(wú)論是抽獎(jiǎng)還是秒殺,獎(jiǎng)品和商品都是有限的,所以后面涌入的大量請(qǐng)求其實(shí)都是無(wú)用的。
舉個(gè)例子,假設(shè)50萬(wàn)人抽獎(jiǎng),就準(zhǔn)備了100臺(tái)手機(jī),那么50萬(wàn)請(qǐng)求瞬間涌入,其實(shí)前500個(gè)請(qǐng)求就把手機(jī)搶完了,后續(xù)的幾十萬(wàn)請(qǐng)求就沒(méi)必要讓他再執(zhí)行業(yè)務(wù)邏輯,直接暴力攔截返回抽獎(jiǎng)結(jié)束就可以了。
同時(shí)前端在按鈕置灰上也可以做一些文章。
那么思考一下如何才能知道獎(jiǎng)品抽完了呢,也就是庫(kù)存和訂單之前的數(shù)據(jù)同步問(wèn)題。
服務(wù)降級(jí)和服務(wù)熔斷
有了以上措施就萬(wàn)無(wú)一失了嗎,不可能的。所以在服務(wù)端還有降級(jí)和熔斷機(jī)制。
在此簡(jiǎn)單做個(gè)補(bǔ)充,詳細(xì)內(nèi)容請(qǐng)持續(xù)關(guān)注作者。
有好多人容易混淆這兩個(gè)概念,通過(guò)一個(gè)小例子讓大家明白:
假設(shè)現(xiàn)在一條粉絲數(shù)突破100萬(wàn),沖上微博熱搜,粉絲甲和粉絲乙都打開(kāi)微博觀看,但甲看到了一條新聞發(fā)布會(huì)的內(nèi)容,乙卻看到”系統(tǒng)繁忙“,過(guò)了一會(huì),乙也能看到內(nèi)容了。
(請(qǐng)?jiān)试S一條幻想一下??)
在上述過(guò)程中,首先是熱點(diǎn)時(shí)間造成大量請(qǐng)求,發(fā)生了服務(wù)熔斷,為了保證整個(gè)系統(tǒng)可用,犧牲了部分用戶乙,乙看到的”系統(tǒng)繁忙“就是服務(wù)降級(jí)(fallback),過(guò)了一會(huì)又恢復(fù)訪問(wèn),這也是熔斷器的一個(gè)特性(hystrix)
V3 同步狀態(tài)
接著回到上一節(jié)的問(wèn)題,如何同步抽獎(jiǎng)狀態(tài)?
這不得不提到redis,被廣泛用于高并發(fā)系統(tǒng)的緩存數(shù)據(jù)庫(kù)。
我們可以基于Redis來(lái)實(shí)現(xiàn)這種共享抽獎(jiǎng)狀態(tài),它非常輕量級(jí),很適合兩個(gè)層次的系統(tǒng)的共享訪問(wèn)。
當(dāng)然其實(shí)用ZooKeeper也是可以的,在負(fù)載均衡層可以基于zk客戶端監(jiān)聽(tīng)某個(gè)znode節(jié)點(diǎn)狀態(tài)。一旦抽獎(jiǎng)結(jié)束,抽獎(jiǎng)服務(wù)更新zk狀態(tài),負(fù)載均衡層會(huì)感知到。

V4線程優(yōu)化
對(duì)于線上環(huán)境,工作線程數(shù)量是一個(gè)至關(guān)重要的參數(shù),需要根據(jù)自己的情況調(diào)節(jié)。
眾所周知,對(duì)于進(jìn)入Tomcat的每個(gè)請(qǐng)求,其實(shí)都會(huì)交給一個(gè)獨(dú)立的工作線程來(lái)進(jìn)行處理,那么Tomcat有多少線程,就決定了并發(fā)請(qǐng)求處理的能力。
但是這個(gè)線程數(shù)量是需要經(jīng)過(guò)壓測(cè)來(lái)進(jìn)行判斷的,因?yàn)槊總€(gè)線程都會(huì)處理一個(gè)請(qǐng)求,這個(gè)請(qǐng)求又需要訪問(wèn)數(shù)據(jù)庫(kù)之類的外部系統(tǒng),所以不是每個(gè)系統(tǒng)的參數(shù)都可以一樣的,需要自己對(duì)系統(tǒng)進(jìn)行壓測(cè)。
但是給一個(gè)經(jīng)驗(yàn)值的話,Tomcat的線程數(shù)量不宜過(guò)多。因?yàn)榫€程過(guò)多,普通服務(wù)器的CPU是扛不住的,反而會(huì)導(dǎo)致機(jī)器CPU負(fù)載過(guò)高,最終崩潰。
同時(shí),Tomcat的線程數(shù)量也不宜太少,因?yàn)槿绻?00個(gè)線程,那么會(huì)導(dǎo)致無(wú)法充分利用Tomcat的線程資源和機(jī)器的CPU資源。
所以一般來(lái)說(shuō),Tomcat線程數(shù)量在200~500之間都是可以的,但是具體多少需要自己壓測(cè)一下,不斷的調(diào)節(jié)參數(shù),看具體的CPU負(fù)載以及線程執(zhí)行請(qǐng)求的一個(gè)效率。
在CPU負(fù)載尚可,以及請(qǐng)求執(zhí)行性能正常的情況下,盡可能提高一些線程數(shù)量。
但是如果到一個(gè)臨界值,發(fā)現(xiàn)機(jī)器負(fù)載過(guò)高,而且線程處理請(qǐng)求的速度開(kāi)始下降,說(shuō)明這臺(tái)機(jī)扛不住這么多線程并發(fā)執(zhí)行處理請(qǐng)求了,此時(shí)就不能繼續(xù)上調(diào)線程數(shù)量了。

V5業(yè)務(wù)邏輯
抽獎(jiǎng)邏輯怎么做?
好了,現(xiàn)在該研究一下怎么做抽獎(jiǎng)了
在負(fù)載均衡那個(gè)層面,已經(jīng)把比如50萬(wàn)流量中的48萬(wàn)都攔截掉了,但是可能還是會(huì)有2萬(wàn)流量進(jìn)入抽獎(jiǎng)服務(wù)。
因?yàn)槌楠?jiǎng)活動(dòng)都是臨時(shí)服務(wù),可以阿里云租一堆機(jī)器,也不是很貴,tomcat優(yōu)化完了,服務(wù)器的問(wèn)題也解決了,還剩啥呢?
Mysql,是的,你的Mysql能抗住2萬(wàn)的并發(fā)請(qǐng)求嗎?
答案是很難,怎么辦呢?
把Mysql給替換成redis,單機(jī)抗2萬(wàn)并發(fā)那是很輕松的一件事情。
而且redis的一種數(shù)據(jù)結(jié)構(gòu)set很適合做抽獎(jiǎng),可以隨機(jī)選擇一個(gè)元素并剔除。

V6流量削峰
由上至下,還剩中獎(jiǎng)通知部分沒(méi)有優(yōu)化。
思考這個(gè)問(wèn)題:假設(shè)抽獎(jiǎng)服務(wù)在2萬(wàn)請(qǐng)求中有1萬(wàn)請(qǐng)求抽中了獎(jiǎng)品,那么勢(shì)必會(huì)造成抽獎(jiǎng)服務(wù)對(duì)禮品服務(wù)調(diào)用1萬(wàn)次。
那也要和抽獎(jiǎng)服務(wù)同樣處理嗎?
其實(shí)并不用,因?yàn)榘l(fā)送通知不要求及時(shí)性,完全可以讓一萬(wàn)個(gè)請(qǐng)求慢慢發(fā)送,這時(shí)就要用到消息中間件,進(jìn)行限流削峰。
也就是說(shuō),抽獎(jiǎng)服務(wù)把中獎(jiǎng)信息發(fā)送到MQ,然后通知服務(wù)慢慢的從MQ中消費(fèi)中獎(jiǎng)消息,最終完成完禮品的發(fā)放,這也是我們會(huì)延遲一些收到中獎(jiǎng)信息或者物流信息的原因。
假設(shè)兩個(gè)通知服務(wù)實(shí)例每秒可以完成100個(gè)通知的發(fā)送,那么1萬(wàn)條消息也就是延遲100秒發(fā)放完畢罷了。
同樣對(duì)MySQL的壓力也會(huì)降低,那么數(shù)據(jù)庫(kù)層面也是可以抗住的。
看一下最終結(jié)構(gòu)圖:

答題模板
所謂答題模板,就是高并發(fā)問(wèn)題的幾個(gè)思考方向和解決方案。
單一職責(zé)
一個(gè)基本的設(shè)計(jì)思想,回想高中物理的串聯(lián)和并聯(lián),串聯(lián)一滅全滅,并聯(lián)各自有一個(gè)通路。
一樣的道理,高內(nèi)聚,低耦合。
微服務(wù)之所以興起就是因?yàn)榘褟?fù)雜的功能進(jìn)行拆分,即使網(wǎng)站崩了,無(wú)法下單,但是瀏覽功能依然健康,而不是所有服務(wù)引起連鎖反應(yīng),像雪崩一樣,全面癱瘓。
URL動(dòng)態(tài)加密
這說(shuō)的是防止惡意訪問(wèn),有些爬蟲(chóng)或者刷量腳本會(huì)造成大量的請(qǐng)求訪問(wèn)你的接口,你更加不知道他會(huì)傳什么參數(shù)給你,所以我們定義接口時(shí)一定要多加驗(yàn)證,因?yàn)椴恢故悄愕呐笥颜{(diào)你的接口,敵人也有可能。
靜態(tài)資源——CDN
CDN全稱內(nèi)容分發(fā)網(wǎng)絡(luò),是建立并覆蓋在承載網(wǎng)之上,由分布在不同區(qū)域的邊緣節(jié)點(diǎn)服務(wù)器群組成的分布式網(wǎng)絡(luò)。
通俗的講,就是把經(jīng)常訪問(wèn)又費(fèi)時(shí)的資源放在你附近的服務(wù)器上。
淘寶的圖片訪問(wèn),有98%的流量都走了CDN緩存。只有2%會(huì)回源到源站,節(jié)省了大量的服務(wù)器資源。
但是,如果在用戶訪問(wèn)高峰期,圖片內(nèi)容大批量發(fā)生變化,大量用戶的訪問(wèn)就會(huì)穿透cdn,對(duì)源站造成巨大的壓力。
所以,對(duì)于圖片這種靜態(tài)資源,盡可能都放入CDN。
服務(wù)限流
在上面已有講解,可分為前端限流和后端限流。
前端:按鈕禁用,ip黑名單 后端:服務(wù)熔斷,服務(wù)降級(jí),權(quán)限驗(yàn)證
數(shù)據(jù)預(yù)熱
可以采用定時(shí)任務(wù)(elastic-job)實(shí)時(shí)查詢Druid,把熱點(diǎn)數(shù)據(jù)放入redis緩存中。
思考一個(gè)問(wèn)題:
比如現(xiàn)在庫(kù)存只剩下1個(gè)了,我們高并發(fā)嘛,4個(gè)服務(wù)器一起查詢了發(fā)現(xiàn)都是還有1個(gè),那大家都覺(jué)得是自己搶到了,就都去扣庫(kù)存,那結(jié)果就變成了-3,是的只有一個(gè)是真的搶到了,別的都是超賣的。咋辦?
回答:
可以用CAS+LUA腳本實(shí)現(xiàn)。
Lua腳本是類似Redis事務(wù),有一定的原子性,不會(huì)被其他命令插隊(duì),可以完成一些Redis事務(wù)性的操作。這點(diǎn)是關(guān)鍵。
寫(xiě)一個(gè)腳本把判斷庫(kù)存扣減庫(kù)存的操作都寫(xiě)在一個(gè)腳本丟給Redis去做,那到0了后面的都Return False了是吧,一個(gè)失敗了你修改一個(gè)開(kāi)關(guān),直接擋住所有的請(qǐng)求。
削峰填谷
精通一個(gè)中間件會(huì)給你加分很多
消息隊(duì)列已經(jīng)逐漸成為企業(yè)IT系統(tǒng)內(nèi)部通信的核心手段。
它具有低耦合、可靠投遞、廣播、流量控制、最終一致性等一系列功能,成為異步RPC的主要手段之一。
當(dāng)今市面上有很多主流的消息中間件,如老牌的ActiveMQ、RabbitMQ,炙手可熱的Kafka,阿里巴巴自主開(kāi)發(fā)RocketMQ等。
最原始的MQ,生產(chǎn)者先將消息投遞一個(gè)叫做「隊(duì)列」的容器中,然后再?gòu)倪@個(gè)容器中取出消息,最后再轉(zhuǎn)發(fā)給消費(fèi)者,僅此而已。
更詳細(xì)的MQ下文再續(xù)。
今天就學(xué)這么多,相信大家都對(duì)高并發(fā)系統(tǒng)有了初步的認(rèn)識(shí),面試官問(wèn)起來(lái)也不至于無(wú)話可說(shuō),但是想要學(xué)好任重而道遠(yuǎn),希望大家關(guān)注一條,帶各位一起學(xué)習(xí)!

要求整治程序員高薪的那個(gè)人,被打了!

鏡 | 5 個(gè)維度深度剖析「主從架構(gòu)」原理
