搞清楚秒殺的關(guān)鍵問題所在,有哪些解決辦法衬吆。
知識要點
架構(gòu)設(shè)計原理
如何最大程度分流減壓
如何抵擋突發(fā)大流量
根據(jù)業(yè)務(wù)場景適當?shù)臅r間減庫
做好防刷設(shè)置
系統(tǒng)的伸縮梁钾,監(jiān)控
什么是秒殺
一個時間點開始,大批用戶對少量的商品開始搶購逊抡;往往是價格會低很多姆泻,或者供貨很少,秒級時間內(nèi)就能搶購完
例如:一元秒殺冒嫡,小米手機搶購
或者一定時間段內(nèi)拇勃,對優(yōu)惠的商品進行搶購,這種壓力相對沒有第一種那種大孝凌,更貼切的叫優(yōu)惠促銷
為什么要有秒殺
商家為了吸引顧客潜秋,讓出一部分利益給秒殺商品,得到更多關(guān)注為以后賣出跟多的商品做鋪墊胎许。
或者就是像小米那樣惡意炒作峻呛,如果放開了買大家都觀望,一旦說就數(shù)量有限不買就沒有了辜窑,大家就都回去搶钩述,當然得把握這個度。
我們要怎么去做
秒殺時間點一到穆碎,流量會迅速暴增牙勘,可能是平時的幾十倍上百倍,我們應(yīng)該如何應(yīng)對如此大的流量。
我們?nèi)绾巫屵@么多流量對一份庫存做出正確操作方面,而不會產(chǎn)生超賣放钦,少買以及防止黑客刷單
業(yè)務(wù)分析
首先秒殺需要有的功能模塊有
商品展示
商品信息查詢,商品圖片展示
用戶驗證
用戶信息驗證恭金,商品限制驗證(商品語言操禀,用戶等級限制等)
訂單
用戶下單,創(chuàng)建訂單横腿,減庫颓屑,訂單更新
庫存
庫存同步,分布式系統(tǒng)中一致性保證
支付
訂單支付耿焊,支付信息核對
架構(gòu)設(shè)計
為了能夠抵擋如此高的并發(fā)有幾點我們必須去做
流量預估
首先在秒殺前我們需對流量做一定的預估揪惦,知己知彼先預估下敵人有多少,然后我們怎么對抗罗侯,方案有一下幾種
預約搶購
要搶購就需要提前的預約器腋,然后根據(jù)預約的數(shù)量做一定的百分比判斷。
比如10萬人預約钩杰,并發(fā)最高預估就是10萬纫塌,也有很多人預約了不來,這就要根據(jù)商品的關(guān)注度去預估下百分比榜苫,為了更準確的預估护戳,可以在開始前做短信或者消息推送翎冲,打開鏈接的參與度會高一下垂睬。
UV統(tǒng)計
真正的訪問量是在開始前來到頁面的人數(shù),統(tǒng)計搶購頁面實時的瀏覽人數(shù)
歷史數(shù)據(jù)
之前的秒殺有多少流量抗悍,大致和現(xiàn)在有什么區(qū)別驹饺;
例如:都是手機,上次小米6這次小米8缴渊,流量差距不會特別大赏壹。
宣傳力度
產(chǎn)品定位
等等。衔沼。蝌借。。指蚁。
限流
并發(fā)大了我們不能讓他們來多少就接受多少菩佑,得根據(jù)我們的承受能力去做承受
前端限流
驗證碼
用戶點擊搶購前需要輸入驗證碼
回答問題
比驗證碼的安全級別更高,基本上能防住黑客攻擊凝化,惡意刷單
隨機訪問到服務(wù)器
這樣有點暗箱的意思稍坯,但是很多為了限制流量都這么做;就是用戶端顯示的搶購了搓劫,但是會根據(jù)生產(chǎn)的隨機數(shù)確定要不要請求到后臺瞧哟,比如隨機生產(chǎn)0到10 混巧,只有1可以訪問后臺,一下流量就縮小了10倍
服務(wù)端限流
Nginx限流
帳號勤揩、IP咧党、系統(tǒng)調(diào)用邏輯等在Nginx層面做限流
容器
tomcat(設(shè)置并發(fā)數(shù)),jetty
業(yè)務(wù)限流
有一些限流算法雄可,令牌桶凿傅,漏桶,計算等等
令牌桶
漏桶
令牌桶和漏桶對比:
1.令牌桶是按照固定速率往桶中添加令牌数苫,請求是否被處理需要看桶中令牌是否足夠聪舒,當令牌數(shù)減為零時則拒絕新的請求;
2.漏桶則是按照常量固定速率流出請求虐急,流入請求速率任意箱残,當流入的請求數(shù)累積到漏桶容量時,則新流入的請求被拒絕止吁;
3.令牌桶限制的是平均流入速率(允許突發(fā)請求被辑,只要有令牌就可以處理,支持一次拿3個令牌敬惦,4個令牌)盼理,并允許一定程度突發(fā)流量;
4.漏桶限制的是常量流出速率(即流出速率是一個固定常量值俄删,比如都是1的速率流出宏怔,而不能一次是1,下次又是2)畴椰,從而平滑突發(fā)流入速率臊诊;
5.令牌桶允許一定程度的突發(fā),而漏桶主要目的是平滑流入速率斜脂;
6.兩個算法實現(xiàn)可以一樣抓艳,但是方向是相反的,對于相同的參數(shù)得到的限流效果是一樣的帚戳。
快速處理返回
前邊流量做限制了玷或,還是會有很大的流量到我們服務(wù)器,我們就需要想法做好承受這些請求的能力片任。要多處理數(shù)據(jù)我們就需要處理的快偏友。
請求數(shù)據(jù)的少
數(shù)據(jù)少了傳輸壓力就下,雖然http 都以塊切割蚂踊,但是盡量減少數(shù)據(jù)约谈,塊少一點,隨后解析參數(shù),序列化的時候也會更快一下棱诱,如果一個請求處理時間是100毫秒泼橘,能提升10毫秒就能增加處理百分之的請求。
減少耦合
對我們請求的數(shù)據(jù)減少和外部的耦合迈勋,
比如我們要秒殺的商品不能試用優(yōu)惠劵或積分炬灭,如果使用優(yōu)惠劵或積分我們還需要對優(yōu)惠劵積分系統(tǒng)耦合,耦合多了會拖累我們的服務(wù)靡菇。
減少依賴
減少對外部數(shù)據(jù)的依賴重归,比如用戶必要的驗證信息,需要和庫的比對厦凤,我們就先緩存到我們的服務(wù)中鼻吮,比如庫存信息需要去庫里去等等。
減少請求時間快速返回
快速處理完信息返回給請求端信息较鼓,不能讓整個流程都完成了再返回椎木,那樣請求的占用時間太長了,我們需要快速的返回博烂,為我們更多處理請求香椎,和讓更多的請求進來
服務(wù)穩(wěn)定性
集群部署(應(yīng)用系統(tǒng))
集群部署服務(wù),多個服務(wù)來抗壓禽篱;
這個時候絕對不能出現(xiàn)有任何的依賴服務(wù)是單點畜伐,風險太大,一點掛掉可能會影響其他服務(wù)躺率。
備用節(jié)點預留
都做成無狀態(tài)的服務(wù)玛界,服務(wù)之間做好負載,預留增加服務(wù)器肥照;
除了必要的節(jié)點不做單點之外脚仔,由于我們的并發(fā)請求量大勤众,需要事先做好備用的節(jié)點舆绎,例如我們現(xiàn)在無狀態(tài)秒殺服務(wù),開始有10臺機器預估承受10w并發(fā)請求们颜,請求突然多了吕朵,或者我們的請求處理慢了無法及時解決,就需要提前預留個三五臺甚至更多的機器做準備窥突。
服務(wù)等級
也許我們不需要完全去準備空的機器努溃,做好服務(wù)的等級的劃分,當壓力過大時停掉級別低的服務(wù)阻问,省出來的空間帶寬等到我們當前服務(wù)梧税。
例如:雙十一當天的訂單無法取消,快遞信息也無法查看
開發(fā)維護/模塊劃分
我們要把秒殺業(yè)務(wù)當初單獨的服務(wù)來做,這樣方便后期優(yōu)化
總結(jié)
不同的服務(wù)方式在不通的情況下適用場景第队,沒有什么架構(gòu)就肯定是好的哮塞,需要在適合的場景下運用合適的架構(gòu)。
我們秒殺做成這樣凳谦,不一定就適合所有的業(yè)務(wù)場景忆畅,有的時候業(yè)務(wù)流量是穩(wěn)健的增加,我們就不一定用這種架構(gòu)
小結(jié)1
架構(gòu)設(shè)計講完之后
了解秒殺過程大致解決方法;
回憶吸收下剛剛講解的內(nèi)容尸执,看看能不能解決問題家凯。
課堂練習1
尋找問題
討論下秒殺還有那些問題點困擾著我們
尋找難點
哪些點我們知道但是很難解決;
例如12306永遠都是不好搶票如失,網(wǎng)站經(jīng)常會出現(xiàn)剩余票的錯誤信息
難點和解決方向
動靜分離
首先當搶購時間臨近的時候用戶習慣性的會不斷的去刷新頁面绊诲,因為之前老的秒殺結(jié)構(gòu)都是刷新頁面后開始搶購的,現(xiàn)在可以直接通過點擊按鈕褪贵。
解決這種問題就最簡單做法就是動靜分離驯镊;讓用戶在搶購前訪問的都是靜態(tài)數(shù)據(jù)
動態(tài)數(shù)據(jù)
每個用戶請求的內(nèi)容都不定制不一樣的,會根據(jù)用戶的信息做一系列處理竭鞍。
例如:購物車板惑,訂單查詢,個人資料等等
靜態(tài)數(shù)據(jù)
靜態(tài)數(shù)據(jù)就是每個人無論誰查看都是一樣的東西偎快;當然不僅僅是html或者圖片冯乘,有些頁面是根據(jù)后臺信息生成出來的,但是一定時間內(nèi)給用戶展示是不會修改的晒夹。
例如:商品信息裆馒,圖片,新聞等等
方案
我們要做的是把靜態(tài)數(shù)據(jù)緩存丐怯,可以用的方案有
用戶的緩存
可以把圖片等信息直接放在客戶端或瀏覽器上喷好,html也可以緩存,然后我們通過后臺或者前臺读跷,或者定時的控制更新
CDN
把靜態(tài)信息緩存到離用戶近的CDN節(jié)點用
連接緩存
直接http的連接進行緩存梗搅,由代理服務(wù)器,直接根據(jù)請求的url和參數(shù)進行判斷效览,然后返回給消息主體无切。
最常見的mysql的緩存,同意的sql短時間重復查詢結(jié)果是一樣的丐枉,我們可以再web層做類似的緩存哆键。
服務(wù)端
可能有些信息不完全是沒個用戶都一樣,但是這些信息又有一定特征瘦锹,我們就可以緩存到自己的服務(wù)器中當做靜態(tài)資源籍嘹。
例如:用戶信息闪盔,或者一些關(guān)系對照等
服務(wù)器端的我們選擇就多了,可以用mc redis 或者本地cache緩存辱士,也可以在無狀態(tài)的機器上直接使用靜態(tài)變量都可以的锭沟。
連接跳轉(zhuǎn)中間件
Nginx、Apache 等
優(yōu)化點
上邊講了一些應(yīng)用方式识补,我們來講下實際如何應(yīng)用
URL
URL統(tǒng)一成一定格式族淮,并且要有唯一性,確保url能精確的指向緩存凭涂,我們就可以通過url來生成管理緩存祝辣。
對用戶相關(guān)做分離
如果一部分用戶需要動態(tài),一部分需要靜態(tài)切油,就需要做好區(qū)分蝙斜,在訪問時直接讓他們對靜態(tài)資源做請求
例如,未登陸的用戶不需要顯示在頁面上積分等信息
主服務(wù)體/熱點
我們現(xiàn)在要做的是服務(wù)的主體澎胡,就是處理秒殺任務(wù)的服務(wù)孕荠,這個服務(wù)的當前服務(wù)的核心點。
對應(yīng)核心服務(wù)攻谁,我們需要確定那個請求訪問量大稚伍,然后在上邊做好優(yōu)化。
對秒殺來說我們的熱點數(shù)據(jù)是減庫戚宦,生成訂單个曙。
對業(yè)務(wù)的隔離
剛才我們也講到了,我們需要把這塊業(yè)務(wù)隔離出來
專注模塊
專注自己的模塊業(yè)務(wù)受楼,減少服務(wù)的其他負擔垦搬;
對需要維護緩存配置等服務(wù)和主服務(wù)分開,
例如:服務(wù)和定時任務(wù)不要放到一起艳汽。
版本迭代
首先版本迭代的時候不會受到其他功能的影響
服務(wù)部署
服務(wù)在部署時猴贰,負載時不受其他的服務(wù)影響;在部署時也要跟其他服務(wù)隔離河狐,防止內(nèi)存cpu等資源受到其他程序的影響
數(shù)據(jù)隔離
對應(yīng)服務(wù)使用到的數(shù)據(jù)信息米绕,我們要事先封裝好,放到緩存當中去甚牲,決不能直接去訪問數(shù)據(jù)庫义郑。首先訪問滿蝶柿,再者訪問數(shù)據(jù)庫又會增加一層壓力判斷丈钙,如果數(shù)據(jù)庫鏈接出現(xiàn)問題,出現(xiàn)死鎖等等
流量消峰
即便我們做了一系列的優(yōu)化方案交汤,但是對服務(wù)端來說還是會出現(xiàn)一定的峰值雏赦。我們需要對這些這個大流量做消峰劫笙,就是把流量盡量消減,把大流量平鋪處理星岗。
例如:我們的服務(wù)QPS最高是1w填大,前幾秒過來的都是兩三萬,后邊都是五六千遞減狀態(tài)俏橘。我們每秒1w QPS允华,10秒就是10w,而服務(wù)前兩秒接受到了6w寥掐,我們的服務(wù)肯定應(yīng)付不過來會死掉靴寂,但是后8秒只有3w,如果我們把流量鋪開很容易解決這個問題召耘。
一下是幾種消峰方案
隊列處理
用消息隊列做緩存區(qū)百炬,然后從緩沖區(qū)去想下一個服務(wù);其實就類似于java線程池的工作原理污它,前邊有Q存儲信息后邊有線程從Q那出信息處理
請求進來后我們可以放入隊列剖踊,然后異步的去做下一步操作;每當有程序邏輯需要兩個階段時我們都可以在中間加入隊列衫贬,但是要選擇適當?shù)臅r機德澈。
答題/驗證碼
這個環(huán)節(jié)是為了讓用戶不在同一時間點內(nèi)訪問,每個人的答題或者輸入驗證碼的時間都不是一致的固惯,這樣我們就可以流量相對鋪開圃验。
這個時候就需要添加驗證碼或題庫系統(tǒng)來支撐。
分層過濾
如果流量實在是太大缝呕,用戶訪問的數(shù)據(jù)是我們可以在請求時采取隨機接受請求的情況澳窑,可以在多個層對請求做分流或者是拒絕請求。
服務(wù)優(yōu)化
簡單帶過聊一下供常,具體的太多
傳輸協(xié)議
http,tcp;nio,netty
傳輸序列化
kryo,hession,protostuff和protobuf
編碼優(yōu)化
方法調(diào)用摊聋,對象使用,方法內(nèi)逃逸
緩存使用
redis/mc栈暇,本地cache
并發(fā)控制
分布式鎖麻裁,內(nèi)部java的多線程
jvm調(diào)優(yōu)
參數(shù)調(diào)優(yōu),回收器設(shè)置等等
減庫存問題
減庫存的時機我們需要把握好
下單減
用戶搶購后就是等于下單源祈,這個時候減庫存煎源,然后將庫存直接分配給用戶。
問題:不付款
如果用戶不付款香缺,一定時間后撤銷手销;這種請求的問題是會造成惡意搶單的,讓很多人當時搶不到图张,但是自己又不付款锋拖。
解決方法1
可以把秒殺的付款時間縮短诈悍,或者預付定金能稍微緩解下這種壓力。
解決方法2
隨機分配給請求是否搶購成功兽埃,隨機分配庫存侥钳。
付款減
用戶付款后減庫存。
問題
這個時候如果付款付不了柄错,會出現(xiàn)庫存不夠舷夺,在付款前用戶不是真正的搶到了商品。高壓力的情況下支付系統(tǒng)會很慢售貌,因為支付系統(tǒng)需要驗證太多的東西
解決方法
對支付環(huán)節(jié)做優(yōu)化冕房,當用戶提交支付后暫時鎖庫,然后我們?nèi)フ{(diào)支付系統(tǒng)趁矾,成功了返回耙册,因為鎖定了一定時間可以讓用戶再次支付。
減庫方式
減庫無論在哪個時間點都會出現(xiàn)問題毫捣,我們要吧減庫的方式做好以減少必要或者可能的鎖庫時間详拙。
用隊列,或者緩存去減庫蔓同。
實踐
根據(jù)我們上邊講述的內(nèi)容饶辙,我們搭建一套秒殺服務(wù);
框架架構(gòu)
程序邏輯
用戶請求到秒殺服務(wù)
秒殺服務(wù)去redis檢查庫存并進行redis扣減decrby
redis庫存是由庫存表全量同步過來的
有庫存存入MQ
生成訂單的服務(wù)去MQ消費
生成訂單到訂單表
去DB中做真實減庫
訂單如果沒有支付斑粱,有超時服務(wù)去訂單表檢查
如果超時反庫
反庫會同步到redis中去
靜態(tài)資源
頁面
緩存靜態(tài)頁面到CDN弃揽,我們演示直接用Nginx映射html
js
用js對頁面請求限制
限制訪問時間點,到點后才可以搶購
限制訪問頻率则北,一秒只能請求一次
重復提交矿微,請求回來前不能再次提交
秒殺開始標識,動態(tài)獲取
服務(wù)邏輯
分流/負載
前邊用Nginx做好分流限制
訪問限制
定時打開請求
通過js通知前端可以接受請求
同ip尚揣,同用戶請求處理次數(shù)限制
減庫存
通過redis對庫存做操作涌矢,成功后返回搶購成功;
寫入消息隊列
生成訂單
對訂單支付信息快骗、用戶信息娜庇、優(yōu)惠信息等做驗證
生成訂單,對DB做操作
實現(xiàn)
我們根據(jù)剛才的邏輯實現(xiàn)以下
環(huán)境
安裝Nginx
安裝activeMQ/rabbitmq
安裝Redis
代碼
前端代碼
后端代碼
Controller
令牌桶算法
RateLimiter類可以實現(xiàn)
RateLimiter rateLimiter = RateLimiter.create(10);
超賣判斷
我們在本地緩存中加入一層記錄方篮,記錄商品是否賣完了名秀,這樣超賣后我們會減少到redis的訪問流量
RateLimiter rateLimiter = RateLimiter.create(10);
初步減庫
減去redis 的庫存
long stock = redisService.decr(GoodsKey.getGoodsStock, "" + goodsId);
重復驗證
redis里記錄一層
long stock = redisService.decr(GoodsKey.getGoodsStock, "" + goodsId);
放入隊列
sender.sendSeckillMessage(message);
Q消費
效驗庫存
我們剛才效驗的redis里的庫存,這個時候需要驗證庫里的庫存
int stock = goodsVo.getStockCount();
重復判斷
插入前我們已經(jīng)判斷了一次藕溅,但是我們的一致性沒有嚴格要求匕得,為了更保險在操作庫的時候再驗證一次
//判斷重復秒殺
SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
減庫,生成訂單
真正的對數(shù)據(jù)庫做減庫蜈垮,同時刪除訂單耗跛,兩個操作要寫在一個事務(wù)里裕照。
@Transactional
public OrderInfo seckill(User user, GoodsVo goods){
/**
* 數(shù)據(jù)庫真實減庫存
/
boolean success = goodsService.reduceStock(goods);
if (success){
/*
* 生成訂單
* 里邊的兩步攒发,訂單主表和秒殺表也需要事務(wù)
*/
return orderService.createOrder(user, goods);
}else {
setGoodsOver(goods.getId());
return null;
}
}
狀態(tài)維護
最簡單我們的庫存需要去維護
庫存維護
我們就直接用spring boot 的繼承implements InitializingBean
實現(xiàn)afterPropertiesSet方法來實現(xiàn)调塌。
/**
* 系統(tǒng)初始化
* 將商品信息加載到redis和本地內(nèi)存
/
@Override
public void afterPropertiesSet() {
List<GoodsVo> goodsVoList = goodsService.listGoodsVo();
if (goodsVoList == null) {
return;
}
for (GoodsVo goods : goodsVoList) {
redisService.set(GoodsKey.getGoodsStock, "" + goods.getId(), goods.getStockCount());
/*
* 初始化商品狀態(tài)
*/localOverMap.put(goods.getId(), false);
}
}
結(jié)果獲取
用戶端通過用戶id 和商品id來查詢是否處理成功
/**
* orderId:成功
* -1:秒殺失敗
* 0: 排隊中
*
* 獲取秒殺結(jié)果
*/
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public Result<Long> seckillResult(Model model, User user,
@RequestParam("goodsId") long goodsId) {
model.addAttribute("user", user);
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
long orderId = seckillService.getSeckillResult(user.getId(), goodsId);
return Result.success(orderId);
}
演示和壓測
運行結(jié)果演示
把項目跑起來演示一下
監(jiān)控工具jprofiler
壓測jmeater
拓展點、未來計劃惠猿、行業(yè)趨勢
之前的秒殺和現(xiàn)在的秒殺設(shè)計上已經(jīng)都有很大的改觀羔砾;很少出現(xiàn)那種對單一商品的秒殺,價格上優(yōu)惠也沒有那多了偶妖。
首先這種根據(jù)時間點的秒殺對自己服務(wù)處理起來問題特別多姜凄,再者了解業(yè)務(wù)的黑客競爭對手越來越多,還有用戶的觀念目前提升了趾访。
秒殺的終極體現(xiàn)是态秧,12306 搶票。
總結(jié)
講述了什么是秒殺以及秒殺的問題解決方案扼鞋,希望大家能夠?qū)γ霘⒂懈浞值恼J識申鱼,秒殺并不難只是要一個點一個點慢慢做好就能解決問題。