分布式锁:5种方案解决商品超卖的方案

news2024/11/19 7:27:28

一 分布式锁

1.1 分布式锁的作用

在多线程高并发场景下,为了保证资源的线程安全问题,jdk为我们提供了synchronized关键字和ReentrantLock可重入锁,但是它们只能保证一个工程内的线程安全。在分布式集群、微服务、云原生横行的当下,如何保证不同进程、不同服务、不同机器的线程安全问题。jdk并没有给我们提供既有的解决方案。需要自己通过编写方案来解决,目前主流的实现有以下方式:

  1. 基于mysql关系型实现

  2. 基于redis非关系型数据实现

  3. 基于zookeeper/etcd实现

1.2  四种方案的比较

性能:一个sql > 悲观锁 > jvm锁 > 乐观锁

如果追求极致性能、业务场景简单并且不需要记录数据前后变化的情况下。 优先选择:一个sql

如果写并发量较低(多读),争抢不是很激烈的情况下优先选择:乐观锁

如果写并发量较高,一般会经常冲突,此时选择乐观锁的话,会导致业务代码不间断的重试。​ 优先选择:mysql悲观锁

不推荐jvm本地锁。

二  模拟单体超卖

2.1 工程结构

2.2 编写工程代码

1.pom文件

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <version>2.3.12.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

2.配置文件

server.port=9999
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/fenbu_lock?characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=cloudiip
redis.host=172.16.116.100

 3.controller

@RestController
public class StockController {

    @Autowired
    private StockService stockService;

    @GetMapping("stock/deduct")
    public String deduct(){
       // this.stockService.deduct();
        this.stockService.deductByMsqlDb();
        return "hello stock deduct!!";
    }

}

4.service

@Service
public class StockService {
    @Autowired
    private StockMapper stockMapper;
    private Stock stock = new Stock();

    private ReentrantLock lock = new ReentrantLock();

    public void deduct(){
//        lock.lock();
//        try {
//            stock.setStock(stock.getStock() - 1);
//            System.out.println("库存余量:" + stock.getStock());
//        } finally {
//            lock.unlock();
//        }
    }
    public void deductByMsqlDb(){
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);

        // 再减库存
        if (stock != null && stock.getCount() > 0){
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }
    }

}

5.mapper


@Mapper
public interface StockMapper extends BaseMapper<Stock> {
}

6.启动类

@SpringBootApplication
@MapperScan("com.atguigu.distributed.lock.mapper")
public class DistributedLockApplication {

    public static void main(String[] args)
    {
        SpringApplication.run(DistributedLockApplication.class, args);
        System.out.println("========================启动成功==========");
    }

}

7.pojo类

@Data
@TableName("db_stock")
public class Stock {
    @TableId
    private Long id;

    private String productCode;

    private String stockCode;

    private Integer count;
   // private Integer stock = 5000;
}

8.附件数据表

1.新建一个数据库,附件数据表,如图

2.脚本文件

CREATE TABLE `db_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_code` varchar(255) DEFAULT NULL COMMENT '商品编号',
  `stock_code` varchar(255) DEFAULT NULL COMMENT '仓库编号',
  `count` int(11) DEFAULT NULL COMMENT '库存量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2.3 测试验证

http://localhost:9999/stock/deduct

查看数据库

2.4 jmeter模拟并发访问

2.4.1 启动jmeter

 2.4.2 配置jmeter

1.添加线程组

并发100循环50次,即5000次请求。  

3.给线程组添加HTTP Request请求:

4.将接口地址:http://localhost:9999/stock/deduct  配置到下面

 5.再选择你想要的测试报表,例如这里选择聚合报告:

启动测试,查看压力测试报告:

 参数api说明如下:

1.Label 取样器别名,如果勾选Include group name ,则会添加线程组的名称作为前缀

# Samples 取样器运行次数

Average 请求(事务)的平均响应时间

Median 中位数

90% Line 90%用户响应时间

95% Line 90%用户响应时间

99% Line 90%用户响应时间

Min 最小响应时间

Max 最大响应时间

Error 错误率

Throughput 吞吐率

Received KB/sec 每秒收到的千字节

Sent KB/sec 每秒收到的千字节

测试结果:请求总数5000次,平均请求时间129ms,中位数(50%)请求是在36ms内完成的,错误率0%,每秒钟平均吞吐量716.7次。

结论:此时如果还有人来下单,就会出现超卖现象(别人购买成功,而无货可发)。

三 方案1:使用jvm的本地锁解决冲突

3.1 原理

添加synchronized关键字之后,StockService就具备了对象锁,由于添加了独占的排他锁,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象。

3.2 操作

用jvm锁(synchronized关键字或者ReetrantLock)试试:

2.使用jmeter再次测试

查看数据库

并没有发生超卖现象,完美解决。  

3.3 此方案的缺点失效情况

1.多例模式  2.事务 ;3.集群

四 方案2:使用表级锁的sql解决冲突

4.1 表锁的使用范围

4.1.1 更新sql使用表锁

描述:

会话A执行: update db_stock set count=count-#{count} where product_code='1001' and count>=1

会话B执行:因为会话A执行的更新语句触发了表级锁,导致会话B无法执行插入,更新等语句。

insert into db_stock values(4,'1002','上海仓',5000);

update db_stock set count=count-1 where id=3;

1.会话A: 开启事务,执行更新语句,先不执行commit提交

2.会话B:  由于会话A执行更新语句后未提交,触发表级锁,此时自己进行更新,插入无法进行。

4.1.2 表锁变行锁

mysql悲观锁使用行级锁的条件:
1.锁的查询或者更新必须使用索引字段
2.查询或者更新必须是具体值。

1.给查询条件设置索引字段,让更新语句变为行级锁

2.会话A执行更新,让更新语句变为行级锁

3.会话B进行更新,回车后,可以看到进行提交执行了 

4.2 操作案例 

1.mapper级别

    @Update("update db_stock set count=count-#{count} where product_code=#{productCode} and count>=#{count}")
    int updateStock(@Param("productCode") String productCode,@Param("count") Integer count);

2.service

  public   void deductBySql(){
        // 先查询库存是否充足
       this.stockMapper.updateStock("1001",1);
        System.out.println("请求进来了.......");

    }

 3.查看数据库

4.并发压力测试

5.查看效果:均正确消费。

4.3 此方案缺点

优点:能够解决jvm本地锁多失效的3种情况。

缺点:1.确定锁的范围 行级锁还是表级锁;2.同一个商品有多条库存记录;

3.无法记录库存前后的变化记录。

五  方案3:使用悲观锁解决冲突

5.1 使用悲观锁原理

除了使用jvm锁之外,还可以使用数据锁:悲观锁 或者 乐观锁。

1.悲观锁:在select的时候就会加锁,采用先加锁后处理的模式,虽然保证了数据处理的安全性,但也会阻塞其他线程的写操作。在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 。select ... for update
悲观锁适用于写多读少的场景,因为拿不到锁的线程,会将线程挂起,交出CPU资源,可以把CPU给其他线程使用,提高了CPU的利用率。

会话A:select ... for update   给具体的行数据加上排他锁,也即行锁。

会话B :无法对1001进行更新,因为上了行级锁

5.2 操作案例

一个sql:直接更新时判断,在更新中判断库存是否大于0 ;

update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0

1.mapper:编写悲观锁语句

2.service:添加事务注解  @Transactional

3.数据表

4.jmeter压力测试

 5.查看效果:成功实现所减数据为0,均正确消费。

5.3 此方案的优缺点

1.性能问题;2.死锁问题:对多条数据加锁时,加锁顺序要一致;

3.库存操作要统一,一个会话用 select  x for update  一个会话执行select可以进行查询 ,存在数据不一致情况。

会话A:进行查询上表锁

会话B:可以进行查询查询。

六  方案4:使用乐观锁解决冲突

6.1 乐观锁原理

乐观锁:采取了更加宽松的加锁机制,大多是基于数据版本( Version )及时间戳来实现。适合于读比较多,不会阻塞读,读取数据时不上锁,更新时检查是否数据已经被更新过,如果是则取消当前更新进行重试。version 或者 时间戳(CAS思想)。

6.2 操作案例

使用数据版本(Version)记录机制实现,这是乐观锁最常用的实现 方式。一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录 的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新。

更新sql:

select * from db_stock where product_code='1001'
update db_stock set count=4996,version=version+1 where id=1 and version=0;

1.修改service

2.数据库表

3.压力测试

4.查看消费结果: 均正确消费

6.3 乐观锁存在的缺点

1.高并发情况下,性能比较低下,并发量越小,性能越高。 2.读写情况下,乐观锁不可靠。

七 方案5:使用redis的乐观锁

7.1 redis的乐观锁的原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1066119.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

w10系统 如何使用 C++、cmake、opencv、

w10系统的C环境配置 1.安装 vscode编辑器 首先安装&#xff1a;VScode 安装后开始安装插件&#xff1a; C 插件 2.配置w10系统的C环境 使用编译器MinGW 官方地址&#xff1a;https://www.mingw-w64.org/ 下载地址&#xff1a;https://sourceforge.net/projects/mingw-w64/f…

AI产品经理-能力模型

一、概况 AI产品经理/助理&#xff08;需求工程师&#xff09;&#xff1a;大多数入门的AI产品经理应该都在这里&#xff0c;顾名思义&#xff0c;就是在整体产品规划中帮助大PD实现部分产品功能的助理或者需求工程师&#xff0c;需要具备比较强的AI知识框架与理解能力以保障各…

Openlayers 教程 - 地图以及图层数据导出(打印)图片

Openlayers 教程 - 地图以及图层数据导出&#xff08;打印&#xff09;图片 地图导出核心代码完整代码&#xff1a;在线示例 本文包括地图导出核心代码、完整代码以及在线示例。 地图导出核心代码 这里放上 ES 封装的核心代码&#xff0c;创建多边形或者其他几何对象&#xff…

做小说推文和短剧推广,找数据好的授权平台

小说推文和短剧推广很多平台吃单怎么办&#xff1f;可以试试”巨量推文“&#xff0c;一个不吃单的平台 众所周知 小说推文和短剧推广很多平台会吃单&#xff0c;比如你实际官方数据是10个订单&#xff0c;很多平台只给你5个&#xff0c;这样你损失可能就是一半的利润&#xf…

【MySQL】基本查询(二)

文章目录 一. 结果排序二. 筛选分页结果三. Update四. Delete五. 截断表六. 插入查询结果结束语 操作如下表 //创建表结构 mysql> create table exam_result(-> id int unsigned primary key auto_increment,-> name varchar(20) not null comment 同学姓名,-> chi…

虚拟展厅有什么重要意义,了解虚拟展厅在宣传中的应用

引言&#xff1a; 随着科技的不断进步&#xff0c;虚拟展厅已经逐渐成为展览行业的重要一环。虚拟展厅是一种数字化平台&#xff0c;为观众提供了与传统展览完全不同的体验。 一&#xff0e;虚拟展厅的定义 虚拟展厅是一个通过互联网和虚拟现实技术创建的数字展示空间&#x…

windows系统下利用python对指定文件夹下面的所有文件的创建时间进行修改

windows系统下利用python对指定文件夹下面的所有文件的创建时间进行修改 不知道其他的朋友们有没有这个需求哈&#xff0c;反正咱家是有这个需求 需求1、当前有大量的文件需要更改文件生成的时间&#xff0c;因为不可告知的原因&#xff0c;当前的文件创建时间是不能满足使用的…

现在玩51单片机,这也太LOW了?

作为一名科普的博主&#xff0c;今天我们来聊一聊51单片机。 一、什么是51单片机 单片机是一种微型计算机&#xff0c;广泛应用于各种电子产品和工业控制领域。51单片机是指基于Intel的8051微处理器为核心的单片机&#xff0c;是最为常见和广泛应用的单片机之一。 二、51单片机…

近视眼选择什么台灯好?分享医生都说好的台灯

如今全国近视人数已经超过6亿&#xff0c;差不多占据了我国人口的一半&#xff0c;而青少年的近视率更是位居世界第一&#xff01;据数据显示&#xff0c;全国儿童青少年总体近视率高达53.6%&#xff0c;其中小学生为36.0%&#xff0c;初中生为71.6%&#xff0c;高中生为81.0%&…

微服务学习(十一):安装Git

微服务学习&#xff08;十一&#xff09;&#xff1a;安装Git 1、下载Git 官网下载Git 2、将下载后的资源包上传到服务器 3、解压并安装 tar -zxvf git-2.42.0.tar.gz4、安装依赖 yum install zlib yum install zlib-devel5、执行操作命令 cd /home/git/git-2.42.0 ./co…

华为云CodeArts Check代码检查服务用户声音反馈集锦(8)

作者&#xff1a;gentle_zhou 原文链接&#xff1a;CodeArts Check代码检查服务用户声音反馈集锦&#xff08;8&#xff09;-云社区-华为云 CodeArts Check&#xff08;原CodeCheck&#xff09;&#xff0c;是自主研发的代码检查服务。建立在华为30年自动化源代码静态检查技术…

张量-算术操作函数

tf.add(x,y,name None)求和函数 示例代码如下: import tensorflow.compat.v1 as tf tf.disable_v2_behavior()x 1 y 2a tf.add(x,y)with tf.Session() as sess:print(sess.run(a)) tf.subtract(x,y,name None)减法函数 示例代码如下: import tensorflow.compat.v1 as …

吐血整理,最全Pytest自动化测试框架快速上手(超详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 pytest框架 pyte…

前端【响应式图片处理】之 【picture标签】

目录 &#x1f31f;前言&#x1f31f;目前最常见的解决方案&#x1f31f;新的解决方案<picture>&#x1f31f;<picture>的工作原理&#x1f31f;<picture> 兼容性解决方案&#x1f31f;写在最后 &#x1f31f;前言 哈喽小伙伴们&#xff0c;前端开发过程中经…

<el-input> textarea文本域显示滚动条(超过高度就自动显示)

需求&#xff1a;首先是给定高度&#xff0c;输入文本框要自适应这个高度。文本超出高度就会显示滚动条否则不显示。 <el-row class"textarea-row"><el-col :span"3" class"first-row-title">天气</el-col><el-col :span&…

多目标优化两种算法:加权、智能优化算法

传统数学优化算法&#xff08;加权&#xff09; 使用数学优化算法解决多目标优化问题通常是将各个子目标聚合成一个带权重的单目标函数&#xff0c;系数由决策者决定&#xff0c;或者由优化方法自适应调整。即通过加权等方式将多目标问题转化为单目标问题进行求解。 这样每次只…

编程每日一练(多语言实现)基础篇:控制台打印九九乘法口诀表

文章目录 一、实例描述二、技术要点三、代码实现3.1 C 语言实现3.2 Python 语言实现3.3 Java 语言实现3.4 JavaScript 语言实现3.5 Go 语言实现 一、实例描述 本实例要求打印出乘法口诀表&#xff0c;在乘法口诀有行和列项的相乘得出的乘法结果。根据这个特点&#xff0c;使用…

Configuration of phpstudy and sqli-labs

Go download the app&#xff1a; 小皮面板(phpstudy) - 让天下没有难配的服务器环境&#xff01; (xp.cn) Have done. Then enter the program. Enable both functions&#xff1a; Apache and MySQL. Open the website&#xff1a; Next, Lets make the sqli-liab. GitHub…

Web server failed to start. Port 8080 was already in use

一、问题 package com.djc.boot;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annota…

几个G视频能压缩成几百MB吗?跟我学视频压缩

视频大小是可以压缩的&#xff0c;现在很多情况下&#xff0c;视频文件的大小都会限制我们的行动&#xff0c;例如需要将大量视频文件随身携带&#xff0c;或者在有限的网络带宽下传输视频文件。为了解决这些问题&#xff0c;下面给大家分享几个视频压缩的技巧&#xff0c;轻松…