多线程并发安全问题最典型的代表就是超卖现象
库存在并发量较大情况下很容易发生超卖现象,一旦发生超卖现象,就会出现多成交了订单而发不了货的情况。
场景:商品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才对 !!
建表语句:
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工程:
<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> {
}
@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,没有任何问题。
使用jmeter压力测试工具,高并发下压测一下,添加线程组:并发100循环50次,即5000次请求。
使用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压力测试,效果如下:
可以看到,加锁之后,吞吐量减少了一倍多!
原理
添加synchronized关键字之后,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象
原理: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 ,发生超卖
在加锁的地方加上 @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 ,发生超卖
解决办法
扩大锁的范围,将开启事务,提交事务也包括在锁的代码块中
@GetMapping("stock/deduct")
public String deduct(){
synchronized (this) {
this.stockService.deduct();
}
return "hello stock deduct!!";
}
使用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压力测试
注意
5) 原因
每个服务都有自己的本地锁,所以无法锁住临界区,导致多线程的安全问题
除了使用jvm锁之外,还可以使用mysql自带的锁:悲观锁 或者 乐观锁