秒殺架構模型設計

Yrion發表於2019-08-04

前言:秒殺系統相信很多人見過,比如京東或者淘寶的秒殺,小米手機的秒殺,那麼秒殺系統的後臺是如何實現的呢?我們如何設計一個秒殺系統呢?對於秒殺系統應該考慮哪些問題?如何設計出健壯的秒殺系統?本期我們就來探討一下這個問題:

 

部落格的目錄

一:秒殺系統應該考慮的問題

二:秒殺系統的設計和技術方案

三:系統架構圖

四:總結

一:秒殺應該考慮哪些問題

1.1:超賣問題

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

1.2:高併發

秒殺具有時間短、併發量大的特點,秒殺持續時間只有幾分鐘,而一般公司都為了製造轟動效應,會以極低的價格來吸引使用者,因此參與搶購的使用者會非常的多。短時間內會

有大量請求湧進來,後端如何防止併發過高造成快取擊穿或者失效,擊垮資料庫都是需要考慮的問題。

1.3:介面防刷

現在的秒殺大多都會出來針對秒殺對應的軟體,這類軟體會模擬不斷向後臺伺服器發起請求,一秒幾百次都是很常見的,如何防止這類軟體的重複無效請求,防止不斷髮起的請求也是需要我們針對性考慮的

1.4:秒殺url

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

1.5:資料庫設計

秒殺有把我們伺服器擊垮的風險,如果讓它與我們的其他業務使用在同一個資料庫中,耦合在一起,就很有可能牽連和影響其他的業務。如何防止這類問題發生,就算秒殺發生了當機、伺服器卡死問題,也應該讓他儘量不影響線上正常進行的業務

1.6:大量請求問題

按照1.2的考慮,就算使用快取還是不足以應對短時間的高併發的流量的衝擊。如何承載這樣巨大的訪問量,同時提供穩定低時延的服務保證,是需要面對的一大挑戰。我們來算一筆賬,假如使用的是redis快取,單臺redis伺服器可承受的QPS大概是4W左右,如果一個秒殺吸引的使用者量足夠多的話,單QPS可能達到幾十萬,單體redis還是不足以支撐如此巨大的請求量。快取會被擊穿,直接滲透到DB,從而擊垮mysql.後臺會將會大量報錯

二:秒殺系統的設計和技術方案

2.1:秒殺系統資料庫設計

針對1.5提出的秒殺資料庫的問題,因此應該單獨設計一個秒殺資料庫,防止因為秒殺活動的高併發訪問拖垮整個網站。這裡只需要兩張表,一張是秒殺訂單表,一張是秒殺貨品表

  

其實應該還有幾張表,商品表:可以關聯goods_id查到具體的商品資訊,商品影象、名稱、平時價格、秒殺價格等,還有使用者表:根據使用者user_id可以查詢到使用者暱稱、使用者手機號,收貨地址等其他額外資訊,這個具體就不給出例項了。

2.2:秒殺url的設計

為了避免有程式訪問經驗的人通過下單頁面url直接訪問後臺介面來秒殺貨品,我們需要將秒殺的url實現動態化,即使是開發整個系統的人都無法在秒殺開始前知道秒殺的url。具體的做法就是通過md5加密一串隨機字元作為秒殺的url,然後前端訪問後臺獲取具體的url,後臺校驗通過之後才可以繼續秒殺。

2.3:秒殺頁面靜態化

將商品的描述、引數、成交記錄、影象、評價等全部寫入到一個靜態頁面,使用者請求不需要通過訪問後端伺服器,不需要經過資料庫,直接在前臺客戶端生成,這樣可以最大可能的減少伺服器的壓力。具體的方法可以使用freemarker模板技術,建立網頁模板,填充資料,然後渲染網頁

2.4:單體redis升級為叢集redis

秒殺是一個讀多寫少的場景,使用redis做快取再合適不過。不過考慮到快取擊穿問題,我們應該構建redis叢集,採用哨兵模式,可以提升redis的效能和可用性。

 2.5:使用nginx

nginx是一個高效能web伺服器,它的併發能力可以達到幾萬,而tomcat只有幾百。通過nginx對映客戶端請求,再分發到後臺tomcat伺服器叢集中可以大大提升併發能力。

2.6:精簡sql

典型的一個場景是在進行扣減庫存的時候,傳統的做法是先查詢庫存,再去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設值,比如redis.set(goodsId,100),這裡預放的庫存為100可以設值為常量),每次下單成功之後,Integer stock = (Integer)redis.get(goosId); 然後判斷sock的值,如果小於常量值就減去1;不過注意當取消的時候,需要增加庫存,增加庫存的時候也得注意不能大於之間設定的總庫存數(查詢庫存和扣減庫存需要原子操作,此時可以藉助lua指令碼)下次下單再獲取庫存的時候,直接從redis裡面查就可以了。

2.8:介面限流

秒殺最終的本質是資料庫的更新,但是有很多大量無效的請求,我們最終要做的就是如何把這些無效的請求過濾掉,防止滲透到資料庫。限流的話,需要入手的方面很多:

2.9.1:前端限流

首先第一步就是通過前端限流,使用者在秒殺按鈕點選以後發起請求,那麼在接下來的5秒是無法點選(通過設定按鈕為disable)。這一小舉措開發起來成本很小,但是很有效。

2.9.2:同一個使用者xx秒內重複請求直接拒絕

具體多少秒需要根據實際業務和秒殺的人數而定,一般限定為10秒。具體的做法就是通過redis的鍵過期策略,首先對每個請求都從String value = redis.get(userId);如果獲取到這個

value為空或者為null,表示它是有效的請求,然後放行這個請求。如果不為空表示它是重複性請求,直接丟掉這個請求。如果有效,採用redis.setexpire(userId,value,10).value可以是任意值,一般放業務屬性比較好,這個是設定以userId為key,10秒的過期時間(10秒後,key對應的值自動為null)

2.7.3:令牌桶演算法限流

介面限流的策略有很多,我們這裡採用令牌桶演算法。令牌桶演算法的基本思路是每個請求嘗試獲取一個令牌,後端只處理持有令牌的請求,生產令牌的速度和效率我們都可以自己限定,guava提供了RateLimter的api供我們使用。以下做一個簡單的例子,注意需要引入guava

public class TestRateLimiter {

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

   上面程式碼的思路就是通過RateLimiter來限定我們的令牌桶每秒產生1個令牌(生產的效率比較低),迴圈10次去執行任務。acquire會阻塞當前執行緒直到獲取到令牌,也就是如果任務沒有獲取到令牌,會一直等待。那麼請求就會卡在我們限定的時間內才可以繼續往下走,這個方法返回的是執行緒具體等待的時間。執行如下;

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

public class TestRateLimiter2 {

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

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

其中用到了tryAcquire方法,這個方法的主要作用是設定一個超時的時間,如果在指定的時間內預估(注意是預估並不會真實的等待),如果能拿到令牌就返回true,如果拿不到就返回false.然後我們讓無效的直接跳過,這裡設定每秒生產1個令牌,讓每個任務嘗試在

0.5秒獲取令牌,如果獲取不到,就直接跳過這個任務(放在秒殺環境裡就是直接拋棄這個請求);程式實際執行如下:

只有第1個獲取到了令牌,順利執行了,下面的基本都直接拋棄了,因為0.5秒內,令牌桶(1秒1個)來不及生產就肯定獲取不到返回false了。

2.8:非同步下單

為了提升下單的效率,並且防止下單服務的失敗。需要將下單這一操作進行非同步處理。最常採用的辦法是使用佇列,佇列最顯著的三個優點:非同步、削峰、解耦。這裡可以採用rabbitmq,在後臺經過了限流、庫存校驗之後,流入到這一步驟的就是有效請求。然後傳送到佇列裡,佇列接受訊息,非同步下單。下完單,入庫沒有問題可以用簡訊通知使用者秒殺成功。假如失敗的話,可以採用補償機制,重試。

2.9:服務降級

假如在秒殺過程中出現了某個伺服器當機,或者服務不可用,應該做好後備工作。之前的部落格裡有介紹通過Hystrix進行服務熔斷和降級,可以開發一個備用服務,假如伺服器真的當機了,直接給使用者一個友好的提示返回,而不是直接卡死,伺服器錯誤等生硬的反饋。

三:總結

秒殺流程圖:

      這就是我設計出來的秒殺流程圖,當然不同的秒殺體量針對的技術選型都不一樣,這個流程可以支撐起幾十萬的流量,如果是成千萬破億那就得重新設計了。比如資料庫的分庫分表、佇列改成用kafka、redis增加叢集數量等手段。通過本次設計主要是要表明的是我們如何應對高併發的處理,並開始嘗試解決它,在工作中多思考、多動手能提升我們的能力水平,加油!如果本篇部落格有任何錯誤,請麻煩指出來,不勝感激。

相關文章