Java秒殺系統(tǒng)(十三):數(shù)據(jù)庫(kù)級(jí)別Sql的優(yōu)化與代碼的調(diào)整
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。
摘要:本篇博文是“Java秒殺系統(tǒng)實(shí)戰(zhàn)系列文章”的第十三篇,從本篇文章開始我們將進(jìn)入“秒殺代碼優(yōu)化”環(huán)節(jié),本文將首先從數(shù)據(jù)庫(kù)級(jí)別Sql的優(yōu)化入手,結(jié)合調(diào)整秒殺相關(guān)的部分核心代碼,實(shí)現(xiàn)初步的優(yōu)化!
內(nèi)容:上篇文章我們暴露出了“秒殺接口”在面對(duì)高并發(fā)請(qǐng)求的場(chǎng)景下所出現(xiàn)的“超賣”、“重復(fù)秒殺”等問題,并對(duì)相應(yīng)的問題進(jìn)行了分析,然后就沒有然后了……(事了拂衣去?。?/span>
問題既然落在我們的手里,那么身為一名程序猿,那是沒有理由回避的。通過分析該“秒殺接口”的核心代碼,可以發(fā)現(xiàn)在數(shù)據(jù)庫(kù)層面,其涉及的Sql我們還是可以動(dòng)一動(dòng)手腳的!其調(diào)整后的“秒殺核心業(yè)務(wù)邏輯”的完整源代碼如下所示:
//商品秒殺核心業(yè)務(wù)邏輯的處理-mysql的優(yōu)化
@Override
public Boolean killItemV2(Integer killId, Integer userId) throws Exception {
Boolean result=false;
//TODO:判斷當(dāng)前用戶是否已經(jīng)搶購(gòu)過當(dāng)前商品
if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
//A 查詢待秒殺商品詳情
ItemKill itemKill=itemKillMapper.selectByIdV2(killId);
//TODO:判斷是否可以被秒殺canKill=1?
if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){
//B 扣減庫(kù)存-減一
int res=itemKillMapper.updateKillItemV2(killId);
//TODO:扣減是否成功?是-生成秒殺成功的訂單,同時(shí)通知用戶秒殺成功的消息
if (res>0){
commonRecordKillSuccessInfo(itemKill,userId);
result=true;
}
}
}else{
throw new Exception("您已經(jīng)搶購(gòu)過該商品了!");
}
return result;
}
首先是對(duì)于 注釋A 那里的調(diào)整,即在獲取“秒殺商品詳情”時(shí),我們限定了“可秒殺商品的數(shù)量total需要大于0”,其對(duì)應(yīng)的代碼為:itemKillMapper.updateKillItemV2(killId);完整的動(dòng)態(tài)Sql如下所示:
<!--獲取秒殺詳情V2-->
<select id="selectByIdV2" resultType="com.debug.kill.model.entity.ItemKill">
SELECT
a.*,
b.name AS itemName,
(CASE WHEN (now() BETWEEN a.start_time AND a.end_time)
THEN 1
ELSE 0
END) AS canKill
FROM item_kill AS a LEFT JOIN item AS b ON b.id = a.item_id
WHERE a.is_active = 1 AND a.id =#{id} AND a.total>0
</select>
然后是
注釋B
對(duì)應(yīng)的優(yōu)化調(diào)整,即在扣減庫(kù)存時(shí),我們除了可以保證正常減1的操作之外,還需要保證扣減完之后的數(shù)量大于0,即只有在保證扣減完之后的數(shù)量大于0之下,該Sql操作后受影響的行數(shù)為1,對(duì)應(yīng)的代碼為:itemKillMapper.updateKillItemV2(killId);
其對(duì)應(yīng)的動(dòng)態(tài)Sql如下所示:
<!--搶購(gòu)商品,剩余數(shù)量減一-->
<update id="updateKillItemV2">
UPDATE item_kill
SET total = total - 1
WHERE id = #{killId} AND total>0
</update>
至此,我們?cè)诿霘⒑诵臉I(yè)務(wù)邏輯的優(yōu)化層面~數(shù)據(jù)庫(kù)級(jí)別Sql的優(yōu)化 已經(jīng)搞完了!除此之外,我們還在代碼層面進(jìn)行優(yōu)化,如下所示:
private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{
//TODO:記錄搶購(gòu)成功后生成的秒殺訂單記錄
ItemKillSuccess entity=new ItemKillSuccess();
String orderNo=String.valueOf(snowFlake.nextId());
//entity.setCode(RandomUtil.generateOrderCode()); //傳統(tǒng)時(shí)間戳+N位隨機(jī)數(shù)
entity.setCode(orderNo); //雪花算法
entity.setItemId(kill.getItemId());
entity.setKillId(kill.getId());
entity.setUserId(userId.toString());
entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue());
entity.setCreateTime(DateTime.now().toDate());
//TODO:學(xué)以致用,舉一反三 -> 仿照單例模式的雙重檢驗(yàn)鎖寫法
if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){
int res=itemKillSuccessMapper.insertSelective(entity);
if (res>0){
//TODO:進(jìn)行異步郵件消息的通知=rabbitmq+mail
rabbitSenderService.sendKillSuccessEmailMsg(orderNo);
//TODO:入死信隊(duì)列,用于 “失效” 超過指定的TTL時(shí)間時(shí)仍然未支付的訂單
rabbitSenderService.sendKillSuccessOrderExpireMsg(orderNo);
}
}
}
即我們?cè)凇坝脩裘霘⒊晒ι捎唵斡涗洝钡拇a加入了類似于“單例模式”中的“雙重檢驗(yàn)鎖”,即在生成訂單記錄,再次判斷一下“當(dāng)前用戶是否已經(jīng)真的沒有搶購(gòu)過該商品”!
在后面的篇章中,我們將開始搬上“中間件”這一利器,并結(jié)合本文所介紹Sql的優(yōu)化和調(diào)整后的代碼,徹底解決高并發(fā)壓力測(cè)試的場(chǎng)景下出現(xiàn)的“庫(kù)存超賣”、“重復(fù)秒殺”等亂七八糟的問題!
補(bǔ)充:
1、目前,這一秒殺系統(tǒng)的整體構(gòu)建與代碼實(shí)戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對(duì)應(yīng)的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點(diǎn)擊鏈接進(jìn)行試看以及學(xué)習(xí),實(shí)戰(zhàn)期間有任何問題都可以留言或者與Debug聯(lián)系、交流!
2、另外,Debug也開源了該秒殺系統(tǒng)對(duì)應(yīng)的完整的源代碼以及數(shù)據(jù)庫(kù),其地址可以來(lái)這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star?。。。?/span>
3、最后,不要忘記了關(guān)注一下Debug的技術(shù)微信公眾號(hào):