<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          秒殺系統(tǒng)是如何防止防止超賣的?

          共 5121字,需瀏覽 11分鐘

           ·

          2020-03-08 23:22


          本文公眾號(hào)來源:后端技術(shù)漫談作者:蠻三刀把刀
          秒殺系統(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)如下圖:

          ad6674c34b51cf8f600f12a1414a6ff1.webp

          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ù),開始前我清空了)

          704c527fa7f964ac3fe1e13a741812d1.webp

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

          f2606de8d5c1f353bf47e46679553be9.webp

          你猜會(huì)賣出多少個(gè)呢,先想一想。。。

          答案是:

          賣出了14個(gè),庫存減少了14個(gè),但是每個(gè)請求Spring都處理了,創(chuàng)建了1000個(gè)訂單。

          2e0488827bae48d308fbc208f14911f2.webp

          我這里該夸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è)訂單。我們沒有超賣,可喜可賀。

          eadc5209cf92c883e2b75144c519c846.webp

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

          手速快未必好,還得看運(yùn)氣呀!

          7fb7f2fdd689c1b7c30ab05b3684b220.webp

          OK,今天先到這里,之后我們繼續(xù)一步步完善這個(gè)簡易的秒殺系統(tǒng),它總有從樹苗變成大樹的那一天!

          源碼

          我會(huì)隨著文章的更新,一直同步更新項(xiàng)目代碼,歡迎關(guān)注:

          https://github.com/qqxx6661/miaosha


          戳:百萬字長文帶你學(xué)習(xí)「Java」


          如果大家想要實(shí)時(shí)關(guān)注我更新的文章以及分享的干貨的話,可以關(guān)注我的公眾號(hào)Java3y。

          • 獲取Java精美腦圖0bee630f13d558de054d31d83b0e5ed2.webp

          • ?獲取Java學(xué)習(xí)路線0bee630f13d558de054d31d83b0e5ed2.webp

          • 獲取開發(fā)常用工具0bee630f13d558de054d31d83b0e5ed2.webp

          • ?加入技術(shù)交流群0bee630f13d558de054d31d83b0e5ed2.webp

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

          db49875337a7f78c3ef1ac17d9be051c.webp

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

          瀏覽 78
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.九色 | 韩国精品在线观看 | 天天干妹子 | 黄色高清网站 | 免费电影日本黄色 |