秒殺系統(tǒng)是如何防止防止超賣的?
秒殺系統(tǒng)
秒殺系統(tǒng)介紹
秒殺系統(tǒng)相信網(wǎng)上已經(jīng)介紹了很多了,我也不想黏貼很多定義過來了。
廢話少說,秒殺系統(tǒng)主要應(yīng)用在商品搶購的場景,比如:
電商搶購限量商品
賣周董演唱會(huì)的門票
火車票搶座
…
秒殺系統(tǒng)抽象來說就是以下幾個(gè)步驟:
用戶選定商品下單
校驗(yàn)庫存
扣庫存
創(chuàng)建用戶訂單
用戶支付等后續(xù)步驟…
聽起來就是個(gè)用戶買商品的流程而已嘛,確實(shí),所以我們?yōu)樯兑f他是個(gè)專門的系統(tǒng)呢。。
為什么要做所謂的“系統(tǒng)”
如果你的項(xiàng)目流量非常小,完全不用擔(dān)心有并發(fā)的購買請求,那么做這樣一個(gè)系統(tǒng)意義不大。
但如果你的系統(tǒng)要像12306那樣,接受高并發(fā)訪問和下單的考驗(yàn),那么你就需要一套完整的流程保護(hù)措施,來保證你系統(tǒng)在用戶流量高峰期不會(huì)被搞掛了。(就像12306剛開始網(wǎng)絡(luò)售票那幾年一樣)
這些措施有什么呢:
嚴(yán)格防止超賣:庫存100件你賣了120件,等著辭職吧
防止黑產(chǎn):防止不懷好意的人群通過各種技術(shù)手段把你本該下發(fā)給群眾的利益全收入了囊中。
保證用戶體驗(yàn):高并發(fā)下,別網(wǎng)頁打不開了,支付不成功了,購物車進(jìn)不去了,地址改不了了。這個(gè)問題非常之大,涉及到各種技術(shù),也不是一下子就能講完的,甚至根本就沒法講完。
我們先從“防止超賣”開始吧
畢竟,你網(wǎng)頁可以卡住,最多是大家沒參與到活動(dòng),上網(wǎng)口吐芬芳,罵你一波。但是你要是賣多了,本該拿到商品的用戶可就不樂意了,輕則投訴你,重則找漏洞起訴賠償。讓你吃不了兜著走。
不能再說下去了,我這篇文章可是打著實(shí)戰(zhàn)文章的名頭,為什么我老是要講廢話啊啊啊啊啊啊。
上代碼。
說好的做“簡易”的秒殺系統(tǒng),所以我們只用最簡單的SpringBoot項(xiàng)目
建立“簡易”的數(shù)據(jù)庫表結(jié)構(gòu)
一開始我們先來張最最最簡易的結(jié)構(gòu)表,參考了crossoverjie的秒殺系統(tǒng)文章。
等未來我們需要解決更多的系統(tǒng)問題,再擴(kuò)展表結(jié)構(gòu)。
一張庫存表stock,一張訂單表stock_order
--?----------------------------
--?Table?structure?for?stock
--?----------------------------
DROP?TABLE?IF?EXISTS?`stock`;
CREATE?TABLE?`stock`?(
??`id`?int(11)?unsigned?NOT?NULL?AUTO_INCREMENT,
??`name`?varchar(50)?NOT?NULL?DEFAULT?''?COMMENT?'名稱',
??`count`?int(11)?NOT?NULL?COMMENT?'庫存',
??`sale`?int(11)?NOT?NULL?COMMENT?'已售',
??`version`?int(11)?NOT?NULL?COMMENT?'樂觀鎖,版本號(hào)',
??PRIMARY?KEY?(`id`)
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8;
--?----------------------------
--?Table?structure?for?stock_order
--?----------------------------
DROP?TABLE?IF?EXISTS?`stock_order`;
CREATE?TABLE?`stock_order`?(
??`id`?int(11)?unsigned?NOT?NULL?AUTO_INCREMENT,
??`sid`?int(11)?NOT?NULL?COMMENT?'庫存ID',
??`name`?varchar(30)?NOT?NULL?DEFAULT?''?COMMENT?'商品名稱',
??`create_time`?timestamp?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP?COMMENT?'創(chuàng)建時(shí)間',
??PRIMARY?KEY?(`id`)
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8;
通過HTTP接口發(fā)起一次購買請求
代碼中我們采用最傳統(tǒng)的Spring MVC+Mybaits的結(jié)構(gòu)
結(jié)構(gòu)如下圖:

Controller層代碼
提供一個(gè)HTTP接口: 參數(shù)為商品的Id
@RequestMapping("/createWrongOrder/{sid}")
@ResponseBody
public?String?createWrongOrder(@PathVariable?int?sid)?{
????LOGGER.info("購買物品編號(hào)sid=[{}]",?sid);
????int?id?=?0;
????try?{
????????id?=?orderService.createWrongOrder(sid);
????????LOGGER.info("創(chuàng)建訂單id:?[{}]",?id);
????}?catch?(Exception?e)?{
????????LOGGER.error("Exception",?e);
????}
????return?String.valueOf(id);
}
Service層代碼
@Override
public?int?createWrongOrder(int?sid)?throws?Exception?{
????//校驗(yàn)庫存
????Stock?stock?=?checkStock(sid);
????//扣庫存
????saleStock(stock);
????//創(chuàng)建訂單
????int?id?=?createOrder(stock);
????return?id;
}
private?Stock?checkStock(int?sid)?{
????Stock?stock?=?stockService.getStockById(sid);
????if?(stock.getSale().equals(stock.getCount()))?{
????????throw?new?RuntimeException("庫存不足");
????}
????return?stock;
}
private?int?saleStock(Stock?stock)?{
????stock.setSale(stock.getSale()?+?1);
????return?stockService.updateStockById(stock);
}
private?int?createOrder(Stock?stock)?{
????StockOrder?order?=?new?StockOrder();
????order.setSid(stock.getId());
????order.setName(stock.getName());
????int?id?=?orderMapper.insertSelective(order);
????return?id;
}
發(fā)起并發(fā)購買請求
我們通過JMeter(https://jmeter.apache.org/) 這個(gè)并發(fā)請求工具來模擬大量用戶同時(shí)請求購買接口的場景。
注意:POSTMAN并不支持并發(fā)請求,其請求是順序的,而JMeter是多線程請求。希望以后PostMan能夠支持吧,畢竟JMeter還在倔強(qiáng)的用Java UI框架。畢竟是親兒子呢。
如何通過JMeter進(jìn)行壓力測試,請參考下文,講的非常入門但詳細(xì),包教包會(huì):
https://www.cnblogs.com/stulzq/p/8971531.html
我們在表里添加一個(gè)Iphone,庫存100。(請忽略訂單表里的數(shù)據(jù),開始前我清空了)

在JMeter里啟動(dòng)1000個(gè)線程,無延遲同時(shí)訪問接口。模擬1000個(gè)人,搶購100個(gè)產(chǎn)品的場景。點(diǎn)擊啟動(dòng):

你猜會(huì)賣出多少個(gè)呢,先想一想。。。
答案是:
賣出了14個(gè),庫存減少了14個(gè),但是每個(gè)請求Spring都處理了,創(chuàng)建了1000個(gè)訂單。

我這里該夸Spring強(qiáng)大的并發(fā)處理能力,還是該罵MySQL已經(jīng)是個(gè)成熟的數(shù)據(jù)庫,卻都不會(huì)自己鎖庫存?
避免超賣問題:更新商品庫存的版本號(hào)
為了解決上面的超賣問題,我們當(dāng)然可以在Service層給更新表添加一個(gè)事務(wù),這樣每個(gè)線程更新請求的時(shí)候都會(huì)先去鎖表的這一行(悲觀鎖),更新完庫存后再釋放鎖??蛇@樣就太慢了,1000個(gè)線程可等不及。
我們需要樂觀鎖。
一個(gè)最簡單的辦法就是,給每個(gè)商品庫存一個(gè)版本號(hào)version字段
我們修改代碼:
Controller層
/**
?*?樂觀鎖更新庫存
?*?@param?sid
?*?@return
?*/
@RequestMapping("/createOptimisticOrder/{sid}")
@ResponseBody
public?String?createOptimisticOrder(@PathVariable?int?sid)?{
????int?id;
????try?{
????????id?=?orderService.createOptimisticOrder(sid);
????????LOGGER.info("購買成功,剩余庫存為:?[{}]",?id);
????}?catch?(Exception?e)?{
????????LOGGER.error("購買失?。篬{}]",?e.getMessage());
????????return?"購買失敗,庫存不足";
????}
????return?String.format("購買成功,剩余庫存為:%d",?id);
}
Service層
@Override
public?int?createOptimisticOrder(int?sid)?throws?Exception?{
????//校驗(yàn)庫存
????Stock?stock?=?checkStock(sid);
????//樂觀鎖更新庫存
????saleStockOptimistic(stock);
????//創(chuàng)建訂單
????int?id?=?createOrder(stock);
????return?stock.getCount()?-?(stock.getSale()+1);
}
private?void?saleStockOptimistic(Stock?stock)?{
????LOGGER.info("查詢數(shù)據(jù)庫,嘗試更新庫存");
????int?count?=?stockService.updateStockByOptimistic(stock);
????if?(count?==?0){
????????throw?new?RuntimeException("并發(fā)更新庫存失敗,version不匹配")?;
????}
}
Mapper
"updateByOptimistic"?parameterType="cn.monitor4all.miaoshadao.dao.Stock">
????update?stock
????<set>
??????sale?=?sale?+?1,
??????version?=?version?+?1,
????set>
????WHERE?id?=?#{id,jdbcType=INTEGER}
????AND?version?=?#{version,jdbcType=INTEGER}
??
我們在實(shí)際減庫存的SQL操作中,首先判斷version是否是我們查詢庫存時(shí)候的version,如果是,扣減庫存,成功搶購。如果發(fā)現(xiàn)version變了,則不更新數(shù)據(jù)庫,返回?fù)屬徥 ?/p>
發(fā)起并發(fā)購買請求
這次,我們能成功嗎?
再次打開JMeter,把庫存恢復(fù)為100,清空訂單表,發(fā)起1000次請求。
這次的結(jié)果是:
賣出去了39個(gè),version更新為了39,同時(shí)創(chuàng)建了39個(gè)訂單。我們沒有超賣,可喜可賀。

由于并發(fā)訪問的原因,很多線程更新庫存失敗了,所以在我們這種設(shè)計(jì)下,1000個(gè)人真要是同時(shí)發(fā)起購買,只有39個(gè)幸運(yùn)兒能夠買到東西,但是我們防止了超賣。
手速快未必好,還得看運(yùn)氣呀!

OK,今天先到這里,之后我們繼續(xù)一步步完善這個(gè)簡易的秒殺系統(tǒng),它總有從樹苗變成大樹的那一天!
源碼
我會(huì)隨著文章的更新,一直同步更新項(xiàng)目代碼,歡迎關(guān)注:
https://github.com/qqxx6661/miaosha
如果大家想要實(shí)時(shí)關(guān)注我更新的文章以及分享的干貨的話,可以關(guān)注我的公眾號(hào)Java3y。
獲取Java精美腦圖

?獲取Java學(xué)習(xí)路線

獲取開發(fā)常用工具

?加入技術(shù)交流群

在公眾號(hào)下回復(fù)「888」即可獲取??!

點(diǎn)個(gè)在看
,分享到朋友圈
,對我真的很重要?。?/strong>
