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

基于MySQL和Redis扣減庫存的實踐

 更新時間:2023年05月08日 09:37:44   作者:EzreaLwj  
本文主要介紹了基于MySQL和Redis扣減庫存的實踐,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

背景

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

環(huán)境搭建

后臺系統(tǒng)

基于 SpringBoot 搭建后臺系統(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 進行數(shù)據(jù)的存儲,使用 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 '商品庫存'
) comment '庫存測試表';
insert into t_good(good_name, stock)  value('iphone', 50);

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

測試工具

使用 JMeter 5.5 進行測試

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

扣減模式

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

行鎖

若直接直接在數(shù)據(jù)庫層面進行庫存的直接扣減,100 個線程同時進行請求,肯定會造成庫存的超賣

SQL 語句為

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

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

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

開啟 JMeter 進行測試,可見沒有超賣

CAS

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

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

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

Java 代碼:

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

SQL 語句

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

測試結(jié)果:

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

基于 Redis 實現(xiàn)庫存的扣減

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

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

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

下面給出兩種解決辦法

使用 decrement 方法

redisTemplate.opsForValue().decrement():對某個 key 進行減 1 操作,會返回扣減后的值

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

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

Java 代碼:

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

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

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

測試結(jié)果:

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

MySQL 中的庫存沒有超賣:

使用 LUA 腳本

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

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) then
        return -1;
    end
    if (stock > 0) then
        redis.call('incrby', KEYS[1], -1);
        return 1;
    end
end
return -1

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

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 中的庫存均為 0 ,沒有超賣

使用分布式鎖

可以使用 redisson 分布式鎖進行扣減庫存處理,鎖住查詢和扣減兩個步驟即可;

若是在分布式環(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;
}

測試結(jié)果

Redis 中庫存數(shù)量沒有超賣

MySQL 中庫存數(shù)量沒有超賣

總結(jié)

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

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

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

相關(guān)文章

  • mysql delete limit 使用方法詳解

    mysql delete limit 使用方法詳解

    今天研究cms系統(tǒng)的時候發(fā)現(xiàn),delete 語句后面有個limit,一直都是select查詢的時候才使用,不懂為什么要用這個,正好就百度一下為大家分享下delete中使用limit方法與有點
    2014-11-11
  • MySQL之遞歸小問題

    MySQL之遞歸小問題

    這篇文章主要介紹了MySQL之遞歸小問題,需要的朋友可以參考下
    2017-05-05
  • MySQL死鎖檢查處理的正常方法

    MySQL死鎖檢查處理的正常方法

    這篇文章主要給大家介紹了關(guān)于MySQL死鎖檢查處理的正常方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-10-10
  • MySql 5.5.29綠色安裝教程詳解

    MySql 5.5.29綠色安裝教程詳解

    本文給大家介紹mysql5.5.29綠色安裝教程,本文給大家介紹的非常詳細,具有參考借鑒價值,需要的朋友參考下
    2017-01-01
  • 簡單了解MySQL存儲引擎

    簡單了解MySQL存儲引擎

    這篇文章主要介紹了MySQL存儲引擎的相關(guān)資料,為開發(fā)時選擇合適的存儲引擎提供參考,感興趣的朋友可以了解下
    2020-07-07
  • Mysql如何查看表及字段信息

    Mysql如何查看表及字段信息

    這篇文章主要介紹了Mysql如何查看表及字段信息,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-01-01
  • sql在一個表中添加字段并添加備注的方法

    sql在一個表中添加字段并添加備注的方法

    在本篇文章里小編給大家分享了sql在一個表中添加字段并添加備注的方法的內(nèi)容,有需要的朋友們可以參考學習下。
    2019-08-08
  • MySQL GROUP_CONCAT限制解決方案

    MySQL GROUP_CONCAT限制解決方案

    這篇文章主要介紹了MySQL GROUP_CONCAT限制解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值
    2020-09-09
  • Mysql數(shù)據(jù)庫的主從同步配置方式

    Mysql數(shù)據(jù)庫的主從同步配置方式

    這篇文章主要介紹了Mysql數(shù)據(jù)庫的主從同步配置方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-04-04
  • windows下如何安裝和啟動MySQL

    windows下如何安裝和啟動MySQL

    本篇文章主要給大家介紹windows下如何安裝和啟動MySQL,需要的朋友跟著小編一起來學習啦
    2015-08-08

最新評論

通州市| 横山县| 通榆县| 达拉特旗| 射洪县| 额尔古纳市| 拉萨市| 理塘县| 漳州市| 化州市| 苍南县| 丹棱县| 雷山县| 嘉荫县| 潜江市| 漾濞| 河西区| 沙洋县| 黄龙县| 虹口区| 灵宝市| 平和县| 卓资县| 亳州市| 桂阳县| 盐边县| 定边县| 永寿县| 阿瓦提县| 九台市| 东兴市| 太白县| 榆林市| 渝中区| 澎湖县| 巴马| 天祝| 尖扎县| 塔城市| 桓仁| 雷山县|