洛阳做网站公司电话,wordpress影视模板,增城市网站建设,wordpress换标题目录 1.1 从减库存聊起1.2 环境准备1.3 简单实现减库存1.4 演示超卖现象1.5 jvm锁1.6 三种情况导致Jvm本地锁失效1、多例模式下#xff0c;Jvm本地锁失效2、Spring的事务导致Jvm本地锁失效3、集群部署导致Jvm本地锁失效 1.7 mysql锁演示1.7.1、一个sql1.7.2、悲观锁1.7.3、乐观… 目录 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、一个sql1.7.2、悲观锁1.7.3、乐观锁1.7.4、mysql锁总结 1.8 redis乐观锁1.8.1 引入redis1.8.2 redis乐观锁原理1.8.3 redis乐观锁解决超卖问题1.8.4 redis乐观锁的缺点 1.1 从减库存聊起
多线程并发安全问题最典型的代表就是超卖现象 库存在并发量较大情况下很容易发生超卖现象一旦发生超卖现象就会出现多成交了订单而发不了货的情况。
场景商品S库存余量为5时用户A和B同时来购买一个商品此时查询库存数都为5库存充足则开始减库存 用户Aupdate db_stock set stock stock - 1 where id 1 用户Bupdate 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)
) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;表中数据如下
创建分布式锁demo工程
目录结构 pom.xml
dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.46/version/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.4.3.4/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency/dependenciesapplication.yml配置文件
server.port10010
spring.datasource.driver-class-namecom.mysql.jdbc.Driver
spring.datasource.urljdbc:mysql://192.168.239.11:3306/atguigu_distributed_lock
spring.datasource.usernameroot
spring.datasource.passwordhouchenDistributedLockApplication启动类
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 BaseMapperStock {
}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 BaseMapperStock {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 BaseMapperStock {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 UpdateWrapperStock().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 SessionCallbackObject() {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乐观锁的缺点
性能问题