秒殺系統優化方案(下)吐血整理

接上篇秒殺系統優化方案(上)吐血整理

3. 深刻優化設計

3.1   初始方案問題分析

在前面針對數據庫的優化中,因爲數據庫行級鎖存在競爭形成大量的串行阻塞,咱們使用了存儲過程(或者觸發器)等技術綁定操做,整個事務在MySQL端完成,把整個熱點執行放在一個過程中一次性完成,能夠屏蔽掉網絡延遲時間,減小行級鎖持有時間,提升事務併發訪問速度。html

但是問題時併發的流量實際上都是直接穿透讓MYSQL本身去抗,好比說庫存是否賣完以及用戶是否重複秒殺都徹底是靠查詢數據庫去判斷,形成數據庫沒必要要的負擔很是大,然而這些均可以放在緩存作一個標記在服務層進行攔截,對於中小規模的併發還能夠,可是真正的超高併發,顯然這個還不完善。前端

3.2    優化的方向和思路

方向:將請求儘可能攔截在系統上游ajax

傳統秒殺系統之因此掛,請求都壓倒了後端數據層,數據讀寫鎖衝突嚴重,併發高響應慢,幾乎全部請求都超時,流量雖大,下單成功的有效流量甚小【一趟火車其實只有2000張票,200w我的來買,基本沒有人能買成功,請求有效率爲0】 redis

思路:限流和削峯算法

限流:屏蔽掉無用的流量,容許少部分流量流向後端。sql

削峯:瞬時大流量峯值容易壓垮系統,解決這個問題是重中之重。經常使用的消峯方法有異步處理、緩存和消息中間件等技術。數據庫

 

異步處理:秒殺系統是一個高併發系統,採用異步處理模式能夠極大地提升系統併發量,其實異步處理就是削峯的一種實現方式。後端

緩存:秒殺系統自己是一個典型的讀多寫少的應用場景【一趟火車其實只有2000張票,200w我的來買,最多2000我的下單成功,其餘人都是查詢庫存,寫比例只有0.1%,讀比例佔99.9%】,很是適合使用緩存。瀏覽器

消息隊列:消息隊列能夠削峯,將攔截大量併發請求,這也是一個異步處理過程,後臺業務根據本身的處理能力,從消息隊列中主動的拉取請求消息進行業務處理。緩存

3.3   前端優化

3.3.1   靜態資源緩存

1. 頁面靜態化

對商品詳情和訂單詳情進行頁面靜態化處理,頁面是存在html,動態數據是經過接口從服務端獲取,實現先後端分離,靜態頁面無需鏈接數據庫打開速度較動態頁面會有明顯提升。

2.頁面緩存

經過CDN緩存靜態資源,來抗峯值。不使用CDN的話也能夠經過在手動渲染獲得的html頁面緩存到redis。

3.3.2   限流手段

1. 使用數學公式驗證碼

描述:點擊秒殺前,先讓用戶輸入數學公式驗證碼,驗證正確才能進行秒殺。

好處:

1)防止惡意的機器人和爬蟲

2)分散用戶的請求

實現:

1)前端經過把商品id做爲參數調用服務端建立驗證碼接口

2)服務端根據前端傳過來的商品id和用戶id生成驗證碼,並將商品id+用戶id做爲key,生成的驗證碼做爲value存入redis,同時將生成的驗證碼輸入圖片寫入imageIO讓前端展現。

3)將用戶輸入的驗證碼與根據商品id+用戶id從redis查詢到的驗證碼對比,相同就返回驗證成功,進入秒殺;不一樣或從redis查詢的驗證碼爲空都返回驗證失敗,刷新驗證碼重試

 

2. 禁止重複提交

用戶提交以後按鈕置灰,禁止重複提交 

3.4    中間代理層

可利用負載均衡(例如反響代理Nginx等)使用多個服務器併發處理請求,減少服務器壓力。

3.5     後端優化

3.5.1   控制層(網關層)

限制同一UserID訪問頻率:儘可能攔截瀏覽器請求,但針對某些惡意攻擊或其它插件,在服務端控制層須要針對同一個訪問uid,限制訪問頻率。

1.    利用緩存

設置緩存有效時間,在緩存中計數,若是在緩存的有效時間內請求的次數超了的話,就返回請求訪問太頻繁。

2.    利用RateLimiter

RateLimiter是guava提供的基於令牌桶算法的限流實現類,經過調整生成token的速率來限制用戶頻繁訪問秒殺頁面,從而達到防止超大流量沖垮系統。(令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。

3.5.2   服務層

當用戶量很是大的時候,攔截流量後的請求訪問量仍是很是大,此時仍需進一步優化。

1.    業務分離:將秒殺業務系統和其餘業務分離,單獨放在高配服務器上,能夠集中資源對訪問請求抗壓。——應用的拆分

2.    採用消息隊列緩存請求:將大流量請求寫到消息隊列緩存,利用服務器根據本身的處理能力主動到消息緩存隊列中抓取任務處理請求,數據庫層訂閱消息減庫存,減庫存成功的請求返回秒殺成功,失敗的返回秒殺結束。

3.    利用緩存應對讀請求:對於讀多寫少業務,大部分請求是查詢請求,因此能夠讀寫分離,利用緩存分擔數據庫壓力。

4.    利用緩存應對寫請求:緩存也是能夠應對寫請求的,可把數據庫中的庫存數據轉移到Redis緩存中,全部減庫存操做都在Redis中進行,而後再經過後臺進程把Redis中的用戶秒殺請求同步到數據庫中。

能夠將緩存和消息中間件 組合起來,緩存系統負責接收記錄用戶請求,消息中間件負責將緩存中的請求同步到數據庫。

 

方案:本地標記 + redis預處理 + RabbitMQ異步下單 + 客戶端輪詢

描述:經過三級緩衝保護,一、本地標記 二、redis預處理 三、RabbitMQ異步下單,最後纔會訪問數據庫,這樣作是爲了最大力度減小對數據庫的訪問。

實現:

  1. 在秒殺階段使用本地標記對用戶秒殺過的商品作標記,若被標記過直接返回重複秒殺,未被標記才查詢redis,經過本地標記來減小對redis的訪問
  2. 搶購開始前,將商品和庫存數據同步到redis中,全部的搶購操做都在redis中進行處理,經過Redis預減小庫存減小數據庫訪問
  3. 爲了保護系統不受高流量的衝擊而致使系統崩潰的問題,使用RabbitMQ用異步隊列處理下單,實際作了一層緩衝保護,作了一個窗口模型,窗口模型會實時的刷新用戶秒殺的狀態。
  4. client端用js輪詢一個接口,用來獲取處理狀態

3.5.3  數據庫層

  數據庫層是最脆弱的一層,通常在應用設計時在上游就須要把請求攔截掉,數據庫層只承擔「能力範圍內」的訪問請求。因此,上面經過在服務層引入隊列和緩存,讓最底層的數據庫高枕無憂。但依然能夠進行以下方向的優化:

 對於秒殺系統,直接訪問數據庫的話,存在一個【事務競爭優化】問題,可以使用存儲過程(或者觸發器)等技術綁定操做,整個事務在MySQL端完成,把整個熱點執行放在一個過程中一次性完成,能夠屏蔽掉網絡延遲時間,減小行級鎖持有時間,提升事務併發訪問速度。

 

3.7  優化秒殺流程

  1. 秒殺活動開始以前有個活動倒計時,時間到了則會放開秒殺的權限,並生成一個驗證碼展現在前面頁面,並把驗證結果存在redis中,這裏利用redis有過時時間的特性,也給驗證碼的緩存加了個過時時間。這裏的redis緩存用的是redis的string類型。
  2. 在秒殺以前先要填一個驗證碼verifyCode,點擊秒殺按鈕時,先發送ajax請求到後臺獲取真實的秒殺地址path,這裏秒殺地址是隱藏的,目的是防止有人惡意刷秒殺接口。所謂隱藏地址,實際上是在請求地址中加一段隨機字符串,這段字符串是變化的,所以秒殺請求地址是動態的;
  3. 先說下如何獲取真實的秒殺地址,後臺先訪問redis,驗證一下這個驗證碼有沒有過時以及這個verifyCode是否是正確,驗證碼驗證經過後,先刪除這個驗證碼緩存,而後生成真實地址;
  4. 真實地址隨機字符串由uuid以及md5加密生成,而且保存在redis中,而且設置了有效期;
  5. 從瀏覽器端向秒殺地址發起請求,帶上path參數去後臺調用真正的秒殺接口,下面是秒殺接口的邏輯
  6. 訪問redis,驗證path有沒有過時,以及是否是正確。這裏驗證path以及上面的校驗驗證碼,都是用userId對應生成的一個key值去取redis中的數據;
  7. path驗證經過後,先訪問內存標識,看秒殺的這個商品有沒有賣完,減小對redis的沒必要要訪問。每一種參與秒殺活動的商品都在內存裏用HashMap設置了一個標識,標識某個商品id商品是否賣完了。這裏的是否賣完的內存標識設置以及每種參與秒殺商品的庫存存入redis是在系統啓動時作的;
  8. 若是內存標識中這個商品沒有賣完,則要看這個用戶在此次活動中是否重複秒殺,由於咱們的秒殺規則是一個用戶id對於某個商品id的商品只能秒殺一件。如何判斷該用戶有沒有秒殺過這件商品呢,秒殺記錄也保存在redis緩存中
  9. 若是判斷秒殺過則返回提示,若是沒有秒殺過,繼續;
  10. 上面說過系統加載時redis中保存了各商品對應的庫存,這裏用到redis的原子操做的方法decr,將對應商品的庫存減1,此時數據庫時的庫存尚未減,所以是預減庫存
  11. desc方法返回該商品此時的庫存,若是小於0,說明商品已經賣完了,這次秒殺無效,而且設置該商品的內存標識爲true,表示已賣完
  12. 正確地預減庫存後,而後就要真正操做數據庫了,數據庫通常是性能瓶頸,比較耗時,所以決定用異步方式處理。對於每一條秒殺請求存入消息隊列RabbitMQ中,消息體中要包含哪一個用戶秒殺哪一個商品的信息,這裏是封裝了一個消息體類,這樣一個秒殺請求就進入了消息隊列,一個秒殺請求尚未完成,真正的秒殺請求的完成得要持久化到數據庫,生成訂單,減了數據庫的庫存才能算數,這時在客戶端顯示的通常是排隊中,好比之前在搶購小米手機時,我就看到這樣的展現,過一會再刷新頁面就顯示沒搶到;
  13. 消息隊列處理秒殺請求。先從消息體中解析出用戶id和商品id,查數據庫看這個商品是否賣完了查數據庫看該用戶對於這個商品是否有過秒殺記錄數據庫減庫存,數據庫生成訂單,這兩項持久化地寫數據庫操做放在同一個事務中,要麼都執行成功,要麼都失敗。並把秒殺記錄對象,包括秒殺單號、訂單號、用戶id、商品id,存入redis若是數據庫減庫存失敗,代表商品賣完了,則要在redis中設置該商品已賣完的標識消息隊列處理秒殺請求。先從消息體中解析出用戶id和商品id,查數據庫看這個商品是否賣完了查數據庫看該用戶對於這個商品是否有過秒殺記錄
  14. 數據庫減庫存,數據庫生成訂單,這兩項持久化地寫數據庫操做放在同一個事務中,要麼都執行成功,要麼都失敗。並把秒殺記錄對象,包括秒殺單號、訂單號、用戶id、商品id,存入redis若是數據庫減庫存失敗,代表商品賣完了,則要在redis中設置該商品已賣完的標識
  15. ajax發起秒殺請求,秒殺請求的處理邏輯最後也只是把這條請求放入消息隊列,並不能返回是否秒殺成功的結果。所以,當秒殺請求正確響應後,即請求放入消息隊列後,須要另一個請求去輪詢秒殺結果,秒殺成功的標誌是生成秒殺訂單,並把秒殺訂單對象放入redis中。因此輪詢秒殺結果,只用去輪詢redis中是否有對應於該用戶的該商品的秒殺訂單對象,若是有,則代表秒殺成功,並在前臺給出提示。

上面的秒殺流程對應的流程圖以下:
步驟1到12,主體是redis預減庫存,生成消息隊列:

 

步驟13到14是處理消息隊列:

步驟15,是客戶端請求秒殺結果:

 

4. 問題解析

1.      如何解決庫存的超賣問題?

賣超緣由:

(1)一個用戶同時發出了多個請求,若是庫存足夠,沒加限制,用戶就能夠下多個訂單。(2)減庫存的sql上沒有加庫存數量的判斷,併發的時候也會致使把庫存減成負數。

解決辦法:

(1):在後端的秒殺表中,對user_id和goods_id加惟一索引,確保一個用戶對一個商品絕對不會生成兩個訂單。

(2):咱們的減庫存的sql上應該加上庫存數量的判斷

數據庫自身是有行級鎖的,每次減庫存的時候判斷count>0,它其實是串行的執行update的,所以絕對不會賣超!。

UPDATE seckill

        SET number = number-1

        WHERE seckill_id=#{seckillId}

        AND start_time <#{killTime}

        AND end_time >= #{killTime}

        AND number > 0;

 2.    如何解決少賣問題—Redis預減成功而DB扣庫存失敗?

前面的方案中會出現一個少賣的問題。Redis在預減庫存的時候,在初始化的時候就放置庫存的大小,redis的原子減操做保證了多少庫存就會減多少,也就會在消息隊列中放多少。

如今考慮兩種狀況:

1)數據庫那邊出現非庫存緣由好比網絡等形成減庫存失敗,而這時redis已經減了。

2)萬一一個用戶發出多個請求,並且這些請求恰巧比別的請求更早到達服務器,若是庫存足夠,redis就會減屢次,redis提早進入賣空狀態,並拒絕。不過這兩種狀況出現的機率都是很是低的。

兩種狀況都會出現少賣的問題,實際上也是緩存和數據庫出現不一致的問題

可是咱們不是非得解決不一致的問題,自己使用緩存就難以保證強一致性:

在redis中設置庫存比真實庫存多一些就行。

3.   秒殺過程當中怎麼保證redis緩存和數據庫的一致性?

在其餘通常讀大於寫的場景,通常處理的原則是:緩存只作失效,不作更新。

採用Cache-Aside pattern:

失效:應用程序先從cache取數據,沒有獲得,則從數據庫中取數據,成功後,放到緩存中。

更新:先把數據存到數據庫中,成功後,再讓緩存失效。

 4.  Redis中的庫存如何與DB中的庫存保持一致?

Redis中的數量不是庫存,它的做用僅僅時候只是爲了阻擋多餘的請求透傳到db,起到一個保護DB的做用。由於秒殺商品的數量是有限的,好比只有10個,讓1萬個請求去訪問DB是沒有意義的,由於最多隻有10個請求會下單成功,剩餘的9990個請求都是無效的,是能夠不用去訪問db而直接失敗的。

所以,這是一個僞問題,咱們是不須要保持一致的。

 5.   爲何要隱藏秒殺接口?

html是能夠被右鍵->查看源代碼,若是秒殺地址寫死在源文件中,是很容易就被惡意用戶拿到的,就能夠被機器人利用來刷接口,這對於其餘用戶來講是不公平的,咱們也不但願看到這種狀況。因此咱們能夠控制讓用戶在沒有到秒殺時間的時候不能獲取到秒殺地址,只返回秒殺的開始時間。

當到秒殺時間的時候才返回秒殺地址即seckill_id以及根據seckill_id和salt加密的MD5,前端再次拿着seckill_id和MD5才能執行秒殺。假如用戶在秒殺開始前猜想到秒殺地址seckill_id去請求秒殺,也是不會成功的,由於它拿不到須要驗證的MD5。這裏的MD5至關因而用戶進行秒殺的憑證。

6.   一個秒殺系統,500用戶同時登錄訪問服務器A,服務器B如何快速利用登陸名(假設是電話號碼或者郵箱)作其餘查詢?

主從複製,讀寫分離

相關文章
相關標籤/搜索