前言
秒殺系統(tǒng)相信很多人見過赎败,比如京東或者淘寶的秒殺歪玲,小米手機的秒殺迁央。
那么秒殺系統(tǒng)的后臺是如何實現(xiàn)的呢?我們?nèi)绾卧O(shè)計一個秒殺系統(tǒng)呢滥崩?對于秒殺系統(tǒng)應該考慮哪些問題岖圈?如何設(shè)計出健壯的秒殺系統(tǒng)?本期我們就來探討一下這個問題:
一:秒殺應該考慮哪些問題
1.1:超賣問題
分析秒殺的業(yè)務場景钙皮,最重要的有一點就是超賣問題蜂科,假如備貨只有100個顽决,但是最終超賣了200,一般來講秒殺系統(tǒng)的價格都比較低导匣,如果超賣將嚴重影響公司的財產(chǎn)利益擎值,因此首當其沖的就是解決商品的超賣問題。
1.2:高并發(fā)
秒殺具有時間短逐抑、并發(fā)量大的特點鸠儿,秒殺持續(xù)時間只有幾分鐘,而一般公司都為了制造轟動效應厕氨,會以極低的價格來吸引用戶进每,因此參與搶購的用戶會非常的多。
短時間內(nèi)會有大量請求涌進來命斧,后端如何防止并發(fā)過高造成緩存擊穿或者失效田晚,擊垮數(shù)據(jù)庫都是需要考慮的問題。
1.3:接口防刷
現(xiàn)在的秒殺大多都會出來針對秒殺對應的軟件国葬,這類軟件會模擬不斷向后臺服務器發(fā)起請求贤徒,一秒幾百次都是很常見的,如何防止這類軟件的重復無效請求汇四,防止不斷發(fā)起的請求也是需要我們針對性考慮的
1.4:秒殺url
對于普通用戶來講接奈,看到的只是一個比較簡單的秒殺頁面,在未達到規(guī)定時間通孽,秒殺按鈕是灰色的序宦,一旦到達規(guī)定時間,灰色按鈕變成可點擊狀態(tài)背苦。這部分是針對小白用戶的
如果是稍微有點電腦功底的用戶互捌,會通過F12看瀏覽器的network看到秒殺的url,通過特定軟件去請求也可以實現(xiàn)秒殺行剂。
或者提前知道秒殺url的人秕噪,一請求就直接實現(xiàn)秒殺了。這個問題我們需要考慮解決厚宰。
1.5:數(shù)據(jù)庫設(shè)計
秒殺有把我們服務器擊垮的風險腌巾,如果讓它與我們的其他業(yè)務使用在同一個數(shù)據(jù)庫中,耦合在一起固阁,就很有可能牽連和影響其他的業(yè)務壤躲。
如何防止這類問題發(fā)生,就算秒殺發(fā)生了宕機备燃、服務器卡死問題碉克,也應該讓他盡量不影響線上正常進行的業(yè)務。
1.6:大量請求問題
按照1.2的考慮并齐,就算使用緩存還是不足以應對短時間的高并發(fā)的流量的沖擊漏麦。如何承載這樣巨大的訪問量客税,同時提供穩(wěn)定低時延的服務保證,是需要面對的一大挑戰(zhàn)撕贞。
我們來算一筆賬更耻,假如使用的是redis緩存,單臺redis服務器可承受的QPS大概是4W左右捏膨,如果一個秒殺吸引的用戶量足夠多的話秧均,單QPS可能達到幾十萬,單體redis還是不足以支撐如此巨大的請求量号涯。緩存會被擊穿目胡,直接滲透到DB,從而擊垮mysql链快。后臺會將會大量報錯誉己。
二:秒殺系統(tǒng)的設(shè)計和技術(shù)方案
2.1:秒殺系統(tǒng)數(shù)據(jù)庫設(shè)計
針對1.5提出的秒殺數(shù)據(jù)庫的問題,因此應該單獨設(shè)計一個秒殺數(shù)據(jù)庫域蜗,防止因為秒殺活動的高并發(fā)訪問拖垮整個網(wǎng)站巨双。
這里只需要兩張表,一張是秒殺訂單表霉祸,一張是秒殺貨品表
其實應該還有幾張表筑累,商品表:可以關(guān)聯(lián)goods_id查到具體的商品信息,商品圖像脉执、名稱疼阔、平時價格、秒殺價格等半夷,還有用戶表:根據(jù)用戶user_id可以查詢到用戶昵稱、用戶手機號迅细,收貨地址等其他額外信息巫橄,這個具體就不給出實例了。
2.2:秒殺url的設(shè)計
為了避免有程序訪問經(jīng)驗的人通過下單頁面url直接訪問后臺接口來秒殺貨品茵典,我們需要將秒殺的url實現(xiàn)動態(tài)化湘换,即使是開發(fā)整個系統(tǒng)的人都無法在秒殺開始前知道秒殺的url。
具體的做法就是通過md5加密一串隨機字符作為秒殺的url统阿,然后前端訪問后臺獲取具體的url彩倚,后臺校驗通過之后才可以繼續(xù)秒殺。
2.3:秒殺頁面靜態(tài)化
將商品的描述扶平、參數(shù)帆离、成交記錄、圖像结澄、評價等全部寫入到一個靜態(tài)頁面哥谷,用戶請求不需要通過訪問后端服務器岸夯,不需要經(jīng)過數(shù)據(jù)庫,直接在前臺客戶端生成们妥,這樣可以最大可能的減少服務器的壓力猜扮。
具體的方法可以使用freemarker模板技術(shù),建立網(wǎng)頁模板监婶,填充數(shù)據(jù)旅赢,然后渲染網(wǎng)頁。
2.4:單體redis升級為集群redis
秒殺是一個讀多寫少的場景惑惶,使用redis做緩存再合適不過鲜漩。不過考慮到緩存擊穿問題,我們應該構(gòu)建redis集群集惋,采用哨兵模式孕似,可以提升redis的性能和可用性。
2.5:使用nginx
nginx是一個高性能web服務器刮刑,它的并發(fā)能力可以達到幾萬喉祭,而tomcat只有幾百。通過nginx映射客戶端請求雷绢,再分發(fā)到后臺tomcat服務器集群中可以大大提升并發(fā)能力泛烙。
2.6:精簡sql
典型的一個場景是在進行扣減庫存的時候,傳統(tǒng)的做法是先查詢庫存翘紊,再去update蔽氨。這樣的話需要兩個sql,而實際上一個sql我們就可以完成的帆疟。
可以用這樣的做法:
update miaosha_goods set stock =stock-1 where goos_id ={#goods_id} and version = #{version} and sock>0;
這樣的話鹉究,就可以保證庫存不會超賣并且一次更新庫存,還有注意一點這里使用了版本號的樂觀鎖,相比較悲觀鎖踪宠,它的性能較好自赔。
2.7:redis預減庫存
很多請求進來,都需要后臺查詢庫存,這是一個頻繁讀的場景柳琢∩芊粒可以使用redis來預減庫存,在秒殺開始前可以在redis設(shè)置
比如 redis.set(goodsId,100)柬脸,這里預放的庫存為100可以設(shè)值為常量),每次下單成功之后他去,Integer stock = (Integer)redis.get(goosId); 然后判斷sock的值,如果小于常量值就減去1倒堕。
不過注意當取消的時候灾测,需要增加庫存,增加庫存的時候也得注意不能大于之間設(shè)定的總庫存數(shù)(查詢庫存和扣減庫存需要原子操作涩馆,此時可以借助lua腳本)下次下單再獲取庫存的時候行施,直接從redis里面查就可以了允坚。
2.8:接口限流
秒殺最終的本質(zhì)是數(shù)據(jù)庫的更新,但是有很多大量無效的請求蛾号,我們最終要做的就是如何把這些無效的請求過濾掉稠项,防止?jié)B透到數(shù)據(jù)庫。
限流的話鲜结,需要入手的方面很多:
2.8.1:前端限流
首先第一步就是通過前端限流展运,用戶在秒殺按鈕點擊以后發(fā)起請求,那么在接下來的5秒是無法點擊(通過設(shè)置按鈕為disable)精刷。這一小舉措開發(fā)起來成本很小拗胜,但是很有效。
2.8.2:同一個用戶xx秒內(nèi)重復請求直接拒絕
具體多少秒需要根據(jù)實際業(yè)務和秒殺的人數(shù)而定怒允,一般限定為10秒埂软。
具體的做法就是通過redis的鍵過期策略,首先對每個請求都從String value = redis.get(userId);
如果獲取到這個value為空或者為null纫事,表示它是有效的請求勘畔,然后放行這個請求。如果不為空表示它是重復性請求丽惶,直接丟掉這個請求炫七。
如果有效,采用redis.setexpire(userId,value,10).value可以是任意值钾唬,一般放業(yè)務屬性比較好万哪,這個是設(shè)置以userId為key,10秒的過期時間(10秒后,key對應的值自動為null)
2.8.3:令牌桶算法限流
接口限流的策略有很多抡秆,我們這里采用令牌桶算法奕巍。
令牌桶算法的基本思路是每個請求嘗試獲取一個令牌,后端只處理持有令牌的請求琅轧,生產(chǎn)令牌的速度和效率我們都可以自己限定伍绳,guava提供了RateLimter的api供我們使用。
以下做一個簡單的例子乍桂,注意需要引入guava
上面代碼的思路就是通過RateLimiter來限定我們的令牌桶每秒產(chǎn)生1個令牌(生產(chǎn)的效率比較低),循環(huán)10次去執(zhí)行任務效床。
acquire會阻塞當前線程直到獲取到令牌睹酌,也就是如果任務沒有獲取到令牌,會一直等待剩檀。那么請求就會卡在我們限定的時間內(nèi)才可以繼續(xù)往下走憋沿,這個方法返回的是線程具體等待的時間。
執(zhí)行如下:
可以看到任務執(zhí)行的過程中沪猴,第1個是無需等待的辐啄,因為已經(jīng)在開始的第1秒生產(chǎn)出了令牌采章。
接下來的任務請求就必須等到令牌桶產(chǎn)生了令牌才可以繼續(xù)往下執(zhí)行。如果沒有獲取到就會阻塞(有一個停頓的過程)壶辜。
不過這個方式不太好悯舟,因為用戶如果在客戶端請求,如果較多的話砸民,直接后臺在生產(chǎn)token就會卡頓(用戶體驗較差)抵怎,它是不會拋棄任務的,我們需要一個更優(yōu)秀的策略:如果超過某個時間沒有獲取到岭参,直接拒絕該任務反惕。
接下來再來個案例:
其中用到了tryAcquire方法,這個方法的主要作用是設(shè)定一個超時的時間演侯,如果在指定的時間內(nèi)預估(注意是預估并不會真實的等待)姿染,如果能拿到令牌就返回true,如果拿不到就返回false秒际。
然后我們讓無效的直接跳過悬赏,這里設(shè)定每秒生產(chǎn)1個令牌,讓每個任務嘗試在0.5秒獲取令牌程癌,如果獲取不到,就直接跳過這個任務(放在秒殺環(huán)境里就是直接拋棄這個請求)舷嗡;
程序?qū)嶋H運行如下:
只有第1個獲取到了令牌,順利執(zhí)行了嵌莉,下面的基本都直接拋棄了进萄,因為0.5秒內(nèi),令牌桶(1秒1個)來不及生產(chǎn)就肯定獲取不到返回false了锐峭。
這個限流策略的效率有多高呢中鼠?假如我們的并發(fā)請求是400萬瞬間的請求,將令牌產(chǎn)生的效率設(shè)為每秒20個沿癞,每次嘗試獲取令牌的時間是0.05秒援雇,那么最終測試下來的結(jié)果是,每次只會放行4個左右的請求椎扬,大量的請求會被拒絕惫搏,這就是令牌桶算法的優(yōu)秀之處。
2.9:異步下單
為了提升下單的效率蚕涤,并且防止下單服務的失敗筐赔。需要將下單這一操作進行異步處理。
最常采用的辦法是使用隊列揖铜,隊列最顯著的三個優(yōu)點:異步茴丰、削峰、解耦。
這里可以采用rabbitmq贿肩,在后臺經(jīng)過了限流峦椰、庫存校驗之后,流入到這一步驟的就是有效請求汰规。然后發(fā)送到隊列里汤功,隊列接受消息,異步下單控轿。
下完單冤竹,入庫沒有問題可以用短信通知用戶秒殺成功。假如失敗的話,可以采用補償機制茬射,重試鹦蠕。
2.10:服務降級
假如在秒殺過程中出現(xiàn)了某個服務器宕機,或者服務不可用在抛,應該做好后備工作钟病。之前的博客里有介紹通過Hystrix進行服務熔斷和降級,可以開發(fā)一個備用服務刚梭。
假如服務器真的宕機了肠阱,直接給用戶一個友好的提示返回,而不是直接卡死朴读,服務器錯誤等生硬的反饋屹徘。
三:總結(jié)
秒殺流程圖:
這就是我設(shè)計出來的秒殺流程圖,當然不同的秒殺體量針對的技術(shù)選型都不一樣衅金,這個流程可以支撐起幾十萬的流量噪伊,如果是成千萬破億那就得重新設(shè)計了。比如數(shù)據(jù)庫的分庫分表氮唯、隊列改成用kafka鉴吹、redis增加集群數(shù)量等手段。
通過本次設(shè)計主要是要表明的是我們?nèi)绾螒獙Ω卟l(fā)的處理惩琉,并開始嘗試解決它豆励,在工作中多思考、多動手能提升我們的能力水平瞒渠,加油良蒸!