事务与并发
2020-03-26 本文已影响0人
裂开的汤圆
思考
测试环境:springboot+mysql。现在有这么一个场景,有一个接口,每请求一次商品库存减一,该接口已开启事务,事务隔离级别为默认(可重复读),同时起100个线程消费商品,会存在并发问题吗?
测试
代码如下:
//controller
@PostMapping("/consume")
public String consume(Integer goodsId){
return userService.consume(goodsId);
}
// service
@Transactional
public String consume(Integer goodsId){
Goods goods = goodsRepository.findById(goodsId).orElseGet(Goods::new);
if(goods.getNumbers() > 0){
int oldNumber = goods.getNumbers();
goods.setNumbers(goods.getNumbers() - 1);
goodsRepository.saveAndFlush(goods);
System.out.println("商品:" + goods.getName() + ",原库存:" + oldNumber + ",消费后库存:" + goods.getNumbers());
return "商品:" + goods.getName() + ",原库存:" + oldNumber + ",消费后库存:" + goods.getNumbers();
}else{
System.out.println("商品:" + goods.getName() + ",库存不足!");
return "商品:" + goods.getName() + ",库存不足!";
}
}
测试结果
很明显,开启了可重复读级别的事务并不能解决并发问题。那么这里会有一个疑问,事务设计出来不就是为了解决并发问题的吗,为什么这里仍然存在并发问题。
原因在于事务隔离的级别,可重复读级别下,事务读会阻塞其他事务事务写但不阻塞读,事务写会阻塞其他事务读和写。因此,多个线程同时读到库存为100的值,并且在写入时覆盖掉其他事务的数据。流程如下
模拟流程想了解更多事务隔离级别以及存在的问题,可以去看看下面这篇文章
事务并发的可能问题与其解决方案
采用synchronized解决上述问题
我们很容易想到,直接在service层函数上添加synchronized关键字不就好了,代码如下
// service层代码
@Transactional
public synchronized String consume(Integer goodsId){
Goods goods = goodsRepository.findById(goodsId).orElseGet(Goods::new);
if(goods.getNumbers() > 0){
int oldNumber = goods.getNumbers();
goods.setNumbers(goods.getNumbers() - 1);
goodsRepository.saveAndFlush(goods);
System.out.println("商品:" + goods.getName() + ",原库存:" + oldNumber + ",消费后库存:" + goods.getNumbers());
return "商品:" + goods.getName() + ",原库存:" + oldNumber + ",消费后库存:" + goods.getNumbers();
}else{
System.out.println("商品:" + goods.getName() + ",库存不足!");
return "商品:" + goods.getName() + ",库存不足!";
}
}
再来测试一下,将商品库存设置为100,再次开启100个线程去消费,那么最后结果会是0吗?
结果仍然是错误的很明显,红色框里的两条消费记录重复消费了。产生这个问题的原因在于动态代理,具体可以去参考下面这篇文章
Synchronized锁在Spring事务管理下,为啥还线程不安全?
除了在业务层面上锁,我们还能在数据库层面上锁解决该问题
思路:采用select for update,想了解select for update可以去看看下面这篇文章,这里不做分析,需要注意的一点是要使用select for update,必须得把Mysql的自动提交给关闭
MysqL_select for update锁详解
修改后代码
//controller
@PostMapping("/consume")
public String consume(Integer goodsId){
return userService.consume(goodsId);
}
// service
@Transactional
public String consume(Integer goodsId){
// 修改下面这行代码
Goods goods = goodsRepository.getById(goodsId);
if(goods.getNumbers() > 0){
int oldNumber = goods.getNumbers();
goods.setNumbers(goods.getNumbers() - 1);
goodsRepository.saveAndFlush(goods);
System.out.println("商品:" + goods.getName() + ",原库存:" + oldNumber + ",消费后库存:" + goods.getNumbers());
return "商品:" + goods.getName() + ",原库存:" + oldNumber + ",消费后库存:" + goods.getNumbers();
}else{
System.out.println("商品:" + goods.getName() + ",库存不足!");
return "商品:" + goods.getName() + ",库存不足!";
}
}
// GoodsRepository代码
public interface GoodsRepository extends JpaRepository<Goods, Integer> {
// JPA采用@Lock注解上锁
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
Goods getById(Integer goodsId);
}
测试结果
select for update锁
什么场景下使用synchronized,什么场景下使用数据库级别的锁
如果web程序部署到多个服务器上,synchronized这时就没用了,只能采用数据库级别的锁