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

Redis高并發(fā)場景防止庫存數(shù)量超賣少賣

 更新時(shí)間:2024年09月14日 10:54:08   作者:yiridancan  
商品超賣是銷售數(shù)量超過實(shí)際庫存的情況,常因庫存管理不當(dāng)引發(fā),傳統(tǒng)庫存管理在高并發(fā)環(huán)境下易出錯(cuò),可通過線程加鎖或使用Redis同步庫存狀態(tài)解決,本文就來詳細(xì)的介紹一下,感興趣的可以了解一下

簡介

商品超賣現(xiàn)象,即銷售數(shù)量超過了實(shí)際庫存量,通常是由于未能正確判斷庫存狀況而發(fā)生的。在常規(guī)的庫存管理系統(tǒng)中,我們會(huì)在扣減庫存之前進(jìn)行庫存充足性檢驗(yàn):僅當(dāng)庫存數(shù)量大于零時(shí),系統(tǒng)才會(huì)執(zhí)行扣減動(dòng)作;若庫存不足,則即時(shí)返回錯(cuò)誤提示。然而,在高并發(fā)的銷售場景下,傳統(tǒng)的處理方法往往難以確保庫存扣減的準(zhǔn)確性。為了解決這一問題,我們可以采用線程加鎖機(jī)制或利用Redis等內(nèi)存數(shù)據(jù)結(jié)構(gòu)來同步庫存狀態(tài),從而保證即使在大量同時(shí)交易的情況下,庫存扣減也能保持準(zhǔn)確無誤。

數(shù)據(jù)庫校驗(yàn)

商品類

/**
 * @description 商品類
 * @author yiridancan
 * @date 2024/3/23 9:06
 */
public class Goods {

    private int id;

    /**
     * 商品名稱
     */
    private String name;

    /**
     * 庫存數(shù)量
     */
    private int inventoryCount;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getInventoryCount() {
        return inventoryCount;
    }

    public void setInventoryCount(int inventoryCount) {
        this.inventoryCount = inventoryCount;
    }
}

實(shí)現(xiàn)類

import com.yiridancan.reduceInventory.entity.Goods;
import com.yiridancan.reduceInventory.mapper.GoodsMapper;
import com.yiridancan.reduceInventory.service.IGoodsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
 *  商品實(shí)現(xiàn)類
 * @author yiridancan
 * @date 2024/3/23 18:35
 */
@Slf4j
@Service
public class GoodsServiceImpl implements IGoodsService {

    @Autowired
    private GoodsMapper goodsMapper;
    /**
     * 扣減庫存
     * @param goodsId 商品id
     * @author yiridancan
     * @date 2024/3/23 18:33
     */
    @Override
    public void reduceInventory(int goodsId) {
        //1.根據(jù)商品id獲取商品庫存數(shù)量
        Goods goods = goodsMapper.findGoodsInventory(goodsId);
        if(Objects.isNull(goods)){
            log.error("未獲取到商品信息");
            return;
        }
        //2.如果庫存數(shù)量大于0則扣減庫存,如果等于0代表沒有貨物打印錯(cuò)誤信息
        if(goods.getInventoryCount() > 0 ){
            //默認(rèn)扣減庫存1
            goods.setInventoryCount(goods.getInventoryCount()-1);
            goodsMapper.updateGoodsInventory(goods);
            log.info("{}扣減庫存成功,扣減后庫存為:{}",goods.getName(),goods.getInventoryCount());
        }else {
            log.error("{}庫存為0",goods.getName());
        }
    }
}

首先,我們需要根據(jù)商品ID獲取商品數(shù)據(jù)。如果無法獲取到數(shù)據(jù),則打印異常并終止執(zhí)行。接著,通過查詢庫存數(shù)量進(jìn)行校驗(yàn)判斷:若庫存大于0,則扣減庫存;反之,若庫存為0,則打印異常信息。

數(shù)據(jù)庫

測試代碼

    @Test
	void contextLoads() {
		//商品id
		int goodsId = 1;
		//創(chuàng)建固定數(shù)量的線程池
		int num = 20;
		ExecutorService executorService = Executors.newFixedThreadPool(num);
		//模擬20個(gè)并發(fā)同時(shí)請(qǐng)求接口
		for (int i = 0; i < num; i++) {
			executorService.submit(() -> {
					goodsService.reduceInventory(goodsId);
			});
		}
		executorService.shutdown();
		try {
			executorService.awaitTermination(1, TimeUnit.MINUTES);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		//獲取商品最終庫存數(shù)量
		Goods goodsInventory = goodsMapper.findGoodsInventory(goodsId);
		if(Objects.isNull(goodsInventory)){
			return;
		}
		log.info("{}商品最終庫存為:{}",goodsInventory.getName(),goodsInventory.getInventoryCount());
	}

運(yùn)行結(jié)果

測試中,系統(tǒng)面臨了20個(gè)同時(shí)發(fā)出的請(qǐng)求,而可用庫存量僅為10個(gè)。理論上,這意味著應(yīng)當(dāng)有10個(gè)請(qǐng)求能夠成功完成庫存扣減,而另外10個(gè)請(qǐng)求則需被妥善拒絕。為解決此并發(fā)操作導(dǎo)致的數(shù)據(jù)不一致性問題,我們可以通過引入鎖機(jī)制來確保數(shù)據(jù)訪問的同步性,從而保障系統(tǒng)的正確性和穩(wěn)定性。

悲觀鎖

可以通過synchronized、ReentrantLock等悲觀鎖來保證原子性和一致性

我們發(fā)現(xiàn),在20次并發(fā)請(qǐng)求的測試場景中,僅有10次能夠成功減少庫存量,而另外10次則遭到拒絕。這種機(jī)制確保了數(shù)據(jù)一致性的嚴(yán)密守護(hù)。然而,若我們選擇采用悲觀鎖的策略,雖然可以強(qiáng)化數(shù)據(jù)完整性,但卻可能導(dǎo)致大量請(qǐng)求進(jìn)入阻塞隊(duì)列,尤其是在高并發(fā)的環(huán)境下,這種重量級(jí)的同步處理可能會(huì)對(duì)服務(wù)性能和數(shù)據(jù)庫響應(yīng)能力造成顯著負(fù)擔(dān),甚至有可能引發(fā)系統(tǒng)瓶頸。因此,在設(shè)計(jì)高并發(fā)系統(tǒng)時(shí),我們需要權(quán)衡鎖機(jī)制的選擇,以優(yōu)化系統(tǒng)性能,保證服務(wù)的高效流暢。

樂觀鎖

樂觀鎖采用了一種比較寬松的并發(fā)控制策略。它允許多個(gè)線程同時(shí)讀取和修改共享數(shù)據(jù),但在數(shù)據(jù)提交時(shí)會(huì)檢查是否有其他線程在此期間修改過相同的數(shù)據(jù)。如果檢測到?jīng)_突,通常需要重新嘗試操作,直到成功為止。樂觀鎖的核心在于它認(rèn)為沖突不太可能發(fā)生,或者沖突發(fā)生的概率較低,因此不一開始就對(duì)數(shù)據(jù)加鎖,從而避免了鎖機(jī)制可能帶來的性能開銷。一般通過數(shù)據(jù)庫版本號(hào)或者時(shí)間戳來進(jìn)行實(shí)現(xiàn)

定義一個(gè)抽象接口:

    /**
     * 通過樂觀鎖實(shí)現(xiàn)扣減庫存
     * @author yiridancan
     * @date 2024/3/25 22:33
     * @param goodsId 商品id
     */
    void casReduceInventory(int goodsId);

實(shí)現(xiàn)類:

    /**
     * 通過樂觀鎖實(shí)現(xiàn)扣減庫存
     * @param goodsId 商品id
     * @author yiridancan
     * @date 2024/3/25 22:33
     */
    @Override
    public void casReduceInventory(int goodsId) {
        int retryCount = 0;
        //重試次數(shù)設(shè)置為3,避免無休止的重試占用紫鳶
        while (retryCount <=3){
            //1.根據(jù)商品id獲取商品信息
            Goods goods = goodsMapper.findGoodsInventory(goodsId);
            if(Objects.isNull(goods) || goods.getInventoryCount() == 0){
                log.error("未獲取到商品信息或庫存數(shù)量不足");
                return;
            }
            //默認(rèn)扣減庫存1
            goods.setInventoryCount(goods.getInventoryCount()-1);
            int updateRow = goodsMapper.updateGoodsInventoryByCAS(goods);
            //如果修改條數(shù)大于0代表扣減庫存成功
            if(updateRow > 0 ){
                log.info("{}扣減庫存成功,扣減后庫存為:{}",goods.getName(),goods.getInventoryCount());
                return;
            }
            retryCount++;
            log.error("{}商品被修改過,進(jìn)行重試!!版本號(hào):{}",goods.getName(),goods.getDataVersion());
        }
    }

首先會(huì)先定義一個(gè)重試次數(shù),避免一直重試占用資源。然后獲取到具體的商品信息,默認(rèn)扣減庫存為1(實(shí)際可以根據(jù)用戶設(shè)置的數(shù)量進(jìn)行扣減),然后根據(jù)查詢出來的版本號(hào)和id去數(shù)據(jù)庫中更新數(shù)據(jù),如果返回更新數(shù)量代表扣減庫存成功,則打印相關(guān)打印進(jìn)行結(jié)束,否則進(jìn)行重試,直到庫存數(shù)量不足或扣減庫存成功才結(jié)束

<update id="updateGoodsInventoryByCAS">
        update goods set inventory_count=#{inventoryCount},data_version=data_version+1 where id=#{id} and data_version=#{dataVersion}
</update>

Redis

借助Redis單線程的特性,再加上lua腳本執(zhí)行過程原子性的保障。我們可以在Redis中通過lua腳本進(jìn)行庫存扣減操作

因?yàn)閘ua腳本在執(zhí)行過程中,可以避免被打斷,并且redis執(zhí)行的過程也是單線程的,所以在腳本中進(jìn)行判斷,再扣減,這個(gè)過程是可以避免并發(fā)的。所以也就可以實(shí)現(xiàn)前面我們說的原子性+有序性了。

并且Redis是一個(gè)高性能的分布式緩存,使用Lua腳本扣減庫存的方案也非常的高效

首先將商品庫存初始化到Redis中,然后后續(xù)對(duì)Redis進(jìn)行庫存扣減

local key = KEYS[1] -- 商品的鍵名
local amount = tonumber(ARGV[1]) -- 扣減的數(shù)量

-- 獲取商品當(dāng)前的庫存量
local stock = tonumber(redis.call('get', key))

-- 如果庫存足夠,則減少庫存并返回新的庫存量
if stock >= amount then
    redis.call('decrby', key, amount)
    return redis.call('get', key)
else
    return "INSUFFICIENT STOCK"
end

編寫Lua腳本,通常是單獨(dú)放在一個(gè)文件中。這里偷了一個(gè)懶直接聲明成字符串了

/**
     * 通過Redis扣減庫存
     *
     * @param goodsId 商品id
     * @author yiridancan
     * @date 2024/3/27 15:48
     */
    @Override
    public void redisReduceInventory(int goodsId) {
        String prefix = "goodsInventory:";
        //將商品數(shù)據(jù)緩存到Redis中,key是商品id,value是商品庫存數(shù)量
        goodsMapper.findGoodsAll().forEach(goods -> {
            stringRedisTemplate.opsForValue().set(prefix+goods.getId(),String.valueOf(goods.getInventoryCount()));
        });

        //lua腳本,一般放在文件中
        String script = "local key = KEYS[1] -- 商品的鍵名\n" +
                "local amount = tonumber(ARGV[1]) -- 扣減的數(shù)量\n" +
                "\n" +
                "-- 獲取商品當(dāng)前的庫存量\n" +
                "local stock = tonumber(redis.call('get', key))\n" +
                "\n" +
                "-- 如果庫存足夠,則減少庫存并返回新的庫存量\n" +
                "if stock >= amount then\n" +
                "    redis.call('decrby', key, amount)\n" +
                "    return redis.call('get', key)\n" +
                "else\n" +
                "    return \"INSUFFICIENT STOCK\"\n" +
                "end\n";

        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);

        // 創(chuàng)建一個(gè)包含庫存key的列表
        List<String> keys = Collections.singletonList(prefix + goodsId);
        // 創(chuàng)建一個(gè)包含扣減數(shù)量的參數(shù)列表
        List<String> args = Collections.singletonList(Integer.toString(1));

        // 執(zhí)行Lua腳本,傳入鍵列表和參數(shù)列表
        String result = stringRedisTemplate.execute(redisScript, keys, args.toArray(new String[0]));
        //如果不是庫存不足代表扣減成功
        if(!result.equals("INSUFFICIENT STOCK")){
            log.info("扣減庫存成功,庫存數(shù)量:{}",result);
        }else {
            log.error("庫存數(shù)量不足");
        }
    }

首先把商品數(shù)據(jù)統(tǒng)一緩存到Redis中,然后編寫一段Lua腳本交給DefaultRedisScript,DefaultRedisScript可以自定義數(shù)據(jù)返回類型

創(chuàng)建兩個(gè)集合,分別存放key和參數(shù),通過StringRedisTemplate.execute執(zhí)行Lua腳本,如果返回的值是INSUFFICIENT STOCK代表庫存不足,打印錯(cuò)誤日志,否則扣減庫存成功

最后在任務(wù)執(zhí)行完成后定時(shí)將Redis中的庫存同步到數(shù)據(jù)庫中做持久化即可

其他方案

  • Redis+MQ+數(shù)據(jù)庫:利用Redis來扛高并發(fā)流量。先在Redis扣減庫存,然后發(fā)送一個(gè)MQ消息,消費(fèi)者在接收到消息后做數(shù)據(jù)庫庫存的真正扣減和業(yè)務(wù)邏輯
  • 把修改轉(zhuǎn)換成新增,直接插入一次占用記錄,然后異步統(tǒng)計(jì)剩余庫存,或者通過SQL統(tǒng)計(jì)流水方式計(jì)算剩余庫存

  • 通過Redisson進(jìn)行加鎖處理

  • ..............

總結(jié)

綜合來說,實(shí)踐中往往會(huì)根據(jù)業(yè)務(wù)需求和現(xiàn)有技術(shù)棧選擇合適的方法,Redis因其高性能和原子操作特性,在很多場景下成為首選方案之一。而具體實(shí)施時(shí),可能還需要結(jié)合多種手段以及負(fù)載均衡、熔斷、降級(jí)等策略來應(yīng)對(duì)復(fù)雜的高并發(fā)挑戰(zhàn)。

到此這篇關(guān)于Redis高并發(fā)場景防止庫存數(shù)量超賣少賣的文章就介紹到這了,更多相關(guān)Redis防止超賣少賣內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • windows下java環(huán)境變量的設(shè)置方法

    windows下java環(huán)境變量的設(shè)置方法

    在“系統(tǒng)變量”中,設(shè)置3項(xiàng)屬性,JAVA_HOME,PATH,CLASSPATH(大小寫無所謂),若已存在則點(diǎn)擊“編輯”,不存在則點(diǎn)擊“新建”
    2013-09-09
  • Netty客戶端接入流程N(yùn)ioSocketChannel創(chuàng)建解析

    Netty客戶端接入流程N(yùn)ioSocketChannel創(chuàng)建解析

    這篇文章主要為大家介紹了Netty客戶端接入流程N(yùn)ioSocketChannel創(chuàng)建源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-03-03
  • Java中json格式化BigDecimal保留2位小數(shù)

    Java中json格式化BigDecimal保留2位小數(shù)

    這篇文章主要給大家介紹了關(guān)于Java中json格式化BigDecimal保留2位小數(shù)的相關(guān)資料,BigDecimal是Java中的一個(gè)數(shù)學(xué)庫,可以實(shí)現(xiàn)高精度計(jì)算,文中給出了詳細(xì)的代碼實(shí)例,需要的朋友可以參考下
    2023-09-09
  • SpringCloud超詳細(xì)講解Feign聲明式服務(wù)調(diào)用

    SpringCloud超詳細(xì)講解Feign聲明式服務(wù)調(diào)用

    Feign可以把Rest的請(qǐng)求進(jìn)行隱藏,偽裝成類似Spring?MVC的Controller一樣。不用再自己拼接url,拼接參數(shù)等等操作,一切都交給Feign去做
    2022-06-06
  • SpringBoot自動(dòng)配置原理,你真的懂嗎?(簡單易懂)

    SpringBoot自動(dòng)配置原理,你真的懂嗎?(簡單易懂)

    這篇文章主要介紹了SpringBoot自動(dòng)配置原理,你真的懂嗎?本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-05-05
  • Java中while語句的簡單知識(shí)及應(yīng)用

    Java中while語句的簡單知識(shí)及應(yīng)用

    這篇文章主要給大家介紹了關(guān)于Java中while語句的簡單知識(shí)及應(yīng)用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-01-01
  • java基礎(chǔ)之Integer與int類型輸出示例解析

    java基礎(chǔ)之Integer與int類型輸出示例解析

    這篇文章主要為大家介紹了java基礎(chǔ)之Integer與int類型輸出示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • 如何解決報(bào)錯(cuò):java.net.BindException:無法指定被請(qǐng)求的地址問題

    如何解決報(bào)錯(cuò):java.net.BindException:無法指定被請(qǐng)求的地址問題

    在Linux虛擬機(jī)上安裝并啟動(dòng)Tomcat時(shí)遇到啟動(dòng)失敗的問題,通過檢查端口及配置文件未發(fā)現(xiàn)異常,后發(fā)現(xiàn)/etc/hosts文件中缺少localhost的映射,添加后重啟Tomcat成功,Tomcat啟動(dòng)時(shí)會(huì)檢查localhost的IP映射,缺失或錯(cuò)誤都可能導(dǎo)致啟動(dòng)失敗
    2024-10-10
  • MyBatis-Plus 分頁插件配置的兩種方式實(shí)現(xiàn)

    MyBatis-Plus 分頁插件配置的兩種方式實(shí)現(xiàn)

    本文主要介紹了MyBatis-Plus 分頁插件配置的兩種方式實(shí)現(xiàn),包括使用PaginationInterceptor和MybatisPlusInterceptor兩種方式,具有一定的參考價(jià)值,感興趣的可以了解一下
    2025-03-03
  • Spring的Aware接口實(shí)現(xiàn)及執(zhí)行順序詳解

    Spring的Aware接口實(shí)現(xiàn)及執(zhí)行順序詳解

    這篇文章主要為大家介紹了Spring的Aware接口實(shí)現(xiàn)及執(zhí)行順序詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12

最新評(píng)論

宣汉县| 青龙| 宁陵县| 巴楚县| 涪陵区| 南昌县| 定边县| 建平县| 深圳市| 历史| 北辰区| 抚州市| 卢龙县| 滨州市| 衢州市| 淅川县| 年辖:市辖区| 沙雅县| 吉林省| 肃北| 洛川县| 海宁市| 永德县| 老河口市| 德阳市| 襄城县| 白山市| 丰镇市| 黄陵县| 河西区| 彝良县| 肇州县| 轮台县| 沂南县| 沧源| 诸城市| 乡城县| 安泽县| 宁明县| 澄城县| 中宁县|