在一次减库存的操作中,出现了一个bug,特此记录如下.
基本场景如下,每次从数据库中查出某商品当前库存,对库存进行检验之后减一,然后写回数据库中,并发执行100次减库存的操作后,数据库会存在数据库数据未达到预期数量的问题.
为了探寻问题所在,对这段代码加上不同的注解然后并发执行30次减库存操作,每次减少1,观察变化.
代码如下:
//全局变量 测试成功次数
public static int i= 1;
//@Transactional
public Boolean testStock(Integer productId) {
Product product = productMapper.selectByPrimaryKey(productId);
if (product == null) {
throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
}
//减库存
Integer result = product.getStock() - 1;
if (result < 0) {
throw new SellException(ResultEnum.PRODUCT_STOCK_ERROR);
}
product.setStock(result);
//更新
int recond = productMapper.updateByPrimaryKey(product);
if (recond>0){
//每次成功之后都打印一次
System.out.println("succcess"+i++);
return true;
}
else {
System.out.println("f");
return false;
}
}
什么都不加
使用JMeter进行测试,30个线程循环一次,即一共请求30次,每次减少1,库存应该减少30.
结果如下:
控制台每次都打印出了相应的结果
但是数据库中总库存只减少了2.此方法有误。
仅使用 @Transactional注解
使用@Transactional是声明式事务,声明式事务使用动态代理.
在使用@Transactional时,在发生异常时默认仅对RuntimeException和Error发生回滚.
在相同条件下再进行同样的操作
所有请求全部正常执行,控制台打印也正常,但是数据库的库存与预期不一致
仅使用synchronized关键字
使用synchronized之后,并行的程序变成了顺序执行,所有一定不会出错,但这也违背了本来的业务需求,即并发执行需求.
达到了预期的正确性要求,但是吞吐量太低,不建议使用.
@Transactional注解和synchronized关键字一起使用
结果如下:
所有请求全部正常执行,控制台打印也正常,但是数据库的库存与预期不一致
结果总结
正确性 | |
---|---|
什么都不加 | × |
仅使用@Transactional注解 | × |
仅使用synchronized关键字 | √ |
@Transactional注解和synchronized关键字一起使用 | × |
分析
事务中的数据库操作,要么都做,要么都不做。
Java 中的 synchronized 关键字可以在多线程环境下用来作为线程安全的同步锁,这是java 级别的处理线程安全问题的操作。
对于第一种情况,直接多线程访问数据库而数据库没有做对应的同步操作,所以库存减少数量存在问题。
对于第二种情况,(待补充)
对于第三种情况,synchroniezd 关键字直接使得程序串行执行,所以不会存在问题。
对于第四种情况,使用Transactional注解并使用synchronized同步锁,多个线程有几率同时进入方法中。spring中的@Transactional注解是利用动态代理来实现的,当一个线程执行完该方法之后,代理类没有提交事务之前,其他线程有几率进入此方法,而多个线程在同一个事务中访问到的数据库中的数据是没有及时更新的,多个线程对同一个数据进行减一,最后的库存也就肯定不是预期中的数字。
解决方法
在查找商品的sql语句末尾中加上 for update
select for update 是为了在查询时,对这条数据进行加锁,避免其他用户以该表进行插入,修改或删除等操作,造成表的不一致性.
select * from t for update 会等待行锁释放之后,返回查询结果。
参考:
https://www.cnblogs.com/lyc94620/p/9506569.html
https://blog.csdn.net/guyue35/article/details/85052537
https://blog.csdn.net/u011186019/article/details/52348624