秒殺系統相信不少人見過,好比京東或者淘寶的秒殺,小米手機的秒殺,那麼秒殺系統的後臺是如何實現的呢?咱們如何設計一個秒殺系統呢?對於秒殺系統應該考慮哪些問題?如何設計出健壯的秒殺系統?本文咱們就來探討一下這個問題。php
秒殺應該考慮哪些問題html
超賣問題前端
分析秒殺的業務場景,最重要的有一點就是超賣問題,假如備貨只有100個,可是最終超賣了200,通常來說秒殺系統的價格都比較低,若是超賣將嚴重影響公司的財產利益,所以首當其衝的就是解決商品的超賣問題。redis
高併發算法
秒殺具備時間短、併發量大的特色,秒殺持續時間只有幾分鐘,而通常公司都爲了製造轟動效應,會以極低的價格來吸引用戶,所以參與搶購的用戶會很是的多。短期內會有大量請求涌進來,後端如何防止併發太高形成緩存擊穿或者失效,擊垮數據庫都是須要考慮的問題。數據庫
接口防刷後端
如今的秒殺大多都會出來針對秒殺對應的軟件,這類軟件會模擬不斷向後臺服務器發起請求,一秒幾百次都是很常見的,如何防止這類軟件的重複無效請求,防止不斷髮起的請求也是須要咱們針對性考慮的。瀏覽器
秒殺url緩存
對於普通用戶來說,看到的只是一個比較簡單的秒殺頁面,在未達到規定時間,秒殺按鈕是灰色的,一旦到達規定時間,灰色按鈕變成可點擊狀態。這部分是針對小白用戶的,若是是稍微有點電腦功底的用戶,會經過F12看瀏覽器的network看到秒殺的url,經過特定軟件去請求也能夠實現秒殺。或者提早知道秒殺url的人,一請求就直接實現秒殺了。這個問題咱們須要考慮解決。服務器
數據庫設計
秒殺有把咱們服務器擊垮的風險,若是讓它與咱們的其餘業務使用在同一個數據庫中,耦合在一塊兒,就頗有可能牽連和影響其餘的業務。如何防止這類問題發生,就算秒殺發生了宕機、服務器卡死問題,也應該讓他儘可能不影響線上正常進行的業務。
大量請求問題
按照「高併發」的考慮,就算使用緩存仍是不足以應對短期的高併發的流量的衝擊。如何承載這樣巨大的訪問量,同時提供穩定低時延的服務保證,是須要面對的一大挑戰。咱們來算一筆帳,假如使用的是Redis緩存,單臺Redis服務器可承受的QPS大概是4W左右,若是一個秒殺吸引的用戶量足夠多的話,單QPS可能達到幾十萬,單體Redis仍是不足以支撐如此巨大的請求量。緩存會被擊穿,直接滲透到DB,從而擊垮MySQL,後臺會將會大量報錯。
秒殺系統的設計和技術方案
秒殺系統數據庫設計
針對「數據庫設計」提出的秒殺數據庫的問題,所以應該單獨設計一個秒殺數據庫,防止由於秒殺活動的高併發訪問拖垮整個網站。這裏只須要兩張表,一張是秒殺訂單表,一張是秒殺貨品表。
其實應該還有幾張表,商品表:能夠關聯goods_id查到具體的商品信息,商品圖像、名稱、平時價格、秒殺價格等,還有用戶表:根據用戶user_id能夠查詢到用戶暱稱、用戶手機號,收貨地址等其餘額外信息,這個具體就不給出實例了。
秒殺url的設計
爲了不有程序訪問經驗的人經過下單頁面url直接訪問後臺接口來秒殺貨品,咱們須要將秒殺的url實現動態化,即便是開發整個系統的人都沒法在秒殺開始前知道秒殺的url。具體的作法就是經過md5加密一串隨機字符做爲秒殺的url,而後前端訪問後臺獲取具體的url,後臺校驗經過以後才能夠繼續秒殺。
秒殺頁面靜態化
將商品的描述、參數、成交記錄、圖像、評價等所有寫入到一個靜態頁面,用戶請求不須要經過訪問後端服務器,不須要通過數據庫,直接在前臺客戶端生成,這樣能夠最大可能的減小服務器的壓力。具體的方法可使用freemarker模板技術,創建網頁模板,填充數據,而後渲染網頁。
單體Redis升級爲集羣Redis
秒殺是一個讀多寫少的場景,使用Redis作緩存再合適不過。不過考慮到緩存擊穿問題,咱們應該構建Redis集羣,採用哨兵模式,能夠提高Redis的性能和可用性。
使用Nginx
Nginx是一個高性能Web服務器,它的併發能力能夠達到幾萬,而Tomcat只有幾百。經過Nginx映射客戶端請求,再分發到後臺Tomcat服務器集羣中能夠大大提高併發能力。
精簡SQL
典型的一個場景是在進行扣減庫存的時候,傳統的作法是先查詢庫存,再去update。這樣的話須要兩個SQL,而實際上一個SQL咱們就能夠完成的。能夠用這樣的作法:update miaosha_goods set stock =stock-1 where goos_id ={#goods_id} and version = #{version} and sock>0;這樣的話,就能夠保證庫存不會超賣而且一次更新庫存,還有注意一點這裏使用了版本號的樂觀鎖,相比較悲觀鎖,它的性能較好。
Redis預減庫存
不少請求進來,都須要後臺查詢庫存,這是一個頻繁讀的場景。可使用Redis來預減庫存,在秒殺開始前能夠在Redis設值,好比redis.set(goodsId,100),這裏預放的庫存爲100能夠設值爲常量,每次下單成功以後,Integer stock = (Integer)redis.get(goosId); 而後判斷sock的值,若是小於常量值就減去1;不過注意當取消的時候,須要增長庫存,增長庫存的時候也得注意不能大於之間設定的總庫存數(查詢庫存和扣減庫存須要原子操做,此時能夠藉助lua腳本)下次下單再獲取庫存的時候,直接從Redis裏面查就能夠了。
接口限流
秒殺最終的本質是數據庫的更新,可是有不少大量無效的請求,咱們最終要作的就是如何把這些無效的請求過濾掉,防止滲透到數據庫。限流的話,須要入手的方面不少:
前端限流
首先第一步就是經過前端限流,用戶在秒殺按鈕點擊之後發起請求,那麼在接下來的5秒是沒法點擊(經過設置按鈕爲disable)。這一小舉措開發起來成本很小,可是頗有效。
同一個用戶xx秒內重複請求直接拒絕
具體多少秒須要根據實際業務和秒殺的人數而定,通常限定爲10秒。具體的作法就是經過Redis的鍵過時策略,首先對每一個請求都從String value = redis.get(userId);若是獲取到這個value爲空或者爲null,表示它是有效的請求,而後放行這個請求。若是不爲空表示它是重複性請求,直接丟掉這個請求。若是有效,採用redis.setexpire(userId,value,10).value能夠是任意值,通常放業務屬性比較好,這個是設置以userId爲key,10秒的過時時間(10秒後,key對應的值自動爲null)。
令牌桶算法限流
接口限流的策略有不少,咱們這裏採用令牌桶算法。令牌桶算法的基本思路是每一個請求嘗試獲取一個令牌,後端只處理持有令牌的請求,生產令牌的速度和效率咱們均可以本身限定,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了。
這個限流策略的效率有多高呢?假如咱們的併發請求是400萬瞬間的請求,將令牌產生的效率設爲每秒20個,每次嘗試獲取令牌的時間是0.05秒,那麼最終測試下來的結果是,每次只會放行4個左右的請求,大量的請求會被拒絕,這就是令牌桶算法的優秀之處。
異步下單
爲了提高下單的效率,而且防止下單服務的失敗。須要將下單這一操做進行異步處理。最常採用的辦法是使用隊列,隊列最顯著的三個優勢:異步、削峯、解耦。這裏能夠採用RabbitMQ,在後臺通過了限流、庫存校驗以後,流入到這一步驟的就是有效請求。而後發送到隊列裏,隊列接受消息,異步下單。下完單,入庫沒有問題能夠用短信通知用戶秒殺成功。假如失敗的話,能夠採用補償機制,重試。
服務降級
假如在秒殺過程當中出現了某個服務器宕機,或者服務不可用,應該作好後備工做。以前的博客裏有介紹經過Hystrix進行服務熔斷和降級,能夠開發一個備用服務,假如服務器真的宕機了,直接給用戶一個友好的提示返回,而不是直接卡死,服務器錯誤等生硬的反饋。
總結
秒殺流程圖:
這就是我設計出來的秒殺流程圖,固然不一樣的秒殺體量針對的技術選型都不同,這個流程能夠支撐起幾十萬的流量,若是是成千萬破億那就得從新設計了。好比數據庫的分庫分表、隊列改爲用Kafka、Redis增長集羣數量等手段。經過本次設計主要是要代表的是咱們如何應對高併發的處理,並開始嘗試解決它,在工做中多思考、多動手能提高咱們的能力水平,加油!若是本篇博客有任何錯誤,請麻煩指出來,不勝感激。
原文連接:https://www.cnblogs.com/wyq178/p/11261711.html
Kubernetes管理員認證(CKA)培訓
本次CKA培訓將於11月20到22日在北京開課,培訓基於最新考綱,經過線下授課、考題解讀、模擬演練等方式,幫助學員快速掌握Kubernetes的理論知識和專業技能,並針對考試作特別強化訓練,讓學員能從容面對CKA認證考試,使學員既能掌握Kubernetes相關知識,又能經過CKA認證考試,學員可屢次參加培訓,直到經過認證。點擊下方圖片或者閱讀原文連接查看詳情。