秒殺專題-系統(tǒng)的設(shè)計(jì)(一)
觀察從客戶端請(qǐng)求訪問到服務(wù)器择诈,整個(gè)過程經(jīng)歷了 從服務(wù)器網(wǎng)關(guān)->代碼(Service層)->數(shù)據(jù)庫(kù)
根據(jù)木桶理論病曾,整個(gè)訪問的速度取決于系統(tǒng)中響應(yīng)速度最慢的地方挖腰。而訪問數(shù)據(jù)庫(kù)是內(nèi)存對(duì)磁盤進(jìn)行IO,是系統(tǒng)中效率速度最低的地方,同時(shí)數(shù)據(jù)庫(kù)所支持的QPS也是最小的物赶,所有系統(tǒng)在大并發(fā)時(shí)數(shù)據(jù)庫(kù)是最容易崩潰的橡庞。
因此所有對(duì)并發(fā)秒殺的優(yōu)化核心在于如何減少全部請(qǐng)求直接對(duì)數(shù)據(jù)庫(kù)的訪問较坛,全部思路和技術(shù)也是圍繞此展開
簡(jiǎn)單優(yōu)化思路如下:
將數(shù)據(jù)放到Redis中,也就是放到內(nèi)存中扒最,提高查詢的效率丑勤,而不去直接訪問DB,對(duì)于已經(jīng)秒殺完的產(chǎn)品可以直接全部返回吧趣。絕大部分秒殺失敗的請(qǐng)求都被Redis擋住法竞,快速做了處理。
使用MQ强挫,將Redis放進(jìn)處理程序的請(qǐng)求進(jìn)行異步處理岔霸,直接對(duì)用戶進(jìn)行返回而不等待同步處理完成。提高了對(duì)用戶的響應(yīng)速度俯渤,但并沒有減少整體的處理時(shí)間呆细,因?yàn)榈綄?shí)際處理的代碼還是同步在操作數(shù)據(jù)庫(kù)(創(chuàng)建訂單,減庫(kù)存)八匠。
前端的緩存絮爷,頁(yè)面緩存,減少對(duì)刷新頁(yè)面服務(wù)器的請(qǐng)求
總結(jié)上面的思路就得到了如下處理方式:
把所有可以緩存的東西緩存起來
用戶登錄:用戶第一次登錄是攜帶賬號(hào)梨树,密碼進(jìn)行登錄的坑夯,必須要查詢一次數(shù)據(jù)庫(kù)。第一次之后就用
token
存到用戶cookie
中進(jìn)行登錄抡四,但是這個(gè)token
如果寫到DB中那么之后的登錄即使是檢查token
登陸也要訪問DB柜蜈,這就是需要避免的。所以可以將用戶對(duì)象存儲(chǔ)起來床嫌,(使用JSON提供的功能跨释,對(duì)象可以被序列化也可以通過字節(jié)碼反序列化變成對(duì)象),那么之后用戶再登錄直接通過token
就能在redis
中找到用戶對(duì)象進(jìn)行使用秒殺商品的庫(kù)存信息:顯然每次秒殺后庫(kù)存是減少了的厌处,但不應(yīng)該立即就去
MySQL
中進(jìn)行修改鳖谈,那樣又會(huì)直接訪問DB,這些信息同樣也是緩存在redis
中阔涉。秒殺商品的訂單信息:用戶秒殺之后生成了對(duì)應(yīng)的訂單用來組織用戶再次秒殺缆娃,那么顯然成功秒殺的訂單應(yīng)該存在于
redis
中捷绒。秒殺是否已經(jīng)結(jié)束:正常來說用戶每次訪問都會(huì)先去
redis
中查詢庫(kù)存嘗試減庫(kù)存,但是考慮到redis
也是通過網(wǎng)絡(luò)提供服務(wù)贯要,所以對(duì)于秒殺是否還在進(jìn)行這種信息(不需要入庫(kù)的信息)可以直接放在本地內(nèi)存中(使用內(nèi)存標(biāo)記)其實(shí)就是使用一個(gè)數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)特定商品的秒殺狀態(tài)暖侨。
單機(jī)優(yōu)化思路
增加redis緩存,在Redis中減庫(kù)存崇渗。所有請(qǐng)求都會(huì)過redis字逗,只有成功減庫(kù)存的才會(huì)進(jìn)行MySQL。減少了 除秒殺成功之外的請(qǐng)求宅广,增加了全部請(qǐng)求對(duì)Redis的訪問葫掉。
使用內(nèi)存標(biāo)記,在庫(kù)存已經(jīng)減完的情況下不再去訪問Redis跟狱,請(qǐng)求redis也算網(wǎng)絡(luò)開銷了俭厚,內(nèi)存中就是JVM可以直接訪問到,速度最快驶臊。只有在內(nèi)存標(biāo)記被置為售完之前的請(qǐng)求挪挤,(瞬間并發(fā)沖進(jìn)來的那一部分請(qǐng)求)會(huì)訪問redis,修改完內(nèi)存標(biāo)記后关翎,剩下的請(qǐng)求不會(huì)訪問redis了扛门。
使用MQ提升用戶體驗(yàn)。首先MQ將創(chuàng)建訂單和MySQL中真實(shí)減庫(kù)存的操作去異步處理纵寝,但是這一步是沒有提升效率的尖飞,因?yàn)樵炯词共l(fā)去執(zhí)行操作MySQL,也是線程安全的(而且因?yàn)镽edis保證了進(jìn)來的線程均是秒殺成功的線程)而且是串行執(zhí)行的店雅,放到MQ中仍然是串行執(zhí)行(執(zhí)行的線程數(shù)也一樣)。但是區(qū)別在于整個(gè)串行執(zhí)行過程中贞铣,所有秒殺到商品的線程是在阻塞等待去操作MySQL(操作同一行的會(huì)阻塞闹啦,也就是減庫(kù)存),客戶端的請(qǐng)求也就阻塞了辕坝,而異步可以馬上給用戶一個(gè)反饋窍奋,并讓客戶端再進(jìn)行定時(shí)來請(qǐng)求結(jié)果(結(jié)果是存在Redis中的)。那么這樣酱畅,原本阻塞到減庫(kù)存和創(chuàng)建訂單全部成功的長(zhǎng)請(qǐng)求琳袄,被分割成了兩段,第一次請(qǐng)求可以快速響應(yīng)纺酸,第二段是連續(xù)多段的緩存訪問窖逗,阻塞DB->快速響應(yīng),查詢結(jié)果(redis)餐蔬,(DB服務(wù)被MQ去執(zhí)行了).
我們可以得到目前的系統(tǒng)鏈路圖大致如下:
可以看到碎紊,目前為止整個(gè)系統(tǒng)對(duì)于DB的沖擊已經(jīng)十分小了佑附。單機(jī)的QPS就差不多這樣了。但是這個(gè)系統(tǒng)仍然還有其他非常多值得討論的細(xì)節(jié)仗考。
但是后端還有一些需要進(jìn)行處理的問題音同,比如超賣&重復(fù)秒殺
超賣&重復(fù)秒殺
這個(gè)算是最好解決的問題了。超賣問題出現(xiàn)在程序直接去檢查MySQL
中的庫(kù)存來作為庫(kù)存是否充足的判斷標(biāo)準(zhǔn)秃嗜,但實(shí)際上一個(gè)服務(wù)線程的運(yùn)行流程是:檢查庫(kù)存
->減少庫(kù)存
权均。這中間至少包含了兩步,一定不是原子性的操作锅锨,而導(dǎo)致了多個(gè)服務(wù)線程可以減少同一份庫(kù)存叽赊。
只需要直接操作Redis
中的緩存就可以了,無論多少個(gè)線程都是被Redis
單線程執(zhí)行的橡类,每個(gè)線程的操作結(jié)果一定正確蛇尚。而之后只需要判斷操作結(jié)果是否大于等于0即可,線程就安全了顾画,代碼如下:
@RequestMapping("/seckill/{id}")
public Result SecKill(@PathVariable("id") String secId, HttpServletRequest request){
//獲取登錄用戶
User user = secKillService.getLoginUser(request);
if(user==null){
return ResultUtil.error(CodeMsgUtil.USER_NOT_LOGIN);
}
//加內(nèi)存標(biāo)記
if(proMap.containsKey(secId)&&proMap.get(secId)){
return ResultUtil.error(CodeMsgUtil.SEC_SOLD_OUT);
}
//預(yù)減庫(kù)存
Long remain = redisService.decr(secId); //redis是安全的
if(remain < 0){
proMap.put(secId, true);
//不能秒殺的歸還庫(kù)存
redisService.incr(secId);
return ResultUtil.error(CodeMsgUtil.SEC_SOLD_OUT);
}
//到這里多少并發(fā)的線程都是線程安全的了
如果一定要檢查MySQL
去看庫(kù)存數(shù)量取劫,那么在sql語(yǔ)句
中加上對(duì)庫(kù)存數(shù)量≥0的限制就可以了,這樣會(huì)有很多的線程嘗試去減庫(kù)存研侣,但只有等于秒殺商品數(shù)量的線程可以成功減庫(kù)存谱邪。因?yàn)?code>MySQL中對(duì)同一行數(shù)據(jù)的操作(同一件商品的庫(kù)存信息)是加了行鎖的,所以在這里也變成了線程安全的操作庶诡。
<update id="SecKillGoods">
update seckill_goods_list
set stock_count = stock_count - 1
where good_id = #{secId} and stock_count > 0
</update>
而對(duì)于重復(fù)秒殺而已惦银,訂單在MQ
中處理完成之后會(huì)寫入到Redis
中,用于之后用戶再次秒殺時(shí)阻止末誓。但MQ
處理訂單扯俱,到寫入Redis
中間有相當(dāng)長(zhǎng)的一段時(shí)間,可能此時(shí)用戶已經(jīng)再次進(jìn)來秒殺喇澡。所以這里存在的情況是:任務(wù)剛進(jìn)MQ
隊(duì)列迅栅,還沒有寫MySQL
也沒有寫Redis
,所以此時(shí)去Redis
和MySQL
中檢查是都無法得到訂單信息的晴玖。
這里考慮一種在訂單表中通過用戶id和商品id建立唯一索引(用戶和秒殺的商品聯(lián)系起來)的方法读存,MySQL
自身的特性會(huì)阻止第一個(gè)訂單之后的訂單寫入,那么這樣MQ
最終在創(chuàng)建重復(fù)訂單時(shí)就會(huì)失敗呕屎,重復(fù)秒殺就不可能了让簿。
當(dāng)然還可以利用Redis
在緩存中多記錄一些信息來實(shí)現(xiàn),充分利用Redis
的單線程特性秀睛。