国产成人精品久久免费动漫-国产成人精品天堂-国产成人精品区在线观看-国产成人精品日本-a级毛片无码免费真人-a级毛片毛片免费观看久潮喷

您的位置:首頁(yè)技術(shù)文章
文章詳情頁(yè)

基于MySQL和Redis扣減庫(kù)存的實(shí)踐

瀏覽:207日期:2023-05-08 10:17:44
目錄
  • 背景
  • 環(huán)境搭建
    • 后臺(tái)系統(tǒng)
    • 中間件
    • 測(cè)試工具
  • 扣減模式
    • 基于數(shù)據(jù)庫(kù)行鎖 + CAS 實(shí)現(xiàn)庫(kù)存的扣減
    • 基于 Redis 實(shí)現(xiàn)庫(kù)存的扣減
  • 總結(jié)

    背景

    在很多情況下,扣減庫(kù)存是一個(gè)十分常見(jiàn)的需求,例如:學(xué)生選課系統(tǒng)中課程數(shù)量的扣減,抽獎(jiǎng)系統(tǒng)中活動(dòng)次數(shù)的扣減,電商系統(tǒng)中商品庫(kù)存的扣減等,都涉及到數(shù)量的扣減,這些系統(tǒng)在成功扣減的前提下,絕對(duì)不能出現(xiàn)庫(kù)存扣減多了的情況,也就是不能出現(xiàn)超賣。同時(shí),我們也要注重系統(tǒng)性能的提升,這篇文章從這兩個(gè)角度進(jìn)行分析和討論。

    環(huán)境搭建

    后臺(tái)系統(tǒng)

    基于 SpringBoot 搭建后臺(tái)系統(tǒng),JDK 為 1.8

    <properties>    <java.version>1.8</java.version>    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>    <spring-boot.version>2.3.12.RELEASE</spring-boot.version></properties><dependencies>    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>    </dependency>    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>    </dependency>    <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>    </dependency>    <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>    </dependency>    <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version>    </dependency></dependencies>

    中間件

    中間件使用 MySQL + Redis 進(jìn)行數(shù)據(jù)的存儲(chǔ),使用 Mybatis 作為 ORM 框架

    create database t_desc collate utf8mb4_general_ci;use t_desc;create table t_good (    id bigint auto_increment primary key comment "自增id",    good_name varchar(255) not null comment "商品名稱",    stock int not null comment "商品庫(kù)存") comment "庫(kù)存測(cè)試表";insert into t_good(good_name, stock)  value("iphone", 50);

    創(chuàng)建一張商品庫(kù)存表,里面含有商品 id、商品名稱 和庫(kù)存 3 個(gè)字段,所有扣減庫(kù)存的操作都在這張表上進(jìn)行;

    測(cè)試工具

    使用 JMeter 5.5 進(jìn)行測(cè)試

    以下的庫(kù)存數(shù)量統(tǒng)一設(shè)置為 50 個(gè),線程組的數(shù)量為 10 個(gè),循環(huán) 10 次,共 100 個(gè)扣減請(qǐng)求,最終正確的結(jié)果應(yīng)該是扣減完畢后庫(kù)存的數(shù)量應(yīng)該為 0, 而不是 -50

    扣減模式

    基于數(shù)據(jù)庫(kù)行鎖 + CAS 實(shí)現(xiàn)庫(kù)存的扣減

    行鎖

    若直接直接在數(shù)據(jù)庫(kù)層面進(jìn)行庫(kù)存的直接扣減,100 個(gè)線程同時(shí)進(jìn)行請(qǐng)求,肯定會(huì)造成庫(kù)存的超賣

    SQL 語(yǔ)句為

    <update id="descGoodStock">  update t_desc.t_good  set t_good.stock = t_good.stock - 1  where id = #{id}</update>

    考慮到 update 語(yǔ)句,若根據(jù)主鍵索引作為條件進(jìn)行更新,會(huì)對(duì)數(shù)據(jù)庫(kù)的某一行加上行鎖(數(shù)據(jù)庫(kù)開(kāi)啟事務(wù)自動(dòng)提交),所以我們加上 stock > 0 的判斷條件

    <update id="descGoodStockByLock">update t_desc.t_goodset t_good.stock = t_good.stock - 1where id = #{id}  and t_good.stock > 0</update>

    開(kāi)啟 JMeter 進(jìn)行測(cè)試,可見(jiàn)沒(méi)有超賣

    CAS

    CAS 即 Compare and Set,先把舊的庫(kù)存查出來(lái),再把舊的庫(kù)存作為 update 的條件之一,若數(shù)據(jù)庫(kù)中的庫(kù)存與舊的庫(kù)存一致,則進(jìn)行更新,否則不進(jìn)行更新。

    其實(shí)本質(zhì)上與行鎖的方式?jīng)]什么區(qū)別,而且多了一次查詢,寫(xiě)這個(gè)方法只是為了記錄而已

    若有兩個(gè)以上的線程先查詢到了商品的舊庫(kù)存,這種方法可能會(huì)出現(xiàn)扣不完的情況

    Java 代碼:

    @PostMapping("/db")public Map<String, Object> goodDescControllerByDataBase(Long id) {    HashMap<String, Object> ret = new HashMap<>();    // 查出舊的值    Good good = goodMapper.selectStockById(id);    // 再進(jìn)行更新    int i = goodMapper.descGoodStockCAS(id, good.getStock());    if (i > 1) {ret.put("info", "success, 扣減成功");    } else {ret.put("info", "fail, 扣減失敗");    }    return ret;}

    SQL 語(yǔ)句

    <update id="descGoodStockCAS">update t_desc.t_goodset t_good.stock = t_good.stock - 1where id = #{id}  and t_good.stock = #{stock}  and t_good.stock > 0    </update>

    測(cè)試結(jié)果:

    綜上,基于數(shù)據(jù)庫(kù)的兩種扣減庫(kù)存的方式都沒(méi)有實(shí)現(xiàn)超賣,但是畢竟是數(shù)據(jù)庫(kù),數(shù)據(jù)存儲(chǔ)于物理磁盤(pán)中,性能方面就有待考量;

    基于 Redis 實(shí)現(xiàn)庫(kù)存的扣減

    基本思想是:我們把庫(kù)存的數(shù)量提前放到 Redis 上,直接在 Redis 進(jìn)行庫(kù)存的扣減

    • 先查詢 redis 中的庫(kù)存
    • 若小于 0 直接返回
    • 若大于 0 則進(jìn)行 Redis 和 數(shù)據(jù)庫(kù) 中的庫(kù)存扣減

    不過(guò)這里存在 并發(fā) 問(wèn)題,考慮極限情況,兩個(gè)線程同時(shí)獲得 stock = 1,然后再去進(jìn)行庫(kù)存扣減,勢(shì)必會(huì)造成超賣的現(xiàn)象

    下面給出兩種解決辦法

    使用 decrement 方法

    redisTemplate.opsForValue().decrement():對(duì)某個(gè) key 進(jìn)行減 1 操作,會(huì)返回扣減后的值

    若該值大于等于 0 才進(jìn)行數(shù)據(jù)庫(kù)的庫(kù)存的扣減,否則直接返回庫(kù)存不足的提示

    這種方法是基于 Redis 的指令是原子性的

    Java 代碼:

     @PostMapping("/redis")    public Map<String, Object> goodDescControllerByRedis(Long id) throws InterruptedException {HashMap<String, Object> ret = new HashMap<>();ret.put("info", "fail, 扣減失敗");// 查詢 Redis 中的庫(kù)存Integer stock = (Integer) redisTemplate.opsForValue().get(key + id);Thread.sleep(100);if (stock <= 0) {    return ret;}// 扣減 redis 中庫(kù)存Long decrement = redisTemplate.opsForValue().decrement(key + id);if (decrement >= 0) {    // 扣減數(shù)據(jù)庫(kù)庫(kù)存    goodMapper.descGoodStock(id);    ret.put("info", "success, 扣減成功");}return ret;    }

    其實(shí) decrement 方法是原子性的,可以不用對(duì)庫(kù)存先進(jìn)行查詢的操作,只需要判斷扣減后的數(shù)是否大于 0 即可。但是如果并發(fā)量高的話,建議還是加上判斷的邏輯,可以提高 Redis 的性能,不用每次進(jìn)行 decrement 操作;

    缺點(diǎn):這種辦法會(huì)導(dǎo)致 Redis 中庫(kù)存產(chǎn)生超賣現(xiàn)象,若對(duì) Redis 中庫(kù)存數(shù)量要求準(zhǔn)確,就不要使用這種方法;

    測(cè)試結(jié)果:

    Redis 中的庫(kù)存產(chǎn)生超賣現(xiàn)象:

    MySQL 中的庫(kù)存沒(méi)有超賣:

    使用 LUA 腳本

    上述問(wèn)題的關(guān)鍵是:查詢 和 扣減 是兩個(gè)分開(kāi)操作,不是一條原子性的命令。我們可以使用 LUA 腳本,把這兩條命令封裝到 LUA 代碼中,實(shí)現(xiàn)這兩個(gè)操作的原子性

    LUA 代碼

    ------ Generated by EmmyLua(https://github.com/EmmyLua)--- Created by Ezreal.--- DateTime: 2023/5/6 21:56---if (redis.call("exists", KEYS[1]) == 1) then    local stock = tonumber(redis.call("get", KEYS[1]));    if (stock <= 0) thenreturn -1;    end    if (stock > 0) thenredis.call("incrby", KEYS[1], -1);return 1;    endendreturn -1

    先獲取值,然后判斷庫(kù)存數(shù)量,若沒(méi)有小于等于 0 就先進(jìn)行扣減即可

    Java 代碼

    private static final DefaultRedisScript<Long> DECREASE_GOOD_STOCK_SCRIPT = new DefaultRedisScript<>();static {    DECREASE_GOOD_STOCK_SCRIPT.setLocation(new ClassPathResource("/lua/desc_stock.lua"));    // 設(shè)置返回值類型    DECREASE_GOOD_STOCK_SCRIPT.setResultType(Long.class);}@PostMapping("/lua")public Map<String, Object> goodDescControllerByLUA(Long id) {    List<String> keys = new ArrayList<>();    keys.add("stock:" + id);    HashMap<String, Object> ret = new HashMap<>();    ret.put("info", "fail, 扣減失敗");    Long execute = redisTemplate.execute(DECREASE_GOOD_STOCK_SCRIPT, keys);    if (execute == 1) {goodMapper.descGoodStock(id);ret.put("info", "success, 扣減成功");    }    return ret;}

    結(jié)果:Redis 和 MySQL 中的庫(kù)存均為 0 ,沒(méi)有超賣

    使用分布式鎖

    可以使用 redisson 分布式鎖進(jìn)行扣減庫(kù)存處理,鎖住查詢和扣減兩個(gè)步驟即可;

    若是在分布式環(huán)境下,要考慮 分布式鎖 與 LUA 腳本的結(jié)合!

    java 代碼

    @PostMapping("/lock")public Map<String, Object> goodDescControllerByLock(Long id) throws InterruptedException {    HashMap<String, Object> ret = new HashMap<>();    ret.put("info", "fail, 扣減失敗");    // 加鎖    RLock lock = redissonClient.getLock("stock" + id);    boolean tryLock = lock.tryLock(2L, 1L, TimeUnit.SECONDS);    if (tryLock) {Integer stock = (Integer) redisTemplate.opsForValue().get(key + id);if (stock <= 0) {    return ret;}Long decrement = redisTemplate.opsForValue().decrement(key + id);if (decrement >= 0) {    goodMapper.descGoodStock(id);    ret.put("info", "success, 扣減成功");}    }    return ret;}

    測(cè)試結(jié)果

    Redis 中庫(kù)存數(shù)量沒(méi)有超賣

    MySQL 中庫(kù)存數(shù)量沒(méi)有超賣

    總結(jié)

    如果在項(xiàng)目初期流量較少可以考慮基于 數(shù)據(jù)庫(kù)行鎖 進(jìn)行庫(kù)存的扣減,到了后期流量大,幾乎都要用到 Redis:

    • decrement:追求簡(jiǎn)單快速實(shí)現(xiàn),不考慮 Redis 庫(kù)存中的準(zhǔn)確性;
    • LUA 腳本:追求 Redis 中庫(kù)存的準(zhǔn)確性,在 Redis 層面上要進(jìn)行多重的條件判斷
    • Lock:追求 Redis 中庫(kù)存的準(zhǔn)確性,在分布式環(huán)境中要考慮 LUA + Lock 的結(jié)合

    到此這篇關(guān)于基于MySQL和Redis扣減庫(kù)存的實(shí)踐的文章就介紹到這了,更多相關(guān)MySQL和Redis扣減庫(kù)存內(nèi)容請(qǐng)搜索以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持!

    標(biāo)簽: MySQL
    主站蜘蛛池模板: 免费国产不卡午夜福在线 | 欧美性极品hd高清视频 | 欧美精品亚洲精品日韩一区 | 97免费在线观看视频 | 草草在线免费视频 | 日韩一级片免费 | 亚洲另类自拍 | 久久久毛片免费全部播放 | 成人爽爽大片在线观看 | 亚洲一区二区三区四区五区六区 | 夜色毛片永久免费 | 欧美色欧美亚洲高清在线视频 | 久久成人免费 | 一本一道波多野结衣456 | 欧美日韩免费做爰视频 | 成人在线播放 | 国产成人精品高清免费 | 日韩在线中文 | 久久国产夜色精品噜噜亚洲a | 国产成人香蕉在线视频网站 | 杨幂精品国产专区91在线 | 日本一区二区三区在线 视频观看免费 | 日韩一区三区 | 男操女视频 | 91久久99热青草国产 | 三级网站在线 | 日韩在线高清视频 | 无码免费一区二区三区免费播放 | 久久久成人影院 | 久久久网久久久久合久久久久 | 美女张开腿让男人 | 亚洲一级毛片 | 三级黄网站 | 欧美视频www | 欧美人成在线 | 萌白酱在线喷水福利视频 | 透逼视频 | 一级毛片a免费播放王色 | 久久91精品国产99久久yfo | 高清成人爽a毛片免费网站 高清大学生毛片一级 | 国产精品黄在线观看免费 |