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

目录

    • 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 {@TableIdprivate Long id;private String productCode;private String stockCode;private Integer count;
}

StockMapper接口:

public interface StockMapper extends BaseMapper<Stock> {
}

1.3 简单实现减库存

在这里插入图片描述

@RestController
public class StockController {@Autowiredprivate StockService stockService;@GetMapping("stock/deduct")public String deduct(){this.stockService.deduct();return "hello stock deduct!!";}}@Service
public class StockService {@Autowiredprivate 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 {@Autowiredprivate 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 注解

 @Transactionalpublic 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关键词

@Transactionalpublic 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);// 每次更新 版本号 + 1stock.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:1001redisTemplate.execute(new SessionCallback<Object>() {@Overridepublic 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.mzph.cn/news/9556.shtml

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

相关文章

深度学习:常用优化器Optimizer简介

深度学习&#xff1a;常用优化器Optimizer简介 随机梯度下降SGD带动量的随机梯度下降SGD-MomentumSGDWAdamAdamW 随机梯度下降SGD 梯度下降算法是使权重参数沿着整个训练集的梯度方向下降&#xff0c;但往往深度学习的训练集规模很大&#xff0c;计算整个训练集的梯度需要很大…

集睿致远推出CS5466多功能拓展坞方案:支持DP1.4、HDMI2.1视频8K输出

ASL新推出的 CS5466是一款Type-C/DP1.4转HDMI2.1的显示协议转换芯片,&#xff0c;它通过类型C/显示端口链路接收视频和音 频流&#xff0c;并转换为支持TMDS或FRL输出信令。DP接收器支持81.Gbp s链路速率。HDMI输出端口可以作为TMDS或FRL发射机工作。FRL发射机符合HDMI 2.1规范…

OpenCvSharp (C# OpenCV) 二维码畸变矫正--基于透视变换(附源码)

导读 本文主要介绍如何使用OpenCvSharp中的透视变换来实现二维码的畸变矫正。 由于CSDN文章中贴二维码会导致显示失败,大家可以直接点下面链接查看图片: C# OpenCV实现二维码畸变矫正--基于透视变换 (详细步骤 + 代码) 实现步骤 讲解实现步骤之前先看下效果(左边是原图,右边…

菜鸡shader:L13 渐隐渐显的UV动画

文章目录 SD部分Shader代码部分 呃呃&#xff0c;这是目前我学习庄懂AP01课程的最后一节了&#xff0c;我看了一下21集之后的内容是关于LightingMap&#xff0c;目前感觉还用不到&#xff0c;加上之前参与过一个项目里面也有用到LightingMap&#xff0c;也算了解过&#xff0c;…

拦截Bean使用之前各个时机的Spring组件

拦截Bean使用之前各个时机的Spring组件 之前使用过的BeanPostProcessor就是在Bean实例化之后&#xff0c;注入属性值之前的时机。 Spring Bean的生命周期本次演示的是在Bean实例化之前的时机&#xff0c;使用BeanFactoryPostProcessor进行验证&#xff0c;以及在加载Bean之前进…

NISP含金量?NISP真的有必要考么?NISP好考吗?NISP二级为什么那么贵?

NISP证书简述 NISP证书三个级别&#xff0c;分别是&#xff1a;一级、二级、三级&#xff08;专项&#xff09; 证书。其每一项资格证书都有不同的优点&#xff0c;但是优点各有 相同&#xff0c;而且拥有NISP二级证书可以免考更换CISP资格证书&#xff0c;那么证书含金量如何下…

rcu链表综合实践

基础知识 rcu-read copy update的缩写。和读写锁起到相同的效果。据说牛逼一点。对于我们普通程序员&#xff0c;要先学会使用&#xff0c;再探究其内部原理。 链表的数据结构&#xff1a; struct list_head {struct list_head *next, *prev; };还有一种&#xff1a;struct h…

STM32(HAL库)驱动st7789LCD屏幕(7引脚240*240)

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 屏幕引脚配置 2.3 项目生成 3、KEIL端程序整合 3.1 LCD驱动添加 3.2 函数修改 3.2.1 lcd.h修改 3.2.2 lcd_innit.h 修改 3.2.3 lcd.c修改 3.2.4 lcd_inut.c修改 3.3 主函数代码 3.3…

Django学习笔记-表单(forms)的使用

在Django中提供了了form表单&#xff0c;可以更为简单的创建表单模板信息&#xff0c;简化html的表单。 一、网页应用程序中表单的应用 表单通常用来作为提交数据时候使用。 1.1 创建表单模板文件夹 在项目文件夹下创建一个template文件夹&#xff0c;用于存储所有的html模…

使用百度地图SDK计算距离

说明&#xff1a;通过百度地图提供的SDK&#xff0c;可以计算出两个地点之间的距离&#xff0c;另外还有行驶路线等等。本文介绍如果使用百度地图SDK&#xff0c;并用java代码实现。 申请 首先需要登录百度地图的官网&#xff0c;申请开发者认证&#xff0c;个人认证一般都很…

GPT一键化身「AI助理」——自定义指令功能

最近GPT又更新了一个超实用的功能——自定义指令&#xff0c;启用后&#xff0c;你可以给GPT设置一些固定指令&#xff0c;让它记住或扮演某个角色&#xff0c;比如客服、律师、投资管理师、老师、营养师...... 这样&#xff0c;我们就不再需要每次都要打开新的聊天&#xff0c…

ChatGPT的工作原理:从输入到输出

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

华为eNSP:isis配置跨区域路由

一、拓扑图 二、路由器的配置 1、配置接口IP AR1: <Huawei>system-view [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip add 1.1.1.1 24 [Huawei-GigabitEthernet0/0/0]q AR2: [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip add 1.1.1.2 24 [Huawe…

【React Native】学习记录(一)——环境搭建

Expo是一套工具&#xff0c;库和服务&#xff0c;可让您通过编写JavaScript来构建原生iOS和Android应用程序。 一开始学习的时候直接使用的是expo。 npx create-expo-app my-appcd my-appnpm run start接下来需要搭建安卓和IOS端&#xff08;为此特意换成了苹果电脑&#xff09…

云曦暑期学习第二周——文件上传漏洞

1.文件上传 1.1原理 一些web应用程序中允许上传图片、视频、头像和许多其他类型的文件到服务器中。 文件上传漏洞就是利用服务端代码对文件上传路径变量过滤不严格将可执行的文件上传到一个到服务器中 &#xff0c;再通过URL去访问以执行恶意代码。 1.2为什么存在文件上传漏…

Angular:动态依赖注入和静态依赖注入

问题描述&#xff1a; 自己写的服务依赖注入到组件时候是直接在构造器内初始化的。 直到看见代码中某大哥写的 private injector: Injector 动态依赖注入和静态依赖注入 在 Angular 中&#xff0c;使用构造函数注入的方式将服务注入到组件中是一种静态依赖注入的方式。这种方…

ThinkPHP8知识详解:给PHP8和MySQL8添加到环境变量

在PHPenv安装的时候&#xff0c;环境变量默认的PHP版本是7.4的&#xff0c;MySQL的版本是5.7的&#xff0c;要想使用ThinkPHP8来开发&#xff0c;就必须修改环境变量&#xff0c;本文就详细讲解了如果修改PHP和MySQL的环境变量。 1、添加网站 启动phpenv&#xff0c;网站&…

LiveGBS流媒体平台GB/T28181功能-设备树自定义分组自定义组织机构选择通道共享给上级国标平台配置权限给指定用户

LiveGBS流媒体平设备树自定义分组自定义组织机构选择通道共享给上级国标平台权限给指定用户 1、背景2、分组2.1、新建分组2.2、选择通道2.3、导入设备2.4、编辑名称2.5、删除分组2.6、移除分组 3、国标级联3.1、分组共享节点3.1.1、共享给上级平台3.1.2、分配权限给用户 3.2、级…

Android 帧率分析

卡顿&#xff1a; 界面呈现是指从应用生成帧并将其显示在屏幕上的动作。如需确保用户能够流畅地与您的应用互动&#xff0c;您的应用呈现每帧的时间不应超过 16ms&#xff0c;以达到每秒 60 帧的呈现速度&#xff08;为什么是 60fps&#xff1f;&#xff09;。如果您的应用存在…

PV操作解决经典进程同步问题

一.经典同步问题 在学习《操作系统》时&#xff0c;会接触到进程的概念&#xff0c;其中不可避免的接触到进程同步问题&#xff0c;今天我们用熟悉的PV操作解决一些经典的进程同步问题。 二.生产者-消费者问题 1.问题描述 问题描述&#xff1a;一组生产者进程和一组消费者进…