事务与并发

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这时就没用了,只能采用数据库级别的锁

上一篇下一篇

猜你喜欢

热点阅读