秒杀系统优化方案(下)吐血整理
接上篇秒殺系統(tǒng)優(yōu)化方案(上)吐血整理
3. 深入優(yōu)化設(shè)計(jì)
3.1? ?初始方案問(wèn)題分析
在前面針對(duì)數(shù)據(jù)庫(kù)的優(yōu)化中,由于數(shù)據(jù)庫(kù)行級(jí)鎖存在競(jìng)爭(zhēng)造成大量的串行阻塞,我們使用了存儲(chǔ)過(guò)程(或者觸發(fā)器)等技術(shù)綁定操作,整個(gè)事務(wù)在MySQL端完成,把整個(gè)熱點(diǎn)執(zhí)行放在一個(gè)過(guò)程當(dāng)中一次性完成,可以屏蔽掉網(wǎng)絡(luò)延遲時(shí)間,減少行級(jí)鎖持有時(shí)間,提高事務(wù)并發(fā)訪問(wèn)速度。
可是問(wèn)題時(shí)并發(fā)的流量實(shí)際上都是直接穿透讓MYSQL自己去抗,比如說(shuō)庫(kù)存是否賣(mài)完以及用戶(hù)是否重復(fù)秒殺都完全是靠查詢(xún)數(shù)據(jù)庫(kù)去判斷,造成數(shù)據(jù)庫(kù)不必要的負(fù)擔(dān)非常大,然而這些都可以放在緩存做一個(gè)標(biāo)記在服務(wù)層進(jìn)行攔截,對(duì)于中小規(guī)模的并發(fā)還可以,但是真正的超高并發(fā),顯然這個(gè)還不完善。
3.2??? 優(yōu)化的方向和思路
方向:將請(qǐng)求盡量攔截在系統(tǒng)上游
傳統(tǒng)秒殺系統(tǒng)之所以掛,請(qǐng)求都?jí)旱沽撕蠖藬?shù)據(jù)層,數(shù)據(jù)讀寫(xiě)鎖沖突嚴(yán)重,并發(fā)高響應(yīng)慢,幾乎所有請(qǐng)求都超時(shí),流量雖大,下單成功的有效流量甚小【一趟火車(chē)其實(shí)只有2000張票,200w個(gè)人來(lái)買(mǎi),基本沒(méi)有人能買(mǎi)成功,請(qǐng)求有效率為0】?
思路:限流和削峰
限流:屏蔽掉無(wú)用的流量,允許少部分流量流向后端。
削峰:瞬時(shí)大流量峰值容易壓垮系統(tǒng),解決這個(gè)問(wèn)題是重中之重。常用的消峰方法有異步處理、緩存和消息中間件等技術(shù)。
?
異步處理:秒殺系統(tǒng)是一個(gè)高并發(fā)系統(tǒng),采用異步處理模式可以極大地提高系統(tǒng)并發(fā)量,其實(shí)異步處理就是削峰的一種實(shí)現(xiàn)方式。
緩存:秒殺系統(tǒng)本身是一個(gè)典型的讀多寫(xiě)少的應(yīng)用場(chǎng)景【一趟火車(chē)其實(shí)只有2000張票,200w個(gè)人來(lái)買(mǎi),最多2000個(gè)人下單成功,其他人都是查詢(xún)庫(kù)存,寫(xiě)比例只有0.1%,讀比例占99.9%】,非常適合使用緩存。
消息隊(duì)列:消息隊(duì)列可以削峰,將攔截大量并發(fā)請(qǐng)求,這也是一個(gè)異步處理過(guò)程,后臺(tái)業(yè)務(wù)根據(jù)自己的處理能力,從消息隊(duì)列中主動(dòng)的拉取請(qǐng)求消息進(jìn)行業(yè)務(wù)處理。
3.3???前端優(yōu)化
3.3.1? ?靜態(tài)資源緩存
1. 頁(yè)面靜態(tài)化
對(duì)商品詳情和訂單詳情進(jìn)行頁(yè)面靜態(tài)化處理,頁(yè)面是存在html,動(dòng)態(tài)數(shù)據(jù)是通過(guò)接口從服務(wù)端獲取,實(shí)現(xiàn)前后端分離,靜態(tài)頁(yè)面無(wú)需連接數(shù)據(jù)庫(kù)打開(kāi)速度較動(dòng)態(tài)頁(yè)面會(huì)有明顯提高。
2.頁(yè)面緩存
通過(guò)CDN緩存靜態(tài)資源,來(lái)抗峰值。不使用CDN的話也可以通過(guò)在手動(dòng)渲染得到的html頁(yè)面緩存到redis。
3.3.2? ?限流手段
1. 使用數(shù)學(xué)公式驗(yàn)證碼
描述:點(diǎn)擊秒殺前,先讓用戶(hù)輸入數(shù)學(xué)公式驗(yàn)證碼,驗(yàn)證正確才能進(jìn)行秒殺。
好處:
1)防止惡意的機(jī)器人和爬蟲(chóng)
2)分散用戶(hù)的請(qǐng)求
實(shí)現(xiàn):
1)前端通過(guò)把商品id作為參數(shù)調(diào)用服務(wù)端創(chuàng)建驗(yàn)證碼接口
2)服務(wù)端根據(jù)前端傳過(guò)來(lái)的商品id和用戶(hù)id生成驗(yàn)證碼,并將商品id+用戶(hù)id作為key,生成的驗(yàn)證碼作為value存入redis,同時(shí)將生成的驗(yàn)證碼輸入圖片寫(xiě)入imageIO讓前端展示。
3)將用戶(hù)輸入的驗(yàn)證碼與根據(jù)商品id+用戶(hù)id從redis查詢(xún)到的驗(yàn)證碼對(duì)比,相同就返回驗(yàn)證成功,進(jìn)入秒殺;不同或從redis查詢(xún)的驗(yàn)證碼為空都返回驗(yàn)證失敗,刷新驗(yàn)證碼重試
?
2. 禁止重復(fù)提交
用戶(hù)提交之后按鈕置灰,禁止重復(fù)提交?
3.4? ? 中間代理層
可利用負(fù)載均衡(例如反響代理Nginx等)使用多個(gè)服務(wù)器并發(fā)處理請(qǐng)求,減小服務(wù)器壓力。
3.5? ? ?后端優(yōu)化
3.5.1? ?控制層(網(wǎng)關(guān)層)
限制同一UserID訪問(wèn)頻率:盡量攔截瀏覽器請(qǐng)求,但針對(duì)某些惡意攻擊或其它插件,在服務(wù)端控制層需要針對(duì)同一個(gè)訪問(wèn)uid,限制訪問(wèn)頻率。
1.??? 利用緩存
設(shè)置緩存有效時(shí)間,在緩存中計(jì)數(shù),如果在緩存的有效時(shí)間內(nèi)請(qǐng)求的次數(shù)超了的話,就返回請(qǐng)求訪問(wèn)太頻繁。
2.??? 利用RateLimiter
RateLimiter是guava提供的基于令牌桶算法的限流實(shí)現(xiàn)類(lèi),通過(guò)調(diào)整生成token的速率來(lái)限制用戶(hù)頻繁訪問(wèn)秒殺頁(yè)面,從而達(dá)到防止超大流量沖垮系統(tǒng)。(令牌桶算法的原理是系統(tǒng)會(huì)以一個(gè)恒定的速度往桶里放入令牌,而如果請(qǐng)求需要被處理,則需要先從桶里獲取一個(gè)令牌,當(dāng)桶里沒(méi)有令牌可取時(shí),則拒絕服務(wù)。
3.5.2? ?服務(wù)層
當(dāng)用戶(hù)量非常大的時(shí)候,攔截流量后的請(qǐng)求訪問(wèn)量還是非常大,此時(shí)仍需進(jìn)一步優(yōu)化。
1.??? 業(yè)務(wù)分離:將秒殺業(yè)務(wù)系統(tǒng)和其他業(yè)務(wù)分離,單獨(dú)放在高配服務(wù)器上,可以集中資源對(duì)訪問(wèn)請(qǐng)求抗壓。——應(yīng)用的拆分
2.??? 采用消息隊(duì)列緩存請(qǐng)求:將大流量請(qǐng)求寫(xiě)到消息隊(duì)列緩存,利用服務(wù)器根據(jù)自己的處理能力主動(dòng)到消息緩存隊(duì)列中抓取任務(wù)處理請(qǐng)求,數(shù)據(jù)庫(kù)層訂閱消息減庫(kù)存,減庫(kù)存成功的請(qǐng)求返回秒殺成功,失敗的返回秒殺結(jié)束。
3.??? 利用緩存應(yīng)對(duì)讀請(qǐng)求:對(duì)于讀多寫(xiě)少業(yè)務(wù),大部分請(qǐng)求是查詢(xún)請(qǐng)求,所以可以讀寫(xiě)分離,利用緩存分擔(dān)數(shù)據(jù)庫(kù)壓力。
4.??? 利用緩存應(yīng)對(duì)寫(xiě)請(qǐng)求:緩存也是可以應(yīng)對(duì)寫(xiě)請(qǐng)求的,可把數(shù)據(jù)庫(kù)中的庫(kù)存數(shù)據(jù)轉(zhuǎn)移到Redis緩存中,所有減庫(kù)存操作都在Redis中進(jìn)行,然后再通過(guò)后臺(tái)進(jìn)程把Redis中的用戶(hù)秒殺請(qǐng)求同步到數(shù)據(jù)庫(kù)中。
可以將緩存和消息中間件 組合起來(lái),緩存系統(tǒng)負(fù)責(zé)接收記錄用戶(hù)請(qǐng)求,消息中間件負(fù)責(zé)將緩存中的請(qǐng)求同步到數(shù)據(jù)庫(kù)。
?
方案:本地標(biāo)記 + redis預(yù)處理 + RabbitMQ異步下單 + 客戶(hù)端輪詢(xún)
描述:通過(guò)三級(jí)緩沖保護(hù),1、本地標(biāo)記 2、redis預(yù)處理 3、RabbitMQ異步下單,最后才會(huì)訪問(wèn)數(shù)據(jù)庫(kù),這樣做是為了最大力度減少對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)。
實(shí)現(xiàn):
3.5.3? 數(shù)據(jù)庫(kù)層
? 數(shù)據(jù)庫(kù)層是最脆弱的一層,一般在應(yīng)用設(shè)計(jì)時(shí)在上游就需要把請(qǐng)求攔截掉,數(shù)據(jù)庫(kù)層只承擔(dān)“能力范圍內(nèi)”的訪問(wèn)請(qǐng)求。所以,上面通過(guò)在服務(wù)層引入隊(duì)列和緩存,讓最底層的數(shù)據(jù)庫(kù)高枕無(wú)憂。但依然可以進(jìn)行如下方向的優(yōu)化:
?對(duì)于秒殺系統(tǒng),直接訪問(wèn)數(shù)據(jù)庫(kù)的話,存在一個(gè)【事務(wù)競(jìng)爭(zhēng)優(yōu)化】問(wèn)題,可使用存儲(chǔ)過(guò)程(或者觸發(fā)器)等技術(shù)綁定操作,整個(gè)事務(wù)在MySQL端完成,把整個(gè)熱點(diǎn)執(zhí)行放在一個(gè)過(guò)程當(dāng)中一次性完成,可以屏蔽掉網(wǎng)絡(luò)延遲時(shí)間,減少行級(jí)鎖持有時(shí)間,提高事務(wù)并發(fā)訪問(wèn)速度。
?
3.7? 優(yōu)化秒殺流程
上面的秒殺流程對(duì)應(yīng)的流程圖如下:
步驟1到12,主體是redis預(yù)減庫(kù)存,生成消息隊(duì)列:
?
步驟13到14是處理消息隊(duì)列:
步驟15,是客戶(hù)端請(qǐng)求秒殺結(jié)果:
?
4. 問(wèn)題解析
1.????? 如何解決庫(kù)存的超賣(mài)問(wèn)題?
賣(mài)超原因:
(1)一個(gè)用戶(hù)同時(shí)發(fā)出了多個(gè)請(qǐng)求,如果庫(kù)存足夠,沒(méi)加限制,用戶(hù)就可以下多個(gè)訂單。(2)減庫(kù)存的sql上沒(méi)有加庫(kù)存數(shù)量的判斷,并發(fā)的時(shí)候也會(huì)導(dǎo)致把庫(kù)存減成負(fù)數(shù)。
解決辦法:
(1):在后端的秒殺表中,對(duì)user_id和goods_id加唯一索引,確保一個(gè)用戶(hù)對(duì)一個(gè)商品絕對(duì)不會(huì)生成兩個(gè)訂單。
(2):我們的減庫(kù)存的sql上應(yīng)該加上庫(kù)存數(shù)量的判斷
數(shù)據(jù)庫(kù)自身是有行級(jí)鎖的,每次減庫(kù)存的時(shí)候判斷count>0,它實(shí)際上是串行的執(zhí)行update的,因此絕對(duì)不會(huì)賣(mài)超!。
UPDATE seckill
??????? SET number = number-1
??????? WHERE seckill_id=#{seckillId}
??????? AND start_time <#{killTime}
??????? AND end_time >= #{killTime}
??????? AND number > 0;
?2.? ??如何解決少賣(mài)問(wèn)題—Redis預(yù)減成功而DB扣庫(kù)存失敗?
前面的方案中會(huì)出現(xiàn)一個(gè)少賣(mài)的問(wèn)題。Redis在預(yù)減庫(kù)存的時(shí)候,在初始化的時(shí)候就放置庫(kù)存的大小,redis的原子減操作保證了多少庫(kù)存就會(huì)減多少,也就會(huì)在消息隊(duì)列中放多少。
現(xiàn)在考慮兩種情況:
1)數(shù)據(jù)庫(kù)那邊出現(xiàn)非庫(kù)存原因比如網(wǎng)絡(luò)等造成減庫(kù)存失敗,而這時(shí)redis已經(jīng)減了。
2)萬(wàn)一一個(gè)用戶(hù)發(fā)出多個(gè)請(qǐng)求,而且這些請(qǐng)求恰巧比別的請(qǐng)求更早到達(dá)服務(wù)器,如果庫(kù)存足夠,redis就會(huì)減多次,redis提前進(jìn)入賣(mài)空狀態(tài),并拒絕。不過(guò)這兩種情況出現(xiàn)的概率都是非常低的。
兩種情況都會(huì)出現(xiàn)少賣(mài)的問(wèn)題,實(shí)際上也是緩存和數(shù)據(jù)庫(kù)出現(xiàn)不一致的問(wèn)題!
但是我們不是非得解決不一致的問(wèn)題,本身使用緩存就難以保證強(qiáng)一致性:
在redis中設(shè)置庫(kù)存比真實(shí)庫(kù)存多一些就行。
3.???秒殺過(guò)程中怎么保證redis緩存和數(shù)據(jù)庫(kù)的一致性?
在其他一般讀大于寫(xiě)的場(chǎng)景,一般處理的原則是:緩存只做失效,不做更新。
采用Cache-Aside pattern:
失效:應(yīng)用程序先從cache取數(shù)據(jù),沒(méi)有得到,則從數(shù)據(jù)庫(kù)中取數(shù)據(jù),成功后,放到緩存中。
更新:先把數(shù)據(jù)存到數(shù)據(jù)庫(kù)中,成功后,再讓緩存失效。
?4.??Redis中的庫(kù)存如何與DB中的庫(kù)存保持一致?
Redis中的數(shù)量不是庫(kù)存,它的作用僅僅時(shí)候只是為了阻擋多余的請(qǐng)求透?jìng)鞯絛b,起到一個(gè)保護(hù)DB的作用。因?yàn)槊霘⑸唐返臄?shù)量是有限的,比如只有10個(gè),讓1萬(wàn)個(gè)請(qǐng)求去訪問(wèn)DB是沒(méi)有意義的,因?yàn)樽疃嘀挥?0個(gè)請(qǐng)求會(huì)下單成功,剩余的9990個(gè)請(qǐng)求都是無(wú)效的,是可以不用去訪問(wèn)db而直接失敗的。
因此,這是一個(gè)偽問(wèn)題,我們是不需要保持一致的。
?5.???為什么要隱藏秒殺接口?
html是可以被右鍵->查看源代碼,如果秒殺地址寫(xiě)死在源文件中,是很容易就被惡意用戶(hù)拿到的,就可以被機(jī)器人利用來(lái)刷接口,這對(duì)于其他用戶(hù)來(lái)說(shuō)是不公平的,我們也不希望看到這種情況。所以我們可以控制讓用戶(hù)在沒(méi)有到秒殺時(shí)間的時(shí)候不能獲取到秒殺地址,只返回秒殺的開(kāi)始時(shí)間。
當(dāng)?shù)矫霘r(shí)間的時(shí)候才返回秒殺地址即seckill_id以及根據(jù)seckill_id和salt加密的MD5,前端再次拿著seckill_id和MD5才能執(zhí)行秒殺。假如用戶(hù)在秒殺開(kāi)始前猜測(cè)到秒殺地址seckill_id去請(qǐng)求秒殺,也是不會(huì)成功的,因?yàn)樗貌坏叫枰?yàn)證的MD5。這里的MD5相當(dāng)于是用戶(hù)進(jìn)行秒殺的憑證。
6.???一個(gè)秒殺系統(tǒng),500用戶(hù)同時(shí)登陸訪問(wèn)服務(wù)器A,服務(wù)器B如何快速利用登錄名(假設(shè)是電話號(hào)碼或者郵箱)做其他查詢(xún)?
主從復(fù)制,讀寫(xiě)分離
?
轉(zhuǎn)載于:https://www.cnblogs.com/xiangkejin/p/9351501.html
總結(jié)
以上是生活随笔為你收集整理的秒杀系统优化方案(下)吐血整理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 游戏设计、原型与开发:基于Unity与C
- 下一篇: 线性复杂度的素数筛选法