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、一个sql

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;

表中数据如下:
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第1张图片

创建分布式锁demo工程:

目录结构
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第2张图片
pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.46version>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.3.4version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        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 简单实现减库存

1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第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、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第4张图片

查看数据库:
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第5张图片

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

1.4 演示超卖现象

使用jmeter压力测试工具,高并发下压测一下,添加线程组:并发100循环50次,即5000次请求。
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第6张图片
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第7张图片

启动测试,查看压力测试报告:
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第8张图片

  • 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、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第9张图片

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数据库:
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第10张图片
并没有发生超卖现象,完美解决。

原理
添加synchronized关键字之后,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第11张图片

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 ,发生超卖
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第12张图片

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 ,发生超卖
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第13张图片

造成超卖的原因:
Spring事务默认的隔离级别是可重复读
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第14张图片

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

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

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

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

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

1)启动两个服务(端口号分别10010 10086),如下:
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第15张图片

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的监听端口)
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第16张图片
请求正常,说明nginx负载均衡起作用了

4) Jmeter压力测试
注意

  • 先把数据库库存量还原到5000
  • 重新配置访问路径 http://localhost:80/stock/deduct
    1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第17张图片
    两台机器时,吞吐量明显大于单个机器

查看数据库,库存不为0,表示多服务时,Jvm锁失效
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)_第18张图片

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

1.7 mysql锁演示

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

1、一个sql

你可能感兴趣的:(分布式锁,分布式锁)