教你从0到1搭建秒杀系统-防超卖
各位讀者好,最近筆者學(xué)了很多東西,其實(shí)都想跟大家進(jìn)行分享,奈何需要將所學(xué)習(xí)的知識(shí)整理出來(lái)需要耗費(fèi)大量的時(shí)間,包括總結(jié),或各種圖形以及寫(xiě)代碼示例,所以可能更新的速度會(huì)比較慢。但大家放心,只要有時(shí)間我就會(huì)將自己學(xué)習(xí)的內(nèi)容總結(jié)出來(lái)供大家一起學(xué)習(xí)討論,有總結(jié)的不對(duì)的地方大家隨時(shí)都可以批評(píng)指正,畢竟我說(shuō)的也不全都是對(duì)的,希望大家耐心等待。如果你喜歡讀者的內(nèi)容,可以點(diǎn)個(gè)關(guān)注時(shí)刻了解我的動(dòng)態(tài),同時(shí)也歡迎各位小伙伴轉(zhuǎn)發(fā)和分享。
前期概述
最近想就秒殺系統(tǒng)做一個(gè)梳理和總結(jié),從0到1搭建一個(gè)簡(jiǎn)易的秒殺系統(tǒng),說(shuō)實(shí)在的,也就經(jīng)常會(huì)聽(tīng)到秒殺系統(tǒng),筆者還沒(méi)有真正的自己去搭建過(guò)一個(gè)秒殺系統(tǒng),相信很多人跟我一樣。所以為了不只是停在聽(tīng)說(shuō)階段,我們說(shuō)干就干,自己搭建一個(gè)建議的秒殺系統(tǒng)。我會(huì)在里面講解那些需要注意的地方以及涉及秒殺系統(tǒng)我們需要注意的問(wèn)題,幫助大家快速了解秒殺系統(tǒng)的難點(diǎn)。同時(shí)后期也會(huì)將書(shū)寫(xiě)的代碼給出來(lái)鏈接,供大家下載,代碼我會(huì)盡量精簡(jiǎn)干練,供大家學(xué)習(xí)參考,并且依據(jù)參考可以迅速上手實(shí)際的項(xiàng)目,后期想要增加更多的功能也可以直接在項(xiàng)目上進(jìn)行更改即可。
我會(huì)分幾個(gè)階段進(jìn)行講解,盡量做到通俗易懂,以下是我的規(guī)劃內(nèi)容(大家看的時(shí)候注意順序):
- 教你從0到1搭建秒殺系統(tǒng)-防超賣(mài).
- 教你從0到1搭建秒殺系統(tǒng)-限流.
- 教你從0到1搭建秒殺系統(tǒng)-搶購(gòu)接口隱藏 與單用戶限制頻率.
- 教你從0到1搭建秒殺系統(tǒng)-緩存與數(shù)據(jù)庫(kù)雙寫(xiě)一致.
- 教你從0到1搭建秒殺系統(tǒng)-Canal快速入門(mén)(番外篇).
- 教你從0到1搭建秒殺系統(tǒng)-訂單異步處理.
秒殺系統(tǒng)簡(jiǎn)介
本文要講的就是第一個(gè)問(wèn)題:防超賣(mài)。在進(jìn)行正式問(wèn)題開(kāi)始分析之前,我們還是啰嗦的簡(jiǎn)單介紹一下什么是秒殺系統(tǒng)。相信其實(shí)有很多人都知道秒殺,像淘寶,京東等等這些里面的商戶一到節(jié)日,像什么雙11,雙12都會(huì)整一些活動(dòng),免不了有秒殺的活動(dòng),在特定的時(shí)間范圍進(jìn)行搶購(gòu),平時(shí)需要幾百的東西可能這會(huì)只要幾十塊錢(qián)。其實(shí)秒殺系統(tǒng)真的有好多,網(wǎng)上對(duì)他更專業(yè)的定義也有很多,我在這里就只是簡(jiǎn)單介紹一下,其他的我就不贅述,畢竟這不是
我們要講解的重點(diǎn)。我們可以將秒殺系統(tǒng)歸屬為一些場(chǎng)景:
- 電商搶購(gòu)限量的商品
- 12306春節(jié)搶票
- …
這些每一個(gè)功能都可以叫做秒殺系統(tǒng)。作為秒殺系統(tǒng),用戶在進(jìn)行操作了以后,大家有沒(méi)有想過(guò)最終是怎么秒殺成功或者秒殺失敗的呢?可能有的人會(huì)有這樣的疑惑,在這里不管你是想要設(shè)計(jì)秒殺系統(tǒng)的人還是想要搞清楚你作為買(mǎi)家最終是怎么成功秒殺或者失敗的,相信你看了這幾篇文章以后都可以說(shuō)出個(gè)所以然來(lái)。這整體的處理邏輯可以抽象成以下幾個(gè)步驟:
- 用戶選定商品下單
- 校驗(yàn)庫(kù)存
- 扣庫(kù)存
- 創(chuàng)建用戶訂單
- 用戶支付等
- 后續(xù)步驟…
我說(shuō)的只是一種秒殺系統(tǒng)的設(shè)計(jì)方案,其實(shí)對(duì)于不同的場(chǎng)景,有的公司設(shè)計(jì)的秒殺系統(tǒng)可能步驟有所不同,有的可能最終才會(huì)扣庫(kù)存。其實(shí)不管怎么操作,這些基本的步驟都是需要的,在這里我就以我上面寫(xiě)出的步驟邏輯進(jìn)行講解。
也許你看到這里有一個(gè)疑惑,這整體就是個(gè)用戶買(mǎi)商品的流程而已啊,為啥要說(shuō)它是個(gè)專門(mén)的系統(tǒng)呢?如果你的項(xiàng)目流量非常小,完全不用擔(dān)心有并發(fā)的購(gòu)買(mǎi)請(qǐng)求,那么做這樣一個(gè)系統(tǒng)的確意義不大。但如果你的系統(tǒng)要像12306那樣,接受高并發(fā)訪問(wèn)和下單的考驗(yàn),那么你就需要一套完整的流程保護(hù)措施,來(lái)保證你系統(tǒng)在用戶流量高峰期不會(huì)被搞掛了。
防超賣(mài)
好了,廢話不多說(shuō),我們接下來(lái)從防止超賣(mài)開(kāi)始直接搭建項(xiàng)目進(jìn)行實(shí)際操作。這里只是搭建簡(jiǎn)易的秒殺系統(tǒng),我們采用最傳統(tǒng)的Spring MVC+Mybaits的結(jié)構(gòu)。
建立數(shù)據(jù)庫(kù)表結(jié)構(gòu)
說(shuō)了是簡(jiǎn)易的秒殺系統(tǒng),所以我們先來(lái)張最最最簡(jiǎn)易的結(jié)構(gòu)表,等未來(lái)我們需要解決更多的系統(tǒng)問(wèn)題,再擴(kuò)展表結(jié)構(gòu)。我們定義兩張表,一張庫(kù)存表stock,一張訂單表stock_order,相關(guān)SQL如下:
-- ---------------------------- -- 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 '庫(kù)存',`sale` int(11) NOT NULL COMMENT '已售',`version` int(11) NOT NULL COMMENT '樂(lè)觀鎖,版本號(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 '庫(kù)存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;項(xiàng)目結(jié)構(gòu)
怎么創(chuàng)建一個(gè)項(xiàng)目我這里就不一步步帶大家去創(chuàng)建了,這里給大家展示一下項(xiàng)目的整體結(jié)構(gòu)圖:
典型的MCV模式。
項(xiàng)目代碼
Controller層代碼,提供一個(gè)HTTP接口,參數(shù)為商品的Id:
@RequestMapping("/createWrongOrder/{sid}")@ResponseBodypublic String createWrongOrder(@PathVariable int 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)庫(kù)存Stock stock = checkStock(sid);//扣庫(kù)存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("庫(kù)存不足");}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; }這里提供了三個(gè)方法:校驗(yàn)庫(kù)存,扣庫(kù)存和創(chuàng)建訂單。
接口測(cè)試
我們通過(guò)JMeter(https://jmeter.apache.org/) 這個(gè)并發(fā)請(qǐng)求工具來(lái)模擬大量用戶同時(shí)請(qǐng)求購(gòu)買(mǎi)接口的場(chǎng)景。我們?cè)诒砝锾砑右粋€(gè)Iphone,庫(kù)存100。在JMeter里啟動(dòng)1000個(gè)線程,無(wú)延遲同時(shí)訪問(wèn)接口,模擬1000個(gè)人,搶購(gòu)100個(gè)產(chǎn)品的場(chǎng)景。點(diǎn)擊啟動(dòng):
最后執(zhí)行完以后,我們查詢數(shù)據(jù)如下:
賣(mài)出了56個(gè),庫(kù)存減少了56個(gè),但是每個(gè)請(qǐng)求都處理了,創(chuàng)建了1000個(gè)訂單。這顯然是不對(duì)的。我們需要的是賣(mài)出100個(gè),不多也不少才能保證我們的收益。由此看來(lái)之前的設(shè)計(jì)是有問(wèn)題的。既然沒(méi)有賣(mài)出去那么多就不應(yīng)該創(chuàng)建那么多的訂單。
為了解決上面的超賣(mài)問(wèn)題,我們可以在Service層給更新表添加一個(gè)事務(wù),這樣每個(gè)線程更新請(qǐng)求的時(shí)候都會(huì)先去鎖表的這一行(悲觀鎖),更新完庫(kù)存后再釋放鎖。可這樣就太慢了。我們需要樂(lè)觀鎖,個(gè)最簡(jiǎn)單的辦法就是,給每個(gè)商品庫(kù)存一個(gè)版本號(hào)version字段。我們Controller層修改代碼如下:
/*** 樂(lè)觀鎖更新庫(kù)存* @param sid* @return*/ @RequestMapping("/createOptimisticOrder/{sid}") @ResponseBody public String createOptimisticOrder(@PathVariable int sid) {int id;try {id = orderService.createOptimisticOrder(sid);LOGGER.info("購(gòu)買(mǎi)成功,剩余庫(kù)存為: [{}]", id);} catch (Exception e) {LOGGER.error("購(gòu)買(mǎi)失敗:[{}]", e.getMessage());return "購(gòu)買(mǎi)失敗,庫(kù)存不足";}return String.format("購(gòu)買(mǎi)成功,剩余庫(kù)存為:%d", id); }Service層代碼更新如下:
@Override public int createOptimisticOrder(int sid) throws Exception {//校驗(yàn)庫(kù)存Stock stock = checkStock(sid);//樂(lè)觀鎖更新庫(kù)存saleStockOptimistic(stock);//創(chuàng)建訂單int id = createOrder(stock);return stock.getCount() - (stock.getSale()+1); }private void saleStockOptimistic(Stock stock) {LOGGER.info("查詢數(shù)據(jù)庫(kù),嘗試更新庫(kù)存");int count = stockService.updateStockByOptimistic(stock);if (count == 0){throw new RuntimeException("并發(fā)更新庫(kù)存失敗,version不匹配") ;} }Mapper中updateByOptimistic方法的代碼如下:
<update id="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}</update>我們?cè)趯?shí)際減庫(kù)存的SQL操作中,首先判斷version是否是我們查詢庫(kù)存時(shí)候的version,如果是,扣減庫(kù)存,成功搶購(gòu)。如果發(fā)現(xiàn)version變了,則不更新數(shù)據(jù)庫(kù),返回?fù)屬?gòu)失敗。
修改之后,我們跟之前一樣再重新發(fā)起請(qǐng)求(首先清空之前的數(shù)據(jù),將庫(kù)存格式化重新設(shè)置為100,賣(mài)出為0):
可以看到,最終賣(mài)出去了45個(gè),version更新為了45,同時(shí)創(chuàng)建了45個(gè)訂單,此時(shí)沒(méi)有超賣(mài),不會(huì)創(chuàng)建多余的訂單了。
由于并發(fā)訪問(wèn)的原因,很多線程更新庫(kù)存失敗了,所以在我們這種設(shè)計(jì)下,1000個(gè)人真要是同時(shí)發(fā)起購(gòu)買(mǎi),只有39個(gè)幸運(yùn)兒能夠買(mǎi)到東西,我們雖然防止了超賣(mài)。但是實(shí)際上需要賣(mài)出100個(gè)商品,那剩余的沒(méi)有賣(mài)出去的話就會(huì)造成利益的減少,這樣也是不可以的。那么怎么保證在不超賣(mài)的同時(shí)還可以賣(mài)出給定數(shù)量的商品呢?我們?cè)谙乱黄恼轮懈蠹抑v解。
猜你感興趣:
教你從0到1搭建秒殺系統(tǒng)-防超賣(mài)
教你從0到1搭建秒殺系統(tǒng)-限流
教你從0到1搭建秒殺系統(tǒng)-搶購(gòu)接口隱藏與單用戶限制頻率
教你從0到1搭建秒殺系統(tǒng)-緩存與數(shù)據(jù)庫(kù)雙寫(xiě)一致
教你從0到1搭建秒殺系統(tǒng)-Canal快速入門(mén)(番外篇)
教你從0到1搭建秒殺系統(tǒng)-訂單異步處理
更多文章請(qǐng)點(diǎn)擊:更多…
參考文章:
https://cloud.tencent.com/developer/article/148805
https://juejin.im/post/5dd09f5af265da0be72aacbd
https://crossoverjie.top/%2F2018%2F05%2F07%2Fssm%2FSSM18-seconds-kill%2F
總結(jié)
以上是生活随笔為你收集整理的教你从0到1搭建秒杀系统-防超卖的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: MYSQL专题-使用Binlog日志恢复
- 下一篇: 教你从0到1搭建秒杀系统-限流