<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何設(shè)計一個秒殺系統(tǒng)呢?

          共 3107字,需瀏覽 7分鐘

           ·

          2021-01-25 11:30


          不點藍字,我們哪來故事?

          每天 11 點更新文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領(lǐng)!》


          秒殺系統(tǒng)相信很多人見過,比如京東或者淘寶的秒殺,小米手機的秒殺,那么秒殺系統(tǒng)的后臺是如何實現(xiàn)的呢?我們?nèi)绾卧O(shè)計一個秒殺系統(tǒng)呢?對于秒殺系統(tǒng)應(yīng)該考慮哪些問題?如何設(shè)計出健壯的秒殺系統(tǒng)?本文我們就來探討一下這個問題。


          秒殺應(yīng)該考慮哪些問題


          超賣問題

          分析秒殺的業(yè)務(wù)場景,最重要的有一點就是超賣問題,假如備貨只有100個,但是最終超賣了200,一般來講秒殺系統(tǒng)的價格都比較低,如果超賣將嚴重影響公司的財產(chǎn)利益,因此首當(dāng)其沖的就是解決商品的超賣問題。

          高并發(fā)

          秒殺具有時間短、并發(fā)量大的特點,秒殺持續(xù)時間只有幾分鐘,而一般公司都為了制造轟動效應(yīng),會以極低的價格來吸引用戶,因此參與搶購的用戶會非常的多。短時間內(nèi)會有大量請求涌進來,后端如何防止并發(fā)過高造成緩存擊穿或者失效,擊垮數(shù)據(jù)庫都是需要考慮的問題。

          接口防刷

          現(xiàn)在的秒殺大多都會出來針對秒殺對應(yīng)的軟件,這類軟件會模擬不斷向后臺服務(wù)器發(fā)起請求,一秒幾百次都是很常見的,如何防止這類軟件的重復(fù)無效請求,防止不斷發(fā)起的請求也是需要我們針對性考慮的。

          秒殺url

          對于普通用戶來講,看到的只是一個比較簡單的秒殺頁面,在未達到規(guī)定時間,秒殺按鈕是灰色的,一旦到達規(guī)定時間,灰色按鈕變成可點擊狀態(tài)。這部分是針對小白用戶的,如果是稍微有點電腦功底的用戶,會通過F12看瀏覽器的network看到秒殺的url,通過特定軟件去請求也可以實現(xiàn)秒殺。或者提前知道秒殺url的人,一請求就直接實現(xiàn)秒殺了。這個問題我們需要考慮解決。

          數(shù)據(jù)庫設(shè)計

          秒殺有把我們服務(wù)器擊垮的風(fēng)險,如果讓它與我們的其他業(yè)務(wù)使用在同一個數(shù)據(jù)庫中,耦合在一起,就很有可能牽連和影響其他的業(yè)務(wù)。如何防止這類問題發(fā)生,就算秒殺發(fā)生了宕機、服務(wù)器卡死問題,也應(yīng)該讓他盡量不影響線上正常進行的業(yè)務(wù)。

          大量請求問題

          按照「高并發(fā)」的考慮,就算使用緩存還是不足以應(yīng)對短時間的高并發(fā)的流量的沖擊。如何承載這樣巨大的訪問量,同時提供穩(wěn)定低時延的服務(wù)保證,是需要面對的一大挑戰(zhàn)。我們來算一筆賬,假如使用的是Redis緩存,單臺Redis服務(wù)器可承受的QPS大概是4W左右,如果一個秒殺吸引的用戶量足夠多的話,單QPS可能達到幾十萬,單體Redis還是不足以支撐如此巨大的請求量。緩存會被擊穿,直接滲透到DB,從而擊垮MySQL,后臺會將會大量報錯。


          秒殺系統(tǒng)的設(shè)計和技術(shù)方案


          秒殺系統(tǒng)數(shù)據(jù)庫設(shè)計

          針對「數(shù)據(jù)庫設(shè)計」提出的秒殺數(shù)據(jù)庫的問題,因此應(yīng)該單獨設(shè)計一個秒殺數(shù)據(jù)庫,防止因為秒殺活動的高并發(fā)訪問拖垮整個網(wǎng)站。這里只需要兩張表,一張是秒殺訂單表,一張是秒殺貨品表。


          其實應(yīng)該還有幾張表,商品表:可以關(guān)聯(lián)goods_id查到具體的商品信息,商品圖像、名稱、平時價格、秒殺價格等,還有用戶表:根據(jù)用戶user_id可以查詢到用戶昵稱、用戶手機號,收貨地址等其他額外信息,這個具體就不給出實例了。

          秒殺url的設(shè)計

          為了避免有程序訪問經(jīng)驗的人通過下單頁面url直接訪問后臺接口來秒殺貨品,我們需要將秒殺的url實現(xiàn)動態(tài)化,即使是開發(fā)整個系統(tǒng)的人都無法在秒殺開始前知道秒殺的url。具體的做法就是通過md5加密一串隨機字符作為秒殺的url,然后前端訪問后臺獲取具體的url,后臺校驗通過之后才可以繼續(xù)秒殺。

          秒殺頁面靜態(tài)化

          將商品的描述、參數(shù)、成交記錄、圖像、評價等全部寫入到一個靜態(tài)頁面,用戶請求不需要通過訪問后端服務(wù)器,不需要經(jīng)過數(shù)據(jù)庫,直接在前臺客戶端生成,這樣可以最大可能的減少服務(wù)器的壓力。具體的方法可以使用freemarker模板技術(shù),建立網(wǎng)頁模板,填充數(shù)據(jù),然后渲染網(wǎng)頁。

          單體Redis升級為集群Redis

          秒殺是一個讀多寫少的場景,使用Redis做緩存再合適不過。不過考慮到緩存擊穿問題,我們應(yīng)該構(gòu)建Redis集群,采用哨兵模式,可以提升Redis的性能和可用性。

          使用Nginx

          Nginx是一個高性能Web服務(wù)器,它的并發(fā)能力可以達到幾萬,而Tomcat只有幾百。通過Nginx映射客戶端請求,再分發(fā)到后臺Tomcat服務(wù)器集群中可以大大提升并發(fā)能力。

          精簡SQL

          典型的一個場景是在進行扣減庫存的時候,傳統(tǒng)的做法是先查詢庫存,再去update。這樣的話需要兩個SQL,而實際上一個SQL我們就可以完成的。可以用這樣的做法:update miaosha_goods set stock =stock-1 where goos_id ={#goods_id} and version = #{version} and sock>0;這樣的話,就可以保證庫存不會超賣并且一次更新庫存,還有注意一點這里使用了版本號的樂觀鎖,相比較悲觀鎖,它的性能較好。

          Redis預(yù)減庫存

          很多請求進來,都需要后臺查詢庫存,這是一個頻繁讀的場景。可以使用Redis來預(yù)減庫存,在秒殺開始前可以在Redis設(shè)值,比如redis.set(goodsId,100),這里預(yù)放的庫存為100可以設(shè)值為常量,每次下單成功之后,Integer stock = (Integer)redis.get(goosId); 然后判斷sock的值,如果小于常量值就減去1;不過注意當(dāng)取消的時候,需要增加庫存,增加庫存的時候也得注意不能大于之間設(shè)定的總庫存數(shù)(查詢庫存和扣減庫存需要原子操作,此時可以借助lua腳本)下次下單再獲取庫存的時候,直接從Redis里面查就可以了。

          接口限流

          秒殺最終的本質(zhì)是數(shù)據(jù)庫的更新,但是有很多大量無效的請求,我們最終要做的就是如何把這些無效的請求過濾掉,防止?jié)B透到數(shù)據(jù)庫。限流的話,需要入手的方面很多:

          前端限流

          首先第一步就是通過前端限流,用戶在秒殺按鈕點擊以后發(fā)起請求,那么在接下來的5秒是無法點擊(通過設(shè)置按鈕為disable)。這一小舉措開發(fā)起來成本很小,但是很有效。

          同一個用戶xx秒內(nèi)重復(fù)請求直接拒絕

          具體多少秒需要根據(jù)實際業(yè)務(wù)和秒殺的人數(shù)而定,一般限定為10秒。具體的做法就是通過Redis的鍵過期策略,首先對每個請求都從String value = redis.get(userId);如果獲取到這個value為空或者為null,表示它是有效的請求,然后放行這個請求。如果不為空表示它是重復(fù)性請求,直接丟掉這個請求。如果有效,采用redis.setexpire(userId,value,10).value可以是任意值,一般放業(yè)務(wù)屬性比較好,這個是設(shè)置以userId為key,10秒的過期時間(10秒后,key對應(yīng)的值自動為null)。

          令牌桶算法限流

          接口限流的策略有很多,我們這里采用令牌桶算法。令牌桶算法的基本思路是每個請求嘗試獲取一個令牌,后端只處理持有令牌的請求,生產(chǎn)令牌的速度和效率我們都可以自己限定,Guava提供了RateLimter的API供我們使用。以下做一個簡單的例子,注意需要引入Guava:

          public?class?TestRateLimiter?{

          ????public?static?void?main(String[]?args)?{
          ????????//1秒產(chǎn)生1個令牌
          ????????final?RateLimiter?rateLimiter?=?RateLimiter.create(1);
          ????????for?(int?i?=?0;?i?????????????//該方法會阻塞線程,直到令牌桶中能取到令牌為止才繼續(xù)向下執(zhí)行。
          ????????????double?waitTime=?rateLimiter.acquire();
          ????????????System.out.println("任務(wù)執(zhí)行"?+?i?+?"等待時間"?+?waitTime);
          ????????}
          ????????System.out.println("執(zhí)行結(jié)束");
          ????}
          }

          上面代碼的思路就是通過RateLimiter來限定我們的令牌桶每秒產(chǎn)生1個令牌(生產(chǎn)的效率比較低),循環(huán)10次去執(zhí)行任務(wù)。acquire會阻塞當(dāng)前線程直到獲取到令牌,也就是如果任務(wù)沒有獲取到令牌,會一直等待。那么請求就會卡在我們限定的時間內(nèi)才可以繼續(xù)往下走,這個方法返回的是線程具體等待的時間。執(zhí)行如下:


          可以看到任務(wù)執(zhí)行的過程中,第1個是無需等待的,因為已經(jīng)在開始的第1秒生產(chǎn)出了令牌。接下來的任務(wù)請求就必須等到令牌桶產(chǎn)生了令牌才可以繼續(xù)往下執(zhí)行。如果沒有獲取到就會阻塞(有一個停頓的過程)。不過這個方式不太好,因為用戶如果在客戶端請求,如果較多的話,直接后臺在生產(chǎn)token就會卡頓(用戶體驗較差),它是不會拋棄任務(wù)的,我們需要一個更優(yōu)秀的策略:如果超過某個時間沒有獲取到,直接拒絕該任務(wù)。接下來再來個案例:

          public?class?TestRateLimiter2?{

          ????public?static?void?main(String[]?args)?{
          ????????final?RateLimiter?rateLimiter?=?RateLimiter.create(1);

          ????????for?(int?i?=?0;?i?????????????long?timeOut?=?(long)?0.5;
          ????????????boolean?isValid?=?rateLimiter.tryAcquire(timeOut,?TimeUnit.SECONDS);
          ????????????System.out.println("任務(wù)"?+?i?+?"執(zhí)行是否有效:"?+?isValid);
          ????????????if?(!isValid)?{
          ????????????????continue;
          ????????????}
          ????????????System.out.println("任務(wù)"?+?i?+?"在執(zhí)行");
          ????????}
          ????????System.out.println("結(jié)束");
          ????}
          }

          其中用到了tryAcquire方法,這個方法的主要作用是設(shè)定一個超時的時間,如果在指定的時間內(nèi)預(yù)估(注意是預(yù)估并不會真實的等待),如果能拿到令牌就返回true,如果拿不到就返回false。然后我們讓無效的直接跳過,這里設(shè)定每秒生產(chǎn)1個令牌,讓每個任務(wù)嘗試在0.5秒獲取令牌,如果獲取不到,就直接跳過這個任務(wù)(放在秒殺環(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)秀之處。

          異步下單

          為了提升下單的效率,并且防止下單服務(wù)的失敗。需要將下單這一操作進行異步處理。最常采用的辦法是使用隊列,隊列最顯著的三個優(yōu)點:異步、削峰、解耦。這里可以采用RabbitMQ,在后臺經(jīng)過了限流、庫存校驗之后,流入到這一步驟的就是有效請求。然后發(fā)送到隊列里,隊列接受消息,異步下單。下完單,入庫沒有問題可以用短信通知用戶秒殺成功。假如失敗的話,可以采用補償機制,重試。

          服務(wù)降級

          假如在秒殺過程中出現(xiàn)了某個服務(wù)器宕機,或者服務(wù)不可用,應(yīng)該做好后備工作。之前的博客里有介紹通過Hystrix進行服務(wù)熔斷和降級,可以開發(fā)一個備用服務(wù),假如服務(wù)器真的宕機了,直接給用戶一個友好的提示返回,而不是直接卡死,服務(wù)器錯誤等生硬的反饋。


          總結(jié)


          秒殺流程圖:


          這就是我設(shè)計出來的秒殺流程圖,當(dāng)然不同的秒殺體量針對的技術(shù)選型都不一樣,這個流程可以支撐起幾十萬的流量,如果是成千萬破億那就得重新設(shè)計了。比如數(shù)據(jù)庫的分庫分表、隊列改成用Kafka、Redis增加集群數(shù)量等手段。通過本次設(shè)計主要是要表明的是我們?nèi)绾螒?yīng)對高并發(fā)的處理,并開始嘗試解決它,在工作中多思考、多動手能提升我們的能力水平,加油!

          ·END·

          往期推薦

          1.3 萬億條的數(shù)據(jù)查詢做到了毫秒級響應(yīng)?

          天真!這簡歷一看就是包裝過的!

          你真的會正確使用日志嗎?

          如何保證API接口數(shù)據(jù)安全?


          下方二維碼關(guān)注我

          技術(shù)草根堅持分享?編程,算法,架構(gòu)

          看完文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領(lǐng)!》

          朋友,助攻一把!點個在看
          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲欧美不卡高清在线 | 天天射天天搞天天干 | 91美女视频| 国产成人亚洲综合AV婷婷 | 国产乱伦一 |