什麼是秒殺
秒殺場景一般會在電商網站舉行一些活動或者節假日在12306網站上搶票時遇到。對於電商網站中一些稀缺或者特價商品,電商網站一般會在約定時間點對其進行限量銷售,因爲這些商品的特殊性,會吸引大量用戶前來搶購,並且會在約定的時間點同時在秒殺頁面進行搶購。
秒殺系統場景特點
- 秒殺時大量用戶會在同一時間同時進行搶購,網站瞬時訪問流量激增。
- 秒殺一般是訪問請求數量遠遠大於庫存數量,只有少部分用戶能夠秒殺成功。
- 秒殺業務流程比較簡單,一般就是下訂單減庫存。
秒殺架構設計理念
保障服務穩定的三大利器:熔斷降級、服務限流和故障模擬。
限流: 鑑於只有少部分用戶能夠秒殺成功,所以要限制大部分流量,只允許少部分流量進入服務後端。
削峯:對於秒殺系統瞬時會有大量用戶涌入,所以在搶購一開始會有很高的瞬間峯值。高峯值流量是壓垮系統很重要的原因,所以如何把瞬間的高流量變成一段時間平穩的流量也是設計秒殺系統很重要的思路。實現削峯的常用的方法有利用緩存和消息中間件等技術。
異步處理:秒殺系統是一個高併發系統,採用異步處理模式可以極大地提高系統併發量,其實異步處理就是削峯的一種實現方式。
內存緩存:秒殺系統最大的瓶頸一般都是數據庫讀寫,由於數據庫讀寫屬於磁盤IO,性能很低,如果能夠把部分數據或業務邏輯轉移到內存緩存,效率會有極大地提升。
可拓展:當然如果我們想支持更多用戶,更大的併發,最好就將系統設計成彈性可拓展的,如果流量來了,拓展機器就好了。像淘寶、京東等雙十一活動時會增加大量機器應對交易高峯。
設計思路
將請求攔截在系統上游,降低下游壓力:秒殺系統特點是併發量極大,但實際秒殺成功的請求數量卻很少,所以如果不在前端攔截很可能造成數據庫讀寫鎖衝突,甚至導致死鎖,最終請求超時。
充分利用緩存:利用緩存可極大提高系統讀寫速度。
異步消息隊列:消息隊列可以削峯,將攔截大量併發請求,這也是一個異步處理過程,後臺業務根據自己的處理能力,從消息隊列中主動的拉取請求消息進行業務處理。
前端方案
瀏覽器端(js):
頁面靜態化:緩存在網絡、緩存在服務器本機、緩存在用戶瀏覽器
1、CDN:將活動頁面上的所有可以靜態的元素全部靜態化,並儘量減少動態元素。通過CDN來抗峯值。
2、可以在服務器端使用freemarker,不需要佔用服務器邏輯,但是會佔用帶寬
3、將服務器的頁面用原生的Js、Jquery等寫出來,並設置spring配置文件參數讓瀏覽器端可以緩存頁面,這樣以後當下次請求到來的時候瀏覽器可以直接走自己的緩存,幾乎不需要與服務器交互
靜態資源還可以進行優化:(tengine)
1、JS/CSS壓縮,減少流量(webpack專門用於打包前端資源)
2、多個JS/CSS組合,減少連接數
禁止表單重複提交:
1、js禁止提交按鈕,用戶提交之後按鈕置灰,禁止重複提交
當用戶提交了表單後,去執行一個客戶端的重定向,轉到提交成功信息頁面。
這能避免用戶按F5導致的重複提交,而其也不會出現瀏覽器表單重複提交的警告,也能消除按瀏覽器前進和後退按導致的同樣問題。
3、在session中存放一個特殊標誌
服務端對用戶生成一個標識符存入session,同時將它寫入表單的隱藏字段中,用戶錄入信息後點擊提交,在服務器端獲取表單中隱藏字段的值,與session中的唯一標識符進行比較,相等說明是首次提交,處理完此次請求後刪除session標識符,下次用戶再提交則失敗
4、使用header函數轉向:用戶點擊表單後立即轉向其他的頁面,使頁面腳本失效
5、在數據庫中添加唯一性約束
6、使用cookie記錄表單提交的狀態(如果用戶禁用cookie則不生效)
用戶限流:在某一時間段內只允許用戶提交一次請求,比如可以採取IP限流
後端方案
服務端控制器層(網關層)
接口防刷方案:
1、限制uid(UserID)訪問頻率:我們上面攔截了瀏覽器訪問的請求,但針對某些惡意攻擊或其它插件,在服務端控制層需要針對同一個訪問uid或者其url,限制訪問頻率。(可以在redis裏面加一次幾秒鐘的key,其value就是用戶可以訪問的次數)
2、驗證碼、滑塊、找不同
服務層
上面只攔截了一部分訪問請求,當秒殺的用戶量很大時,即使每個用戶只有一個請求,到服務層的請求數量還是很大。比如我們有100W用戶同時搶100臺手機,服務層併發請求壓力至少爲100W。
- 採用消息隊列緩存請求:既然服務層知道庫存只有100臺手機,那完全沒有必要把100W個請求都傳遞到數據庫啊,那麼可以先把這些請求都寫到消息隊列緩存一下,數據庫層訂閱消息減庫存,減庫存成功的請求返回秒殺成功,失敗的返回秒殺結束。
- 利用緩存應對讀請求:對類似於12306等購票業務,是典型的讀多寫少業務,大部分請求是查詢請求,所以可以利用緩存分擔數據庫壓力。
- 利用緩存應對寫請求:緩存也是可以應對寫請求的,比如我們就可以把數據庫中的庫存數據轉移到Redis緩存中,所有減庫存操作都在Redis中進行,然後再通過後臺進程把Redis中的用戶秒殺請求同步到數據庫中。
數據庫層
數據庫層是最脆弱的一層,一般在應用設計時在上游就需要把請求攔截掉,數據庫層只承擔“能力範圍內”的訪問請求。所以,上面通過在服務層引入隊列和緩存,讓最底層的數據庫高枕無憂
重啓與過載保護
如果系統發生“雪崩”,貿然重啓服務,是無法解決問題的。最常見的現象是,啓動起來後,立刻掛掉。這個時候,最好在入口層將流量拒絕,然後再將重啓。如果是redis/memcache這種服務也掛了,重啓的時候需要注意“預熱”,並且很可能需要比較長的時間。
秒殺和搶購的場景,流量往往是超乎我們系統的準備和想象的。這個時候,過載保護是必要的。如果檢測到系統滿負載狀態,拒絕請求也是一種保護措施。在前端設置過濾是最簡單的方式,但是,這種做法是被用戶“千夫所指”的行爲。更合適一點的是,將過載保護設置在CGI入口層,快速將客戶的直接請求返回。
https://www.cnblogs.com/andy-zhou/p/5364136.html
架構上:
1、獨立部署秒殺系統:將秒殺系統獨立部署,甚至使用獨立域名,使其與網站完全隔離——防止高併發導致整個網站癱瘓
2、頁面靜態化:重新設計秒殺商品頁面,不使用網站原來的商品詳細頁面,頁面內容靜態化,用戶請求不需要經過應用服務,由反向代理指引至靜態文件處——防止用戶刷新頁面導致的流量過多(服務器上freemarker、網絡上CDN、用戶瀏覽器上spring)
3、CDN緩存:將秒殺商品頁面緩存在CDN,和CDN服務商臨時租借新增的出口帶寬——防止系統帶寬不夠
4、合理設置下單鏈接:
1、將下單頁面URL動態化:是在下單頁面URL加入由服務器端生成的隨機數作爲參數,在秒殺開始的時候才能得到,這個隨機數只生成一次——避免用戶直接訪問下單頁面URL
2、在redis中設置一個key,設置過期時間爲倒計時時間,而下單的邏輯只有當檢測到redis過期纔有效,這樣便可防止提前下單。
5、js文件設置購買按鈕:購買按鈕在秒殺開始時才能點擊。如果頁面是動態生成的,可以在服務端構造響應頁面輸出,控制按鈕狀態,但是因爲要使用上面所說的CDN、靜態頁面與反向代理等,所以在靜態頁面中加入一個JS文件引用,其包含秒殺開始標誌爲false,當秒殺開始由後端系統生成一個相同名字的JS文件,其開始標誌爲true,並帶有下單頁面的URL及服務端生成的隨機數(第4點),js文件帶有隨機版本號,防止被用戶瀏覽器、CDN和反向代理服務器緩存,而且js文件特別小,所以不會對系統造成太大壓力。同時,爲了防止用戶重複提交請求,設置用戶在x秒內只能提交一次請求。
如果是帶有倒計時的按鈕:
爲了防止js調用客戶端時間出現的時間不一致,可以將web服務器的時間發給客戶端,由於不涉及後端邏輯,因此速度很快,同時可以優化http請求使得數據量很小
爲了防止集羣上的不同web服務器時間不一致,可以每隔一分鐘讓所有參與活動的web服務器就與時間服務器做一次時間同步。
6、提高併發量並防止超賣:數據庫樂觀鎖,秒殺系統集羣,查詢走redis,更新用異步kafka方式寫數據庫再設置redis
1、防止超賣1:在數據庫上設置一個版本字段,用於實現樂觀鎖(吞吐量高,性能低);或者在數據庫+唯一索引+判斷庫存>0
2、防止超賣2:每次更新數據庫的時候都獲取表鎖(select for update)或行鎖,實現悲觀鎖(吞吐量低,性能高)
3、防止超賣3:在memcache或redis中設置庫存計數器,每次成功下單-1,直到爲0禁止購買,最後再統一修改庫存
4、防止超賣4:不在數據庫加鎖,而在redis加鎖,當用戶下單某個產品時,將產品id存儲到redis裏面,其他用戶下單時發現redis有這個key,就禁止下單
5、爲了進一步提高秒殺時的吞吐量以及響應效率,對服務進行了集羣,使用nginx做負載均衡
6、爲了防止連接數據庫的請求過多導致數據庫奔潰,使用分佈式限流,將併發控制在一個可控的範圍之內,爲了防止過多的select查詢庫存,可以使用redis緩存,每次查詢庫存時走redis,扣庫存時更新redis(需要提前將庫存信息寫入redis)
7、利用同步轉異步來提高性能,將寫訂單以及更新庫存的操作進行異步化,利用kafka來進行解耦和隊列的作用:每當一個請求通過了限流到達了Service層通過了庫存校驗之後就將訂單信息發給Kafka,這樣一個請求就可以直接返回了(減少佔用系統併發資源),異步之後需要通過回調或其他方式提醒用戶購買完成
7、防止黑客(1%的用戶):用戶使用工具或for循環調用後端的http請求
1)同一個uid,限制訪問頻度,做頁面緩存,x秒內到達站點層的請求,均返回同一頁面
2)同一個item的查詢,例如某款手機頁面,做頁面緩存,x秒內到達站點層的請求,均返回同一頁面
3)限制指定IP的請求頻率(容易出現誤判,比如多人宿舍用一個wifi)
4)彈出驗證碼(防止自動化腳本)
5)多個賬戶、不同IP發送不同請求
對於第五點,說實話,這種場景下的請求,和真實用戶的行爲,已經基本相同了,想做分辨很困難。再做進一步的限制很容易“誤傷“真實用戶,這個時候,通常只能通過設置業務門檻高來限制這種請求了,或者通過賬號行爲的”數據挖掘“來提前清理掉它們。
服務層
ConcurrentLinkedQueue使用的是CAS原語無鎖隊列實現,是一個異步隊列,入隊的速度很快,出隊進行了加鎖,性能稍慢。可以用在用戶請求模塊,存儲請求
ArrayBlockingQueue是初始容量固定的阻塞隊列,我們可以用來作爲數據庫模塊成功競拍的隊列,比如有10個商品,那麼我們就設定一個10大小的數組隊列。
數據庫方面(庫切分、主從複製、雙主互備、雙主當主從(master-slave))
1、爲了解決數據量太大的問題:加入分片機制(水平、垂直拆分)
2、爲了解決可用性問題:加入分組機制(多個一樣的數據庫,主從複製)
目前很多公司採用的是寫主數據庫,而讀從庫,數據庫做主從複製,讀一般有多個,而寫只有一個,有單點故障問題(故障後可以通過選舉的機制選一個從庫當主庫)
如果是有一個用戶發送大量的請求過來,而我們的邏輯是一個用戶只能請求一次,這時判斷邏輯是:讀庫上判斷用戶請求過,進而放行,寫入數據庫,而寫數據庫將數據同步到從庫需要點時間,而這時間用戶的其他請求仍然在從庫得到用戶可以放行的結果(可以採用redis緩存標誌位,也可以採用將用戶的請求放入隊列,讓隊列一個一個處理而不是並行處理)
3、爲了保證寫數據庫的高可用:
1.冗餘寫庫,採用雙主互備的方式
存在自增id衝突的問題,有兩種解決方案:
1)兩個寫庫使用不同的初始值,相同的步長來增加id:1寫庫的id爲0,2,4,6…;2寫庫的id爲1,3,5,7…;
2)不使用數據的id,業務層自己生成唯一的id,保證數據不衝突;
2. 雙主當主從用
有一個主提供服務(讀+寫),另一個主是“shadow-master”,只用來保證高可用,平時不提供服務。 master掛了,shadow-master頂上(vip漂移,對業務層透明,不需要人工介入)
好處:讀寫高可用且沒有延遲
不足:無法擴展讀性能,資源利用率爲50%
綜上:
提高併發量/秒殺手段:
1、設置靜態頁面:CDN緩存(網絡)、freemarker緩存(服務器)、spring設置緩存(瀏覽器)
2、設置緩存減少數據庫訪問:根據商品ID信息設置緩存
3、設置緩存防止用戶刷接口:根據IP/userID+productId作爲redis的key來限制用戶的訪問次數,或者添加驗證碼減低接口訪問速度
4、針對秒殺:可以將秒殺服務單獨部署到服務器,同時租賃雲服務器,防止影響其他業務
5、其他服務可以使用服務降級降低帶寬佔用
6、同步轉異步:使用MQ來處理高併發的秒殺請求、MQ滿了給用戶顯示等待中、庫存扣完了提示用戶賣完了
7、防止超賣:數據庫悲觀/樂觀鎖/redis商品id鎖/redis扣庫存再減數據庫
8、防止表單重複提交:js設置點擊一次後變灰、服務器設置session狀態、瀏覽器用cookie記錄訪問次數。數據庫設置唯一字段……
9、防止用戶提前獲得下單鏈接:服務器redis標記秒殺開始是否開始、秒殺開始時由服務器發送一個隨機數添加到url使得秒殺鏈接有效
10、用戶界面顯示倒計時:由服務端將時間發送客戶端,服務端之間設置一個時間服務器,其他服務器定時與其同步時間
11、限流:計數器、漏桶、令牌桶
參考(部分轉載)鏈接:https://blog.csdn.net/suifeng3051/article/details/52607544