最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

java秒殺系統(tǒng)常見問題庫存超賣解決實例分析

 更新時間:2023年11月02日 12:00:00   作者:sum墨  
這篇文章主要為大家介紹了java秒殺系統(tǒng)常見問題庫存超賣解決實例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

先看問題

首先上一串代碼

public String buy(Long goodsId, Integer goodsNum) {
    //查詢商品庫存
    Goods goods = goodsMapper.selectById(goodsId);
    //如果當前庫存為0,提示商品已經賣光了
    if (goods.getGoodsInventory() <= 0) {
        return "商品已經賣光了!";
    }
    //如果當前購買數量大于庫存,提示庫存不足
    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ā)訪問,并提供快速的響應。
  • 快速響應:秒殺場景下,由于時間限制和競爭激烈,需要系統(tǒng)能夠快速響應購物者的請求,否則可能會導致購買失敗,影響購物者的購物體驗。
  • 分布式系統(tǒng): 秒殺場景下,單臺服務器扛不住請求高峰,分布式系統(tǒng)可以提高系統(tǒng)的容錯能力和抗壓能力,非常適合秒殺場景。

在這種場景下,請求不可能是一個接一個這種,而是成千上萬個請求同時打過來,那么就會出現(xiàn)多個請求在同一時刻查詢庫存,如下圖:

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

舉個例子,現(xiàn)在商品的庫存是10件,請求1買6件,請求2買5件,由于兩次請求查詢到的庫存都是10,肯定是可以賣的。

但是真實情況是5+6=11>10,明顯有問題好吧!這兩筆請求必然有一筆失敗才是對的!

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

問題解決

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

在說方案前,先把我的測試表結構貼出來:

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駐留內存數據網格(In-Memory Data Grid)。它封裝了Redis客戶端API,并提供了一個分布式鎖、分布式集合、分布式對象、分布式Map等常用的數據結構和服務。Redisson支持Java 6以上版本和Redis 2.6以上版本,并且采用編解碼器和序列化器來支持任何對象類型。 Redisson還提供了一些高級功能,比如異步API和響應式流式API。它可以在分布式系統(tǒng)中被用來實現(xiàn)高可用性、高性能、高可擴展性的數據處理。

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");
        // 根據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,提示商品已經賣光了
        if (goods.getGoodsInventory() <= 0) {
                return "商品已經賣光了!";
        }
        //如果當前購買數量大于庫存,提示庫存不足
        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)橥?,讓購買操作一個一個進行,解決了庫存超賣問題,但是會讓用戶等待的時間加長,影響了用戶體驗。

方法二、MySQL的行鎖

行鎖介紹

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

  • MySQL的行鎖只能在InnoDB存儲引擎中使用。
  • 行鎖需要有索引才能實現(xiàn),否則會自動鎖定整張表。
  • 可以通過使用“SELECT ... FOR UPDATE”和“SELECT ... LOCK IN SHARE MODE”語句來顯式地使用行鎖。

總之,行鎖可以有效地保證數據的一致性和完整性,但是過多的行鎖也會導致性能問題,因此在使用行鎖時需要謹慎考慮,避免出現(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

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

方法三、樂觀鎖

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

商品表增加version字段并初始化數據為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,提示商品已經賣光了
    if (goods.getGoodsInventory() &lt;= 0) {
        return "商品已經賣光了!";
    }
    if (goodsMapper.updateInventoryAndVersion(goodsId, goodsNum, goods.getVersion()) &gt; 0) {
      return "購買成功!";
    }
    return "庫存不足!";
}

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

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

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

//更新庫存
goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
goodsMapper.updateById(goods);

我們把庫存的扣減寫在了代碼中,這樣肯定是不行的,因為在分布式系統(tǒng)中我們獲取到的庫存可能都是一樣的,應該把庫存的扣減邏輯放到SQL中,即:

update t_goods
 set goods_inventory = goods_inventory - #{goodsNum}
 where id = #{goodsId}

上面的SQL保證了每次獲取的庫存都是取數據庫的庫存,不過我們還需要加一個判斷:保證庫存大于購買量,即:

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,提示商品已經賣光了
    if (goods.getGoodsInventory() <= 0) {
        return "商品已經賣光了!";
    }
    //此處需要判斷更新操作是否成功
    if (goodsMapper.updateInventory(goodsId, goodsNum) > 0) {
        return "購買成功!";
     }
    return "庫存不足!";
}

還有一種辦法和where條件一樣,就是unsigned 非負字段限制,把庫存字段設置為unsigned 非負字段類型,那么在扣減時也不會出現(xiàn)扣成負數的情況。

總結

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

方案有很多,用法結合實際業(yè)務來看,沒有最優(yōu),只有更優(yōu)。

以上就是java秒殺系統(tǒng)常見問題庫存超賣的詳細內容,更多關于java秒殺系統(tǒng)庫存超賣的資料請關注腳本之家其它相關文章!

相關文章

  • MyBatis-Plus UpdateWrapper 使用常見陷阱和解決方案

    MyBatis-Plus UpdateWrapper 使用常見陷阱和解決方案

    MyBatis-Plus是Mybatis的一個增強,簡化了Mybatis的開發(fā)過程,不僅保持了Mybatis原有的功能,而且在無代碼侵略下增加了許多的增強的功能,提供了豐富的CRUD操作,單表的CRUD操作無需編寫SQL語句,本文介紹的是UpdateWrapper的常見陷阱和對應的解決方案,感興趣的朋友一起看看吧
    2024-08-08
  • SpringBoot項目從18.18M瘦身到0.18M的實現(xiàn)

    SpringBoot項目從18.18M瘦身到0.18M的實現(xiàn)

    本文主要介紹了SpringBoot項目從18.18M瘦身到0.18M的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-01-01
  • 實例解析Java單例模式編程中對抽象工廠模式的運用

    實例解析Java單例模式編程中對抽象工廠模式的運用

    這篇文章主要介紹了實例解析Java單例模式編程中對抽象工廠模式的運用,抽象工廠模式可以看作是工廠方法模式的升級版,本需要的朋友可以參考下
    2016-02-02
  • Java中Synchronized鎖的使用和原理詳解

    Java中Synchronized鎖的使用和原理詳解

    這篇文章主要介紹了Java中Synchronized鎖的使用和原理詳解,synchronized是?Java?內置的關鍵字,它提供了一種獨占的加鎖方式,synchronized的獲取和釋放鎖由JVM實現(xiàn),用戶不需要顯示的釋放鎖,非常方便,需要的朋友可以參考下
    2023-07-07
  • 基于spring注入為null的原因及解決方案

    基于spring注入為null的原因及解決方案

    這篇文章主要介紹了spring注入為null的原因及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • java高效實現(xiàn)大文件拷貝功能

    java高效實現(xiàn)大文件拷貝功能

    這篇文章主要為大家詳細介紹了java高效實現(xiàn)大文件拷貝功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-05-05
  • Springboot工具類ReflectionUtils使用教程

    Springboot工具類ReflectionUtils使用教程

    這篇文章主要介紹了Springboot內置的工具類之ReflectionUtils的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧
    2022-12-12
  • Java中的異步非阻塞AIO模型詳解

    Java中的異步非阻塞AIO模型詳解

    這篇文章主要介紹了Java中的異步非阻塞AIO模型詳解,AIO需要操作系統(tǒng)的支持,在linux內核2.6版本中加入了對真正異步IO的支持,java從jdk1.7開始支持AIO,本文提供了部分實現(xiàn)代碼,需要的朋友可以參考下
    2023-09-09
  • Java基于遞歸解決全排列問題算法示例

    Java基于遞歸解決全排列問題算法示例

    這篇文章主要介紹了Java基于遞歸解決全排列問題算法,結合實例形式分析了Java使用遞歸算法解決全排列問題的原理與具體實現(xiàn)技巧,需要的朋友可以參考下
    2017-11-11
  • 淺談Servlet開發(fā)技術基礎

    淺談Servlet開發(fā)技術基礎

    這篇文章主要介紹了淺談Servlet開發(fā)技術基礎,具有一定借鑒價值,需要的朋友可以參考下。
    2017-12-12

最新評論

内乡县| 额敏县| 深水埗区| 纳雍县| 和龙市| 仁化县| 彝良县| 西贡区| 河西区| 灯塔市| 陈巴尔虎旗| 涿鹿县| 阜阳市| 浙江省| 濮阳县| 应城市| 宁阳县| 平江县| 莱芜市| 当涂县| 河北省| 洛扎县| 镇坪县| 新蔡县| 金坛市| 镇沅| 南木林县| 横山县| 永德县| 庆元县| 黔南| 射洪县| 星子县| 融水| 文昌市| 安丘市| 鄂温| 浦县| 鹤岗市| 阳江市| 澜沧|