欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

java秒殺系統(tǒng)常見(jiàn)問(wèn)題庫(kù)存超賣(mài)解決實(shí)例分析

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

先看問(wèn)題

首先上一串代碼

public String buy(Long goodsId, Integer goodsNum) {
    //查詢商品庫(kù)存
    Goods goods = goodsMapper.selectById(goodsId);
    //如果當(dāng)前庫(kù)存為0,提示商品已經(jīng)賣(mài)光了
    if (goods.getGoodsInventory() <= 0) {
        return "商品已經(jīng)賣(mài)光了!";
    }
    //如果當(dāng)前購(gòu)買(mǎi)數(shù)量大于庫(kù)存,提示庫(kù)存不足
    if (goodsNum > goods.getGoodsInventory()) {
        return "庫(kù)存不足!";
    }
    //更新庫(kù)存
    goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
    goodsMapper.updateById(goods);
    return "購(gòu)買(mǎi)成功!";
}

我們看一下這串代碼,邏輯用流程圖表示如下:


從圖上看,邏輯還是很清晰明了的,而且單測(cè)的話,也測(cè)試不出來(lái)什么bug。但是在秒殺場(chǎng)景下,問(wèn)題可就大發(fā)了,100件商品可能賣(mài)出1000單,出現(xiàn)超賣(mài)問(wèn)題,這下就真的需要?dú)€(gè)程序員祭天了。

問(wèn)題分析

正常情況下,如果請(qǐng)求是一個(gè)一個(gè)接著來(lái)的話,這串代碼也不會(huì)有問(wèn)題,如下圖:

不同的時(shí)刻不同的請(qǐng)求,每次拿到的商品庫(kù)存都是更新過(guò)之后的,邏輯是ok的。

那為啥會(huì)出現(xiàn)超賣(mài)問(wèn)題呢?首先我們給這串代碼增加一個(gè)場(chǎng)景:商品秒殺(非秒殺場(chǎng)景難以復(fù)現(xiàn)超賣(mài)問(wèn)題)。
秒殺場(chǎng)景的特點(diǎn)如下:

  • 高并發(fā)處理:秒殺場(chǎng)景下,可能會(huì)有大量的購(gòu)物者同時(shí)涌入系統(tǒng),因此需要具備高并發(fā)處理能力,保證系統(tǒng)能夠承受高并發(fā)訪問(wèn),并提供快速的響應(yīng)。
  • 快速響應(yīng):秒殺場(chǎng)景下,由于時(shí)間限制和競(jìng)爭(zhēng)激烈,需要系統(tǒng)能夠快速響應(yīng)購(gòu)物者的請(qǐng)求,否則可能會(huì)導(dǎo)致購(gòu)買(mǎi)失敗,影響購(gòu)物者的購(gòu)物體驗(yàn)。
  • 分布式系統(tǒng): 秒殺場(chǎng)景下,單臺(tái)服務(wù)器扛不住請(qǐng)求高峰,分布式系統(tǒng)可以提高系統(tǒng)的容錯(cuò)能力和抗壓能力,非常適合秒殺場(chǎng)景。

在這種場(chǎng)景下,請(qǐng)求不可能是一個(gè)接一個(gè)這種,而是成千上萬(wàn)個(gè)請(qǐng)求同時(shí)打過(guò)來(lái),那么就會(huì)出現(xiàn)多個(gè)請(qǐng)求在同一時(shí)刻查詢庫(kù)存,如下圖:

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

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

但是真實(shí)情況是5+6=11>10,明顯有問(wèn)題好吧!這兩筆請(qǐng)求必然有一筆失敗才是對(duì)的!

那么,這種問(wèn)題怎么解決呢?

問(wèn)題解決

從上面例子來(lái)看,問(wèn)題好像是由于我們每次拿到的庫(kù)存都是一樣的,才導(dǎo)致庫(kù)存超賣(mài)問(wèn)題,那是不是只要保證每次拿到的庫(kù)存都是最新的話,這個(gè)問(wèn)題不就迎刃而解了嗎!

在說(shuō)方案前,先把我的測(cè)試表結(jié)構(gòu)貼出來(lái):

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 '商品庫(kù)存',
  `goods_price` decimal(10,2) DEFAULT NULL COMMENT '商品價(jià)格',
  `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
  `update_time` datetime DEFAULT NULL COMMENT '更新時(shí)間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

方法一、redis分布式鎖

Redisson介紹

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

Redisson使用 引入

<!--使用redisson作為分布式鎖-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.8</version>
</dependency>

注入對(duì)象

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 {
    /**
     * 所有對(duì)Redisson的使用都是通過(guò)RedissonClient對(duì)象
     *
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        // 創(chuàng)建配置 指定redis地址及節(jié)點(diǎn)信息
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");
        // 根據(jù)config創(chuàng)建出RedissonClient實(shí)例
        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();
        //查詢商品庫(kù)存
        Goods goods = goodsMapper.selectById(goodsId);
        //如果當(dāng)前庫(kù)存為0,提示商品已經(jīng)賣(mài)光了
        if (goods.getGoodsInventory() <= 0) {
                return "商品已經(jīng)賣(mài)光了!";
        }
        //如果當(dāng)前購(gòu)買(mǎi)數(shù)量大于庫(kù)存,提示庫(kù)存不足
        if (goodsNum > goods.getGoodsInventory()) {
                return "庫(kù)存不足!";
        }
        //更新庫(kù)存
        goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
        goodsMapper.updateById(goods);
        return "購(gòu)買(mǎi)成功!";
    } catch (Exception e) {
        log.error("秒殺失敗");
    } finally {
        lock.unlock();
    }
    return "購(gòu)買(mǎi)失敗";
}

加上Redisson分布式鎖之后,使得請(qǐng)求由異步變?yōu)橥?,讓?gòu)買(mǎi)操作一個(gè)一個(gè)進(jìn)行,解決了庫(kù)存超賣(mài)問(wèn)題,但是會(huì)讓用戶等待的時(shí)間加長(zhǎng),影響了用戶體驗(yàn)。

方法二、MySQL的行鎖

行鎖介紹

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

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

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

那么回到庫(kù)存超賣(mài)這個(gè)問(wèn)題上來(lái),我們可以在一開(kāi)始查詢商品庫(kù)存的時(shí)候增加一個(gè)行鎖,實(shí)現(xiàn)非常簡(jiǎn)單,也就是將

//查詢商品庫(kù)存
Goods goods = goodsMapper.selectById(goodsId);
原始查詢SQL
SELECT *
  FROM t_goods
  WHERE id = #{goodsId}
改寫(xiě)為
 SELECT *
  FROM t_goods
  WHERE id = #{goodsId} for update

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

方法三、樂(lè)觀鎖

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

商品表增加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) {
    //查詢商品庫(kù)存(該語(yǔ)句使用了行鎖)
    Goods goods = goodsMapper.selectById(goodsId);
    //如果當(dāng)前庫(kù)存為0,提示商品已經(jīng)賣(mài)光了
    if (goods.getGoodsInventory() &lt;= 0) {
        return "商品已經(jīng)賣(mài)光了!";
    }
    if (goodsMapper.updateInventoryAndVersion(goodsId, goodsNum, goods.getVersion()) &gt; 0) {
      return "購(gòu)買(mǎi)成功!";
    }
    return "庫(kù)存不足!";
}

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

方法四、where條件和unsigned 非負(fù)字段限制

前面的兩種辦法是通過(guò)每次都拿到最新的庫(kù)存從而解決超賣(mài)問(wèn)題,那換一種思路:保證在扣除庫(kù)存的時(shí)候,庫(kù)存一定大于購(gòu)買(mǎi)量是不是也可以解決這個(gè)問(wèn)題呢?
答案是可以的?;氐缴厦娴拇a:

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

我們把庫(kù)存的扣減寫(xiě)在了代碼中,這樣肯定是不行的,因?yàn)樵诜植际较到y(tǒng)中我們獲取到的庫(kù)存可能都是一樣的,應(yīng)該把庫(kù)存的扣減邏輯放到SQL中,即:

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

上面的SQL保證了每次獲取的庫(kù)存都是取數(shù)據(jù)庫(kù)的庫(kù)存,不過(guò)我們還需要加一個(gè)判斷:保證庫(kù)存大于購(gòu)買(mǎi)量,即:

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) {
    //查詢商品庫(kù)存(該語(yǔ)句使用了行鎖)
    Goods goods = goodsMapper.queryById(goodsId);
    //如果當(dāng)前庫(kù)存為0,提示商品已經(jīng)賣(mài)光了
    if (goods.getGoodsInventory() <= 0) {
        return "商品已經(jīng)賣(mài)光了!";
    }
    //此處需要判斷更新操作是否成功
    if (goodsMapper.updateInventory(goodsId, goodsNum) > 0) {
        return "購(gòu)買(mǎi)成功!";
     }
    return "庫(kù)存不足!";
}

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

總結(jié)

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

方案有很多,用法結(jié)合實(shí)際業(yè)務(wù)來(lái)看,沒(méi)有最優(yōu),只有更優(yōu)。

以上就是java秒殺系統(tǒng)常見(jiàn)問(wèn)題庫(kù)存超賣(mài)的詳細(xì)內(nèi)容,更多關(guān)于java秒殺系統(tǒng)庫(kù)存超賣(mài)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • java創(chuàng)建txt文件并寫(xiě)入內(nèi)容的方法代碼示例

    java創(chuàng)建txt文件并寫(xiě)入內(nèi)容的方法代碼示例

    這篇文章主要介紹了java創(chuàng)建txt文件并寫(xiě)入內(nèi)容的兩種方法,分別是使用java.io.FileWriter和BufferedWriter,以及使用Java7的java.nio.file包中的Files和Path類,需要的朋友可以參考下
    2025-01-01
  • Java基礎(chǔ)之static關(guān)鍵字的使用講解

    Java基礎(chǔ)之static關(guān)鍵字的使用講解

    這篇文章主要介紹了Java基礎(chǔ)之static關(guān)鍵字的使用講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07
  • Java如何基于IO流實(shí)現(xiàn)同一文件讀寫(xiě)操作

    Java如何基于IO流實(shí)現(xiàn)同一文件讀寫(xiě)操作

    這篇文章主要介紹了Java如何基于IO流實(shí)現(xiàn)文件讀寫(xiě)操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-10-10
  • 簡(jiǎn)單談?wù)凧ava中的棧和堆

    簡(jiǎn)單談?wù)凧ava中的棧和堆

    堆和棧都是Java用來(lái)在RAM中存放數(shù)據(jù)的地方,下面這篇文章主要給大家介紹了關(guān)于Java中棧和堆的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2021-11-11
  • 淺談java 數(shù)據(jù)處理(int[][]存儲(chǔ)與讀取)

    淺談java 數(shù)據(jù)處理(int[][]存儲(chǔ)與讀取)

    下面小編就為大家?guī)?lái)一篇淺談java 數(shù)據(jù)處理(int[][]存儲(chǔ)與讀取)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-06-06
  • 自定義注解實(shí)現(xiàn)Spring容器注入Bean方式(類似于mybatis的@MapperScans)

    自定義注解實(shí)現(xiàn)Spring容器注入Bean方式(類似于mybatis的@MapperScans)

    本文介紹了如何通過(guò)自定義注解@MyService和@MyServiceScans在SpringBoot項(xiàng)目中自動(dòng)將指定包下的類注入Spring容器,詳細(xì)解釋了創(chuàng)建自定義注解、定義包掃描器ClassPathBeanDefinitionScanner的作用與實(shí)現(xiàn)
    2024-09-09
  • 在Spring中使用JDBC和JDBC模板的講解

    在Spring中使用JDBC和JDBC模板的講解

    今天小編就為大家分享一篇關(guān)于在Spring中使用JDBC和JDBC模板的講解,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧
    2019-01-01
  • java字符串反轉(zhuǎn)示例分享

    java字符串反轉(zhuǎn)示例分享

    這篇文章主要介紹了將一個(gè)字符串進(jìn)行反轉(zhuǎn)或者字符串中指定部分進(jìn)行反轉(zhuǎn)的方法,大家參考使用吧
    2014-01-01
  • Java設(shè)計(jì)模式之Template?Pattern模板模式詳解

    Java設(shè)計(jì)模式之Template?Pattern模板模式詳解

    這篇文章主要介紹了Java設(shè)計(jì)模式之Template?Pattern模板模式詳解,模板模式(Template?Pattern)行為型模式之一,抽象父類定義一個(gè)操作中的算法的骨架,而將一些步驟延遲到子類中,需要的朋友可以參考下
    2023-10-10
  • idea一招搞定同步所有配置(導(dǎo)入或?qū)С鏊信渲?

    idea一招搞定同步所有配置(導(dǎo)入或?qū)С鏊信渲?

    使用intellij idea很長(zhǎng)一段時(shí)間,軟件相關(guān)的配置也都按照自己習(xí)慣的設(shè)置好,如果需要重裝軟件,還得需要重新設(shè)置,本文就詳細(xì)的介紹了idea 同步所有配置,感興趣的可以了解一下
    2021-07-07

最新評(píng)論