强烈建议配合之前的JVM本地锁(一)简单实现阅读
mysql场景
将之前的场景修改为mysql场景,即在数据库中保存一条数据,多个线程并发处理该数据。
数据库建表如下
pom.xml中新增mybatis-plus和mysql
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
application.yml
配置文件中配置好mysql的地址和用户名密码
server:
port: 10010
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx:3306/distributed_lock
username: root
password: 123
Stock
@TableName("db_stock")
@Data
public class Stock {
private Long id;
private String productCode;
private String warehouse;
private Integer count;
}
StockMapper
@Mapper
public interface StockMapper extends BaseMapper<Stock> {
}
StockService
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
public void deduct(){
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_code","1001"));
if(stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount()-1);
stockMapper.updateById(stock);
}
}
}
StockController
@RestController
public class StockController {
@Autowired
private StockService stockService;
@GetMapping("stock/deduct")
public String deduct(){
stockService.deduct();
return "hello stock deduct";
}
}
启动后,修改下Jmeter的http request地址
再进行测试
由于需要数据库连接访问,吞吐量明显降低,182。
再看数据库该条数据的count是否清0
并不是0,并发引起了超卖问题。
JVM本地锁解决mysql并发问题
synchronized
直接用JVM本地锁,加上synchronized
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
public synchronized void deduct(){
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_code","1001"));
if(stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount()-1);
stockMapper.updateById(stock);
}
}
}
重启后,把数据库count修改为5000,再次测试
吞吐量惨不忍睹,16。
再看数据库,成功清0
ReentrantLock
简单修改StockService即可
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
private ReentrantLock lock = new ReentrantLock();
public void deduct(){
lock.lock();
try{
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_code","1001"));
if(stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount()-1);
stockMapper.updateById(stock);
}
}finally {
lock.unlock();
}
}
}
重启后,把数据库count修改为5000,再次测试
吞吐量较synchronized略提升,22。
再看数据库
问题解决。
mysql中JVM本地锁不生效的三种情况
1. 多例模式
默认情况下,service controller等class都是单例模式,可以通过添加注解成为多例模式
@Service
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StockService {
重启,修改count数据后再测试
无错误,再看数据库
count并没有清0。
因为多例模式,不同线程访问的可能是不同的service实例,那你锁的其实只是一部分,而不是所有的。
2.事务
在deduct方法上添加事务注解@Transactional
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
private ReentrantLock lock = new ReentrantLock();
@Transactional
public void deduct(){
lock.lock();
try{
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_code","1001"));
if(stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount()-1);
stockMapper.updateById(stock);
}
}finally {
lock.unlock();
}
}
}
重新测试。
依然有问题,这是为什么呢?
举例,如果有两个线程A B同时请求,假设现在count为91
A访问 | B访问 |
---|---|
A获取锁 | |
A查询count:91 | |
A扣减count:90 | |
释放锁 | |
B获取锁 | |
B查询count:91 | |
A提交事务 | |
B扣减count:90 | |
B释放锁 | |
B提交事务 |
很明显,count为91,AB两个线程并发请求后,只变成了90
根本问题还是先释放锁再提交事务,不能保证原子性。
如果设置事务的隔离级别为 READ_UNCOMMITTED就可解决该问题,但是你读取别人未提交的事务,别人的事务很有可能还会回滚,就会造成脏读问题,所以不能使用READ_UNCOMMITTED。
3. 集群部署
类似多例模式,但还是有所不同。
1) 将服务新增一个端口启动。
两个服务都启动成功。
2) nginx负载均衡
修改nginx.conf
vi /usr/local/etc/nginx/nginx.conf
修改完后重启nginx
brew services restart nginx
3)修改jmeter测试端口为80
4)重新测试
很明显看见,有处理同一个库存数现象
数据库也未清0
总结一下:
jvm本地锁有三种情况后导致锁失效
- 多例模式
- 事务
- 集群部署