<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>

          社區(qū)精選|秒殺系統(tǒng)常見問題—庫存超賣

          共 9375字,需瀏覽 19分鐘

           ·

          2023-11-12 14:43

          今天小編為大家?guī)淼氖巧鐓^(qū)作者 sum墨 的文章,讓我們一起來學習秒殺系統(tǒng)常見問題—庫存超賣。




          大家好!我是 sum墨 ,一個一線的底層碼農(nóng),平時喜歡研究和思考一些技術(shù)相關(guān)的問題并整理成文,限于本人水平,如果文章和代碼有表述不當之處,還請不吝賜教。

          以下是正文!


          先看問題

          首先上一串代碼

             
             
          public String buy(Long goodsId, Integer goodsNum) {
          //查詢商品庫存
          Goods goods = goodsMapper.selectById(goodsId);
          //如果當前庫存為0,提示商品已經(jīng)賣光了
          if (goods.getGoodsInventory() <= 0) {
          return "商品已經(jīng)賣光了!";
          }
          //如果當前購買數(shù)量大于庫存,提示庫存不足
          if (goodsNum > goods.getGoodsInventory()) {
          return "庫存不足!";
          }
          //更新庫存
          goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
          goodsMapper.updateById(goods);
          return "購買成功!";
          }
          我們看一下這串代碼,邏輯用流程圖表示如下:


          從圖上看,邏輯還是很清晰明了的,而且單測的話,也測試不出來什么 bug 。但是在秒殺場景下,問題可就大發(fā)了,100 件商品可能賣出 1000 單,出現(xiàn)超賣問題這下就真的需要殺個程序員祭天了。


          問題分析

          正常情況下,如果請求是一個一個接著來的話,這串代碼也不會有問題,如下圖:

          不同的時刻不同的請求,每次拿到的商品庫存都是更新過之后的,邏輯是ok的。


          那為啥會出現(xiàn)超賣問題呢?
          首先我們給這串代碼增加一個場景:商品秒殺(非秒殺場景難以復現(xiàn)超賣問題)。

          秒殺場景的特點如下:
          • 高并發(fā)處理:秒殺場景下,可能會有大量的購物者同時涌入系統(tǒng),因此需要具備高并發(fā)處理能力,保證系統(tǒng)能夠承受高并發(fā)訪問,并提供快速的響應(yīng)。
          • 快速響應(yīng):秒殺場景下,由于時間限制和競爭激烈,需要系統(tǒng)能夠快速響應(yīng)購物者的請求,否則可能會導致購買失敗,影響購物者的購物體驗。
          • 分布式系統(tǒng):秒殺場景下,單臺服務(wù)器扛不住請求高峰,分布式系統(tǒng)可以提高系統(tǒng)的容錯能力和抗壓能力,非常適合秒殺場景。
          在這種場景下,請求不可能是一個接一個這種,而是成千上萬個請求同時打過來,那么就會出現(xiàn)多個請求在同一時刻查詢庫存,如下圖:

          如果在同一時刻查詢商品庫存表,那么得到的商品庫存也肯定是相同的,判斷的邏輯也是相同的。


          舉個例子,現(xiàn)在商品的庫存是 10 件,請求 1 買 6 件,請求 2 買 5 件,由于兩次請求查詢到的庫存都是 10 ,肯定是可以賣的。
          但是真實情況是 5+6=11>10 ,明顯有問題好吧!
          這兩筆請求必然有一筆失敗才是對的!


          那么,這種問題怎么解決呢?


          問題解決

          從上面例子來看,問題好像是由于我們每次拿到的庫存都是一樣的,才導致庫存超賣問題,那是不是只要保證每次拿到的庫存都是最新的話,這個問題不就迎刃而解了嗎!

          在說方案前,先把我的測試表結(jié)構(gòu)貼出來:

             
             
          CREATE TABLE `t_goods` (
          `id` bigint NOT NULL COMMENT '物理主鍵',
          `goods_name` varchar(64) DEFAULT NULL COMMENT '商品名稱',
          `goods_pic` varchar(255) DEFAULT NULL COMMENT '商品圖片',
          `goods_desc` varchar(255) DEFAULT NULL COMMENT '商品描述信息',
          `goods_inventory` int DEFAULT NULL COMMENT '商品庫存',
          `goods_price` decimal(10,2) DEFAULT NULL COMMENT '商品價格',
          `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',
          `update_time` datetime DEFAULT NULL COMMENT '更新時間',
          PRIMARY KEY (`id`)
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

          方法一、 redis 分布式鎖

          Redisson 介紹


          官方介紹:Redisson 是一個基于 Redis的 Java 駐留內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid)。它封裝了 Redis 客戶端 API ,并提供了一個分布式鎖、分布式集合、分布式對象、分布式 Map 等常用的數(shù)據(jù)結(jié)構(gòu)和服務(wù)。Redisson 支持 Java 6 以上版本和 Redis 2.6 以上版本,并且采用編解碼器和序列化器來支持任何對象類型。Redisson 還提供了一些高級功能,比如異步 API 和響應(yīng)式流式 API 。它可以在分布式系統(tǒng)中被用來實現(xiàn)高可用性、高性能、高可擴展性的數(shù)據(jù)處理。


          Redisson 使用

          引入


             
             
          <!--使用redisson作為分布式鎖-->
          <dependency>
          <groupId>org.redisson</groupId>
          <artifactId>redisson</artifactId>
          <version>3.16.8</version>
          </dependency>
          注入對象
          RedissonConfig.java

             
             
          import org.redisson.Redisson;
          import org.redisson.api.RedissonClient;
          import org.redisson.config.Config;
          import org.springframework.context.annotation.Bean;
          import org.springframework.context.annotation.Configuration;

          @Configuration
          public class RedissonConfig {
          /**
          * 所有對Redisson的使用都是通過RedissonClient對象
          *
          * @return
          */

          @Bean(destroyMethod = "shutdown")
          public RedissonClient redissonClient() {
          // 創(chuàng)建配置 指定redis地址及節(jié)點信息
          Config config = new Config();
          config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");

          // 根據(jù)config創(chuàng)建出RedissonClient實例
          RedissonClient redissonClient = Redisson.create(config);
          return redissonClient;

          }
          }

          代碼優(yōu)化


             
             
          public String buyRedisLock(Long goodsId, Integer goodsNum) {
          RLock lock = redissonClient.getLock("goods_buy");
          try {
          //加分布式鎖
          lock.lock();
          //查詢商品庫存
          Goods goods = goodsMapper.selectById(goodsId);
          //如果當前庫存為0,提示商品已經(jīng)賣光了
          if (goods.getGoodsInventory() <= 0) {
          return "商品已經(jīng)賣光了!";
          }
          //如果當前購買數(shù)量大于庫存,提示庫存不足
          if (goodsNum > goods.getGoodsInventory()) {
          return "庫存不足!";
          }
          //更新庫存
          goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
          goodsMapper.updateById(goods);
          return "購買成功!";
          } catch (Exception e) {
          log.error("秒殺失敗");
          } finally {
          lock.unlock();
          }
          return "購買失敗";
          }
          加上 Redisson 分布式鎖之后,使得請求由異步變?yōu)橥剑屬徺I操作一個一個進行,解決了庫存超賣問題,但是會讓用戶等待的時間加長,影響了用戶體驗。

          方法二、MySQL的行鎖

          行鎖介紹


          MySQL 的行鎖是一種針對行級別數(shù)據(jù)的鎖,它可以鎖定某個表中的某一行數(shù)據(jù),以保證在鎖定期間,其他事務(wù)無法修改該行數(shù)據(jù),從而保證數(shù)據(jù)的一致性和完整性。
          特點如下:


          • MySQL 的行鎖只能在 InnoDB 存儲引擎中使用。

          • 行鎖需要有索引才能實現(xiàn),否則會自動鎖定整張表。

          • 可以通過使用“ SELECT ... FOR UPDATE”和“SELECT ... LOCK IN SHARE MODE ”語句來顯式地使用行鎖。

          總之,行鎖可以有效地保證數(shù)據(jù)的一致性和完整性,但是過多的行鎖也會導致性能問題,因此在使用行鎖時需要謹慎考慮,避免出現(xiàn)性能瓶頸。

          那么回到庫存超賣這個問題上來,我們可以在一開始查詢商品庫存的時候增加一個行鎖,實現(xiàn)非常簡單。


           


          //查詢商品庫存
          Goods goods = goodsMapper.selectById(goodsId);

          原始查詢SQL
          SELECT *
          FROM t_goods
          WHERE id = #{goodsId}

          改寫為
          SELECT *
          FROM t_goods
          WHERE id = #{goodsId} for update

          那么被查詢到的這行商品庫存信息就會被鎖住,其他請求想要讀取這行數(shù)據(jù)時就需要等待當前請求結(jié)束了,這樣就做到了每次查詢庫存都是最新的。不過同 Redisson 分布式鎖一樣,會讓用戶等待的時間加長,影響用戶體驗。

          方法三、樂觀鎖


          樂觀鎖機制類似 java 中的 cas 機制,在查詢數(shù)據(jù)的時候不加鎖,只有更新數(shù)據(jù)的時候才比對數(shù)據(jù)是否已經(jīng)發(fā)生過改變,沒有改變則執(zhí)行更新操作,已經(jīng)改變了則進行重試。


          商品表增加 version 字段并初始化數(shù)據(jù)為 0


          `version` int(11) DEFAULT NULL COMMENT '版本'

          將更新 SQL 修改如下


          update t_goods
          set goods_inventory = goods_inventory - #{goodsNum},
          version = version + 1
          where id = #{goodsId}
          and version = #{version}

          Java 代碼修改如下


             
             
          public String buyVersion(Long goodsId, Integer goodsNum) {
          //查詢商品庫存(該語句使用了行鎖)
          Goods goods = goodsMapper.selectById(goodsId);
          //如果當前庫存為0,提示商品已經(jīng)賣光了
          if (goods.getGoodsInventory() <= 0) {
          return "商品已經(jīng)賣光了!";
          }
          if (goodsMapper.updateInventoryAndVersion(goodsId, goodsNum, goods.getVersion()) > 0) {
          return "購買成功!";
          }
          return "庫存不足!";
          }

          通過增加了版本號的控制,在扣減庫存的時候在 where 條件進行版本號的比對。實現(xiàn)查詢的是哪一條記錄,那么就要求更新的是哪一條記錄,在查詢到更新的過程中版本號不能變動,否則更新失敗。


          方法四、 where 條件和 unsigned 非負字段限制


          前面的兩種辦法是通過每次都拿到最新的庫存從而解決超賣問題,那換一種思路:保證在扣除庫存的時候,庫存一定大于購買量是不是也可以解決這個問題呢?


          答案是可以的。回到上面的代碼:

           
             
             
          //更新庫存
          goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
          goodsMapper.updateById(goods);
          我們把庫存的扣減寫在了代碼中,這樣肯定是不行的,因為在分布式系統(tǒng)中我們獲取到的庫存可能都是一樣的,應(yīng)該把庫存的扣減邏輯放到 SQL 中,即:

           
             
             
          update t_goods
          set goods_inventory = goods_inventory - #{goodsNum}
          where id = #{goodsId}
          上面的 SQL 保證了每次獲取的庫存都是取數(shù)據(jù)庫的庫存,不過我們還需要加一個判斷:保證庫存大于購買量,即:

             
             
          update t_goods
          set goods_inventory = goods_inventory - #{goodsNum}
          where id = #{goodsId}
          AND (goods_inventory - #{goodsNum}) >= 0
          那么上面那段 Java 代碼也需修改一下:

             
             
          public String buySqlUpdate(Long goodsId, Integer goodsNum) {
          //查詢商品庫存(該語句使用了行鎖)
          Goods goods = goodsMapper.queryById(goodsId);
          //如果當前庫存為0,提示商品已經(jīng)賣光了
          if (goods.getGoodsInventory() <= 0) {
          return "商品已經(jīng)賣光了!";
          }
          //此處需要判斷更新操作是否成功
          if (goodsMapper.updateInventory(goodsId, goodsNum) > 0) {
          return "購買成功!";
          }
          return "庫存不足!";
          }
          還有一種辦法和 where 條件一樣,就是 unsigned 非負字段限制,把庫存字段設(shè)置為 unsigned 非負字段類型,那么在扣減時也不會出現(xiàn)扣成負數(shù)的情況。


          總結(jié)一下

          解決方案 優(yōu)點 缺點

          redis 分布式鎖

          Redis 分布式鎖可以解決分布式場景下的鎖問題,保證多個節(jié)點對同一資源的訪問順序和安全性,性能較高。

          單點故障問題,如果Redis 節(jié)點宕機,會導致鎖失效。
          MySQL 的行鎖
          可以保證事務(wù)的隔離性,能夠避免并發(fā)情況下的數(shù)據(jù)沖突問題。
          性能較低,對數(shù)據(jù)庫的性能影響較大,同時也存在死鎖問題。
          樂觀鎖
          相對于悲觀鎖,樂觀鎖不會阻塞線程,性能較高。
          需要額外的版本控制字段,且在高并發(fā)情況下容易出現(xiàn)并發(fā)沖突問題。
          where 條件和unsigned 非負字段限制
          可以通過 where 條件和unsigned 非負字段限制來保證庫存不會超賣,簡單易實現(xiàn)。
          可能存在一定的安全隱患,如果某些操作沒有正確限制,仍有可能導致庫存超賣問題。同時,如果某些場景需要對庫存進行多次更新操作,限制條件可能會導致操作失敗,需要再次查詢數(shù)據(jù),對性能會產(chǎn)生影響。


          全文至此結(jié)束,再會!




          點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,“公眾號后臺回復“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~


          - END -




          往期推薦



          社區(qū)精選|面試官:你先實現(xiàn)個 CountDown 計時器組件吧!


          社區(qū)精選|Vue 組件懶加載


          社區(qū)精選|淺析微前端沙箱


          瀏覽 201
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  特操逼| 亚洲国产激情视频 | 四虎三级 | 亚洲成人视频网 | 免费看又色又爽又黄的成人用品 |