美文网首页
事务与并发

事务与并发

作者: 裂开的汤圆 | 来源:发表于2020-03-26 09:53 被阅读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这时就没用了,只能采用数据库级别的锁

    相关文章

      网友评论

          本文标题:事务与并发

          本文链接:https://www.haomeiwen.com/subject/htezyhtx.html