1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)

news2024/12/25 19:12:19

目录

    • 1.1 从减库存聊起
    • 1.2 环境准备
    • 1.3 简单实现减库存
    • 1.4 演示超卖现象
    • 1.5 jvm锁
    • 1.6 三种情况导致Jvm本地锁失效
      • 1、多例模式下,Jvm本地锁失效
      • 2、Spring的事务导致Jvm本地锁失效
      • 3、集群部署导致Jvm本地锁失效
    • 1.7 mysql锁演示
      • 1.7.1、一个sql
      • 1.7.2、悲观锁
      • 1.7.3、乐观锁
      • 1.7.4、mysql锁总结
    • 1.8 redis乐观锁
      • 1.8.1 引入redis
      • 1.8.2 redis乐观锁原理
      • 1.8.3 redis乐观锁解决超卖问题
      • 1.8.4 redis乐观锁的缺点

1.1 从减库存聊起

多线程并发安全问题最典型的代表就是超卖现象
库存在并发量较大情况下很容易发生超卖现象,一旦发生超卖现象,就会出现多成交了订单而发不了货的情况。

场景:商品S库存余量为5时,用户A和B同时来购买一个商品,此时查询库存数都为5,库存充足则开始减库存
用户A:update db_stock set stock = stock - 1 where id = 1
用户B:update db_stock set stock = stock - 1 where id = 1
并发情况下,更新后的结果可能是4,而实际的最终库存量应该是3才对 !!

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;

表中数据如下:
在这里插入图片描述

创建分布式锁demo工程:

目录结构
在这里插入图片描述
pom.xml

<dependencies>
        <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.3.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml配置文件:

server.port=10010
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.239.11:3306/atguigu_distributed_lock
spring.datasource.username=root
spring.datasource.password=houchen

DistributedLockApplication启动类:

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

    public static void main(String[] args) {
        SpringApplication.run(DistributedLockApplication.class, args);
    }

}

Stock实体类:

@Data
@TableName("db_stock")
public class Stock {

    @TableId
    private Long id;

    private String productCode;

    private String stockCode;

    private Integer count;
}

StockMapper接口:

public interface StockMapper extends BaseMapper<Stock> {
}

1.3 简单实现减库存

在这里插入图片描述

@RestController
public class StockController {

    @Autowired
    private StockService stockService;

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

}

@Service
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    public void  deduct(){
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);
        // 再减库存
        if (stock != null && stock.getCount() > 0){
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }
    }
}

测试:
在这里插入图片描述

查看数据库:
在这里插入图片描述

在浏览器中一个一个访问时,每访问一次,库存量减1,没有任何问题。

1.4 演示超卖现象

使用jmeter压力测试工具,高并发下压测一下,添加线程组:并发100循环50次,即5000次请求。
在这里插入图片描述
在这里插入图片描述

启动测试,查看压力测试报告:
在这里插入图片描述

  • 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 每秒收到的千字节

查看mysql数据库剩余库存数:还有4818
在这里插入图片描述

1.5 jvm锁

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

 /**
     *  使用jvm锁来解决超卖问题
     */
    public synchronized void deduct() {
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);
        // 再减库存
        if (stock != null && stock.getCount() > 0) {
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }
    }

重启tomcat服务,再次使用jmeter压力测试,效果如下:
在这里插入图片描述
可以看到,加锁之后,吞吐量减少了一倍多!

查看mysql数据库:
在这里插入图片描述
并没有发生超卖现象,完美解决。

原理
添加synchronized关键字之后,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象
在这里插入图片描述

1.6 三种情况导致Jvm本地锁失效

1、多例模式下,Jvm本地锁失效

原理:StockService有多个对象,不同的对象持有不同的锁,所以还是会有多个线程进入到 临界区

演示:

@Service
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    /**
     *  使用jvm锁来解决超卖问题
     */
    public synchronized void deduct() {
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);
        // 再减库存
        if (stock != null && stock.getCount() > 0) {
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }
    }
}

重启tomcat服务,再次使用jmeter压力测试,查看数据库,发现库存确实没有减到 0 ,发生超卖
在这里插入图片描述

2、Spring的事务导致Jvm本地锁失效

在加锁的地方加上 @Transactional 注解

 @Transactional
    public synchronized void deduct() {
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);
        // 再减库存
        if (stock != null && stock.getCount() > 0) {
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }
    }

重启tomcat服务,再次使用jmeter压力测试,查看数据库,发现库存确实没有减到 0 ,发生超卖
在这里插入图片描述

造成超卖的原因:
Spring事务默认的隔离级别是可重复读
在这里插入图片描述

解决办法
扩大锁的范围,将开启事务,提交事务也包括在锁的代码块中

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

3、集群部署导致Jvm本地锁失效

使用jvm锁在单工程单服务情况下确实没有问题,但是在集群情况下会怎样?

接下启动多个服务并使用nginx负载均衡

1)启动两个服务(端口号分别10010 10086),如下:
在这里插入图片描述

2)配置nginx 负载均衡

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
	
	upstream distributed {
		server localhost:10010;
		server localhost:10086;
	}

    server {
        listen       80;
        server_name  localhost;
		location / {
			proxy_pass http://distributed;
		}
    }
}

3)在post中测试:http://localhost/stock/deduct (其中80是nginx的监听端口)
在这里插入图片描述
请求正常,说明nginx负载均衡起作用了

4) Jmeter压力测试
注意

  • 先把数据库库存量还原到5000
  • 重新配置访问路径 http://localhost:80/stock/deduct
    在这里插入图片描述
    两台机器时,吞吐量明显大于单个机器

查看数据库,库存不为0,表示多服务时,Jvm锁失效
在这里插入图片描述

5) 原因
每个服务都有自己的本地锁,所以无法锁住临界区,导致多线程的安全问题

1.7 mysql锁演示

除了使用jvm锁之外,还可以使用mysql自带的锁:悲观锁 或者 乐观锁

1.7.1、一个sql

update db_stock set count = count - 1 where product_code = '1001' and count >= #{count}
public void deduct() {
        this.stockMapper.updateStock("1001", 1);
    }
    
 public interface StockMapper extends BaseMapper<Stock> {
    @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);
}

这种方式可以解决上述Jvm锁失效的三个问题

缺点:
1、确定好锁范围
当使用的是表锁时,会导致系统的吞吐量直线下降

​ 什么情况下会使用行级锁

​ 1)锁的查询或者更新条件必须是索引字段

​ 2) 查询或者更新条件必须是具体值

2、一件商品多个仓库问题无法处理

3、无法记录仓库变化前后的状态

1.7.2、悲观锁

SELECT ... FOR UPDATE                     (悲观锁)

代码实现

改造StockService: 添加事务注解,去掉synchronized关键词

@Transactional
    public void deduct() {
        Stock stocks = this.stockMapper.queryStockForUpdate("1001");
        if (stocks != null && stocks.getCount() > 0) {
            stocks.setCount(stocks.getCount() - 1);
            this.stockMapper.updateById(stocks);
        }
    }

在StockeMapper中定义selectStockForUpdate方法:

public interface StockMapper extends BaseMapper<Stock> {


    @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);

    @Select("select * from db_stock where product_code = #{productCode} for update")
    Stock queryStockForUpdate(@Param("productCode") String productCode);
}

压力测试
注意:测试之前,需要把库存量改成5000。压测数据如下:比jvm锁性能高很多
在这里插入图片描述
mysql数据库存:
在这里插入图片描述

【注意】使用MySQL乐观锁时,也需要注意锁的粒度,尽量使用行级锁,否则系统吞吐量会降低

1.7.3、乐观锁

乐观锁是相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则重试。

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

给db_stock表添加version字段:
在这里插入图片描述

改造 StockService

  /**
     *  使用MySQL乐观锁来解决库存超卖问题
     */
    public void deduct() {
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);
        // 再减库存
        if (stock != null && stock.getCount() > 0){
            // 获取版本号
            Long version = stock.getVersion();

            stock.setCount(stock.getCount() - 1);
            // 每次更新 版本号 + 1
            stock.setVersion(stock.getVersion() + 1);
            // 更新之前先判断是否是之前查询的那个版本,如果不是重试
            if (this.stockMapper.update(stock, new UpdateWrapper<Stock>().eq("id", stock.getId()).eq("version", version)) == 0) {
                deduct();
            }
        }
    }

重启后使用jmeter压力测试工具结果如下:
在这里插入图片描述
在这里插入图片描述
并发度比较低,说明乐观锁在并发量越大的情况下,性能越低(因为需要大量的重试);并发量越小,性能越高。

乐观锁存在的问题

  • 高并发情况下,性能较低
  • ABA问题
  • 读写分离的情况下,可能会导致乐观锁不可靠

1.7.4、mysql锁总结

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

  • 如果追求极致性能、业务场景简单并且不需要记录数据前后变化的情况下。

​ 优先选择:一个sql

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

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

​ 优先选择:mysql悲观锁

  • 不推荐jvm本地锁。

1.8 redis乐观锁

1.8.1 引入redis

见我的博客 https://blog.csdn.net/hc1285653662/article/details/127564372 中的SpringDataRedis客户端

改造StockService

  /**
     * 为了提高请求响应的速度,将库存放在redis中进行操作
     */
    public void deduct() {
        // 先查询库存是否充足
        String stockStr = redisTemplate.opsForValue().get("stock:" + "1001");
        Long stock = Long.parseLong(stockStr);
        if (stock != null && stock > 0) {
            redisTemplate.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));
        }
    }

演示redis库存超卖
设置redis库存为 5000
在这里插入图片描述
jmeter启动测试,可以看到并发比无锁时候的mysql库存要高
在这里插入图片描述
查询redis库存,发现剩余库存不为0,所以发生超卖现象
在这里插入图片描述

1.8.2 redis乐观锁原理

使用watch命令监视某个key,如果在监视的过程中该key被某个客户端修改后,那么自身对于key的修改将会失败
在这里插入图片描述

1.8.3 redis乐观锁解决超卖问题

改造StockService

/**
     * 为了提高请求响应的速度,将库存放在redis中进行操作
     */
    public void deduct() {
        // 监听 stock:1001
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.watch("stock:" + "1001");
                String stockStr = (String) operations.opsForValue().get("stock:" + "1001");
                Long stock = Long.parseLong(stockStr);
                if (stock != null && stock > 0) {
                    operations.multi();
                    operations.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));
                    List exec = operations.exec();
                    // 如果减库存失败,代表key别其他客户端修改了,则进行重试
                    if (exec == null || exec.size() == 0) {
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        deduct();
                    }
                    return exec;
                }
                return null;
            }
        });
    }

查看测试结果:发现并发很低(可能因为我redis部署在阿里云上的docker里,网络开销导致并发很低),但是确实解决超卖问题
在这里插入图片描述
在这里插入图片描述

1.8.4 redis乐观锁的缺点

  • 性能问题

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

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

相关文章

行为型设计模式之观察者模式【设计模式系列】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 设计模式系列 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everythi…

Alluxio技术分析

Alluxio技术分析 Alluxio: A Virtual Distributed File System Alluxio主要解决的基于磁盘的分布式存储层性能低下的问题&#xff0c;通过alluxio提供的分布式内存来加速数据分析。 Alluxio的这种通过内存加速数据的想法其实是有明确的使用场景的&#xff1a; Immutable da…

【安全】web中的常见编码浅析浏览器解析机制

目录 常见编码 一、ASCII码 二、URL编码 三、Unicode编码 四、HTML实体编码 结合编码理解浏览器解析机制 常见编码 一、ASCII码 ASCII (American Standard Code for Information Interchange&#xff0c;美国信息交换标准代码&#xff09; 计算机内部&#xff0…

QString和QByteArray的区别

QString和QByteArray的区别 本质格式转换QString字符串格式化打印长度 本质 QString是对QByteArray的再次封装 QString可以通过char*来构造&#xff0c;也可以通过QByteArray来构造 QByteArray就是char* QString是编码后的char* QString也是封装了字符串, 但是内部的编码为utf…

Linux笔记——搜索命令find、解压缩命令、vi编辑器、用户权限命令、系统信息相关命令讲解

系列文章目录 Linux笔记——磁盘进行分区与挂载介绍 Linux笔记——管道相关命令以及shell编程 Linux笔记——进程管理与网络监控技术讲解​​​​​​ Linux笔记——rpm与yum下载软件命令介绍 文章目录 系列文章目录 准备工作 一 搜索命令—— find 搜索 1.1 目标 1.…

【UE5 多人联机教程】04-加入游戏

效果 步骤 1. 新建一个控件蓝图&#xff0c;父类为“USC_Button_Standard” 控件蓝图命名为“UMG_Item_Room”&#xff0c;用于表示每一个搜索到的房间的界面 打开“UMG_Item_Room”&#xff0c;在图表中新建一个变量&#xff0c;命名为“Session” 变量类型为“蓝图会话结果…

Matlab 点云曲面特征提取

文章目录 一、简介二、实现代码2.1基于k个邻近点2.2基于邻近半径参考资料一、简介 这里基于每个点的邻域协方差来获取点云中具有的曲面几何特征的点,计算方式如下图所示: 二、实现代码 2.1基于k个邻近点 SurfaceVar.m %% *******</

零信任网络架构与实现技术的研究与思考

目前&#xff0c;国外已有较多有关零信任网络的研究与实践&#xff0c;包括谷歌的 BeyondCorp、BeyondProd&#xff0c;软件定义边界&#xff08;Software Defined Perimeter&#xff0c;SDP&#xff09; 及盖特提出的“持续自适应风险与信任评估”等。国内也有不少安全厂商积极…

Unity 性能优化一:性能标准、常用工具

性能标准 推荐耗时&#xff1a; 性能提现到玩家直观感受&#xff0c;就是帧率&#xff0c;为了达到要求的帧率&#xff0c;就要控制CPU的耗时&#xff0c;不同类型的游戏&#xff0c;对帧率要求不一样。下面是推荐耗时&#xff1a; 推荐内存&#xff1a; 避免游戏闪退的重点…

network failed to load response data: no resource with given ide...

Chrome 开发者工具无法显示服务器正常返回的 HTTP 请求 - Failed to load response data 今天做开发时遇到一个问题&#xff0c;Chrome 开发者工具 network 标签里&#xff0c;虽然一个 HTTP 请求已经成功从服务器端返回&#xff0c;但是 Chrome 开发者工具里&#xff0c;仍然…

Cisco学习笔记(CCNA)——Open Shortest Path First (OSPF)

Open Shortest Path First (OSPF) 动态路由协议介绍 动态路由协议&#xff1a; 向路由表中添加远程网络 探索网络 更新和维护路由表 自主网络探索&#xff1a; 通过共享路由表信息路由器能探索到新的网络 动态路由协议的分类 内部网关协议&#xff08;IGP&#xff09; 适…

华为数通HCIP-ISIS高级

isis区域间的互访 1、L2区域 to L1区域 在L1区域发布的路由会以L1-LSP在L1区域内传递&#xff0c;到达L1-2路由器时&#xff0c;L1-2路由器会将该L1-LSP转换为L2-LSP在L2区域内传递&#xff1b; 因此L2区域的设备可以学习到L1区域的明细路由&#xff0c;进行访问&#xff1b;…

通过 API 远程管理 Jenkins

目录 前言&#xff1a; 背景介绍 Jenkins Remote API 的简介 Jenkins Remote API 的调用 Read More ... 前言&#xff1a; Jenkins 是一种开源的持续集成工具&#xff0c;可以帮助我们更加方便地进行软件开发和测试工作。通过 API 远程管理 Jenkins 可以帮助我们更加方便…

Windows 11 22H2 中文版、英文版 (x64、ARM64) 下载 (updated Jul 2023)

Windows 11 22H2 中文版、英文版 (x64、ARM64) 下载 (updated Jul 2023) Windows 11, version 22H2 官方原版&#xff0c;2023 年 7 月 更新 请访问原文链接&#xff1a;https://sysin.org/blog/windows-11/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作…

(三)RabbitMQ七种模式介绍与代码演示

Lison <dreamlison163.com>, v1.0.0, 2023.06.22 七种模式介绍与代码演示 文章目录 七种模式介绍与代码演示四大交换机四种交换机介绍 工作模式简单模式&#xff08;Hello World&#xff09;工作队列模式&#xff08;Work queues&#xff09;订阅模式&#xff08;Publis…

Qt 类似vscode和matlab的分屏显示效果

运行截图 向右分屏 多分屏 全屏显示 介绍 实现了一个类似vscode和matlab的标签页显示分屏效果&#xff0c;支持鼠标拖拽分屏、全屏显示&#xff0c;可自适应调整大小&#xff0c;程序把要显示的Widget独立出来&#xff0c;可随时替换为其他的用户自定义Widget&#xff0c…

挂载文件系统

文章目录 注册文件系统类型挂载文件系统系统调用mount绑定挂载挂载命名空间1.标准的挂载命名空间2.共享子树&#xff08;1&#xff09;共享挂载&#xff08;2&#xff09;从属挂载&#xff08;3&#xff09;私有挂载&#xff08;4&#xff09;不可绑定挂载 挂载跟文件系统1.根文…

YOLOv5改进RepViT结构:清华 ICCV 2023,原创Bottleneck设计

RepViT: Revisiting Mobile CNN From ViT Perspective 论文方法从块设计改进YOLOv5结构核心代码1核心代码2核心代码3yaml1yaml2yaml3论文:https://arxiv.org/pdf/2307.09283.pdf   代码:https://github.com/THU-MIG/RepViT 最近,轻量级视觉 Trans

Android App 持续集成性能测试:启动流量

目录 前言&#xff1a; get app UID 获取流量数据 获得启动流量数据 总结 前言&#xff1a; Jenkins 是一种开源的持续集成工具&#xff0c;可以帮助我们更加方便地进行软件开发和测试工作。通过 API 远程管理 Jenkins 可以帮助我们更加方便地进行 Jenkins 的配置和管理工…

临时文档3

值传递 当一个对象被当作参数传递到一个方法后&#xff0c;此方法可改变这个对象的属性&#xff0c;并可返回变化后的结果&#xff0c;那么这里到底是值传递还是引用传递 是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时&…