在前面針對數據庫的優化中,因爲數據庫行級鎖存在競爭形成大量的串行阻塞,咱們使用了存儲過程(或者觸發器)等技術綁定操做,整個事務在MySQL端完成,把整個熱點執行放在一個過程中一次性完成,能夠屏蔽掉網絡延遲時間,減小行級鎖持有時間,提升事務併發訪問速度。html
但是問題時併發的流量實際上都是直接穿透讓MYSQL本身去抗,好比說庫存是否賣完以及用戶是否重複秒殺都徹底是靠查詢數據庫去判斷,形成數據庫沒必要要的負擔很是大,然而這些均可以放在緩存作一個標記在服務層進行攔截,對於中小規模的併發還能夠,可是真正的超高併發,顯然這個還不完善。前端
方向:將請求儘可能攔截在系統上游ajax
傳統秒殺系統之因此掛,請求都壓倒了後端數據層,數據讀寫鎖衝突嚴重,併發高響應慢,幾乎全部請求都超時,流量雖大,下單成功的有效流量甚小【一趟火車其實只有2000張票,200w我的來買,基本沒有人能買成功,請求有效率爲0】 redis
思路:限流和削峯算法
限流:屏蔽掉無用的流量,容許少部分流量流向後端。sql
削峯:瞬時大流量峯值容易壓垮系統,解決這個問題是重中之重。經常使用的消峯方法有異步處理、緩存和消息中間件等技術。數據庫
異步處理:秒殺系統是一個高併發系統,採用異步處理模式能夠極大地提升系統併發量,其實異步處理就是削峯的一種實現方式。後端
緩存:秒殺系統自己是一個典型的讀多寫少的應用場景【一趟火車其實只有2000張票,200w我的來買,最多2000我的下單成功,其餘人都是查詢庫存,寫比例只有0.1%,讀比例佔99.9%】,很是適合使用緩存。瀏覽器
消息隊列:消息隊列能夠削峯,將攔截大量併發請求,這也是一個異步處理過程,後臺業務根據本身的處理能力,從消息隊列中主動的拉取請求消息進行業務處理。緩存
1. 頁面靜態化
對商品詳情和訂單詳情進行頁面靜態化處理,頁面是存在html,動態數據是經過接口從服務端獲取,實現先後端分離,靜態頁面無需鏈接數據庫打開速度較動態頁面會有明顯提升。
2.頁面緩存
經過CDN緩存靜態資源,來抗峯值。不使用CDN的話也能夠經過在手動渲染獲得的html頁面緩存到redis。
1. 使用數學公式驗證碼
描述:點擊秒殺前,先讓用戶輸入數學公式驗證碼,驗證正確才能進行秒殺。
好處:
1)防止惡意的機器人和爬蟲
2)分散用戶的請求
實現:
1)前端經過把商品id做爲參數調用服務端建立驗證碼接口
2)服務端根據前端傳過來的商品id和用戶id生成驗證碼,並將商品id+用戶id做爲key,生成的驗證碼做爲value存入redis,同時將生成的驗證碼輸入圖片寫入imageIO讓前端展現。
3)將用戶輸入的驗證碼與根據商品id+用戶id從redis查詢到的驗證碼對比,相同就返回驗證成功,進入秒殺;不一樣或從redis查詢的驗證碼爲空都返回驗證失敗,刷新驗證碼重試
2. 禁止重複提交
用戶提交以後按鈕置灰,禁止重複提交
可利用負載均衡(例如反響代理Nginx等)使用多個服務器併發處理請求,減少服務器壓力。
限制同一UserID訪問頻率:儘可能攔截瀏覽器請求,但針對某些惡意攻擊或其它插件,在服務端控制層須要針對同一個訪問uid,限制訪問頻率。
1. 利用緩存
設置緩存有效時間,在緩存中計數,若是在緩存的有效時間內請求的次數超了的話,就返回請求訪問太頻繁。
2. 利用RateLimiter
RateLimiter是guava提供的基於令牌桶算法的限流實現類,經過調整生成token的速率來限制用戶頻繁訪問秒殺頁面,從而達到防止超大流量沖垮系統。(令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。
當用戶量很是大的時候,攔截流量後的請求訪問量仍是很是大,此時仍需進一步優化。
1. 業務分離:將秒殺業務系統和其餘業務分離,單獨放在高配服務器上,能夠集中資源對訪問請求抗壓。——應用的拆分
2. 採用消息隊列緩存請求:將大流量請求寫到消息隊列緩存,利用服務器根據本身的處理能力主動到消息緩存隊列中抓取任務處理請求,數據庫層訂閱消息減庫存,減庫存成功的請求返回秒殺成功,失敗的返回秒殺結束。
3. 利用緩存應對讀請求:對於讀多寫少業務,大部分請求是查詢請求,因此能夠讀寫分離,利用緩存分擔數據庫壓力。
4. 利用緩存應對寫請求:緩存也是能夠應對寫請求的,可把數據庫中的庫存數據轉移到Redis緩存中,全部減庫存操做都在Redis中進行,而後再經過後臺進程把Redis中的用戶秒殺請求同步到數據庫中。
能夠將緩存和消息中間件 組合起來,緩存系統負責接收記錄用戶請求,消息中間件負責將緩存中的請求同步到數據庫。
方案:本地標記 + redis預處理 + RabbitMQ異步下單 + 客戶端輪詢
描述:經過三級緩衝保護,一、本地標記 二、redis預處理 三、RabbitMQ異步下單,最後纔會訪問數據庫,這樣作是爲了最大力度減小對數據庫的訪問。
實現:
數據庫層是最脆弱的一層,通常在應用設計時在上游就須要把請求攔截掉,數據庫層只承擔「能力範圍內」的訪問請求。因此,上面經過在服務層引入隊列和緩存,讓最底層的數據庫高枕無憂。但依然能夠進行以下方向的優化:
對於秒殺系統,直接訪問數據庫的話,存在一個【事務競爭優化】問題,可以使用存儲過程(或者觸發器)等技術綁定操做,整個事務在MySQL端完成,把整個熱點執行放在一個過程中一次性完成,能夠屏蔽掉網絡延遲時間,減小行級鎖持有時間,提升事務併發訪問速度。
上面的秒殺流程對應的流程圖以下:
步驟1到12,主體是redis預減庫存,生成消息隊列:
步驟13到14是處理消息隊列:
步驟15,是客戶端請求秒殺結果:
賣超緣由:
(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如何快速利用登陸名(假設是電話號碼或者郵箱)作其餘查詢?
主從複製,讀寫分離