事务碰上锁好似那油锅里进了火

目录

前言

 场景

代码复现        

提出疑问

该怎么解决呢

        1.使用编程式事务

          2.将事务独立出一个方法


前言

         很多时候我们谈起事务都是如虎色变,一想起来都是脑袋懵懵的

  1. 事务的隔离级别及传播机制是什么
  2. Spring的事务底层实现原理了解吗
  3. 哪几种情况下事务会失效       

        锁相关的更是让人如临大敌

  1. 可重入锁ReetrantLock和synchronized的区别
  2. 分布式锁的实现
  3. 轻量级锁volatile关键字的实现
  4. 说一说synchronized的锁升级流程

        当然了,大家都很厉害,上面这些稍微有点难度,仍可一力当之

        但是当事务遇上了锁,难上加难,阁下该如何应对呢。

        没开玩笑。

        在日常开发中,事务必不可少,锁也一样,那事务碰上锁,我们该怎么办呢

       


 场景

        我举一个经典的下单场景

                1. 扣减库存

                2. 生成订单

        首先我们会考虑加事务,防止以上哪步数据库操作失败,回滚数据

        再次考虑并发问题,加锁,防止超卖


代码复现        

        我这里有个库存表product,一共10件商品

        事务碰上锁好似那油锅里进了火_第1张图片

        模拟多线程调用,抢购这些商品,并创建订单。

事务碰上锁好似那油锅里进了火_第2张图片

        我们预期商品库存会变为0,并且生成10笔订单数据。

        我们根据直觉很容易写出如下代码,在方法上使用事务注解,用于异常回滚,使用锁(此处为了方便使用ReentrantLock,多服务一般都是使用分布式锁)用于并发控制。

        抢单逻辑如下:

        下面这段代码,方法上加了@Transactional(rollbackFor = Exception.class)

        方法的开启和结束也使用Lock加上了锁。

         给你几秒,好好想下,输出会是什么?

        订单表会生成多少条数据?

        “创建订单成功了”这个日志输出会打印多少次?

事务碰上锁好似那油锅里进了火_第3张图片

       没错,生成了20笔订单!!!!!

       订单表中一共20笔交易数据。

        怎么会这样!!!

        

事务碰上锁好似那油锅里进了火_第4张图片

事务碰上锁好似那油锅里进了火_第5张图片          日志输出也是整整20次

事务碰上锁好似那油锅里进了火_第6张图片

        如果是实际商品售卖场景,结果就是我们只有10件库存,由于我们这段代码有问题,多生成了一倍订单,生成了20笔订单。

        呐,货发不发出去是小事,工作可是要没了呀,这,这以后,以后还怎么带薪摸鱼呢    

事务碰上锁好似那油锅里进了火_第7张图片

        我们把@Transactional(rollbackFor = Exception.class)这行代码先拿掉再重新看下执行结果。当然实际情况下肯定需要加事务的,此处只是为了对比排错。

        可以看到只生成了10笔订单数据

事务碰上锁好似那油锅里进了火_第8张图片

提出疑问

        1.为什么会发生超卖,是锁使用的有问题还是事务使用的有问题

        2.为什么是正正好好多卖了一倍

       

事务碰上锁好似那油锅里进了火_第9张图片


      

        先看第一个问题,为什么会超卖呢?

        先问下,上述代码锁释放的代码,在try代码块执行完之后,finally代码块里执行,那么事务什么时候提交的呢,是在 this.orderMapper.insert(order);这条插入语句之后吗?

        很明显不是的。

        本文示例中事务是在方法执行结束之后提交的,熟悉Spring事务的同学们肯定知道,声明式事务@Transactional注解是基于代理的方式进行的。

        打上断点分析下

        可以看到在TransactionAspectSupport类中,invokeWithinTransaction()方法先对实际@Transactional注解修饰的方法代理执行后,最后才提交的事务

事务碰上锁好似那油锅里进了火_第10张图片

        也就是说在锁释放之后,事务还没有提交,中间是有程序在执行的那么一小段时间的,在这段时间内,如果新的线程进来,查询到库存还是原值,这个时候就会发生超卖。

        虽然这个时间点很小,但是并发量稍微起来点,谁也不能保证什么都不会发生。

        正如我们实例中展示,很容易就发生了超卖

        我这儿有篇收藏已久的Spring事务秘籍,v我50可点击查看

        死磕Spring之AOP篇 - Spring事务详解 - 月圆吖 - 博客园 (cnblogs.com)


        第二个问题,为什么一直是正好是创建了20条数据订单呢

        其实不会一直是正好20条,只是大概率会20条。

        看下图,我们来分下下执行流程。

        假设这个时候库存还是10个

        1.线程1释放锁的时候,扣减库存之后,事务还没来的及提交

        2.这个时候线程2拿到了锁,由于线程1的事务还没有提交,线程2读到的库存数据还是10个,这个时候很大几率就会产生超卖了,注意哦,这里并不是一定会产生。

        3.关键点是线程3这个时候是参与不进来的,它很想进来,但法律不允许3p,不对。。是没有锁的权限,它只能等着线程2去释放锁。

        所以,超卖的情况下最多只能有两个线程,大概率是库存的两倍 20个。

事务碰上锁好似那油锅里进了火_第11张图片

        当然,我们可以尝试复现下创建订单数不是20个的场景。

        我们在每个线程加锁之前先让线程睡一会儿,目的是让之前的线程事务提交完毕再去获取锁,再去查询库存,这样就有几率避免超卖,要根据你系统运行环境的性能来调试。

        事务碰上锁好似那油锅里进了火_第12张图片

        可以看到结果创建订单数就不会是20个了,而是13个

事务碰上锁好似那油锅里进了火_第13张图片

该怎么解决呢

        1.使用编程式事务

          为了方便,我这里使用  TransactionTemplate。

  事务碰上锁好似那油锅里进了火_第14张图片

     

        2.将事务独立出一个方法

        注意哦,这里是会有坑的,不要定义成private方法,不要同类中直接调用。

        抽出doSell()方法,将事务操作独立出来,直接调用是不会生效为了演示方便,我这里当前类自己注入自己

事务碰上锁好似那油锅里进了火_第15张图片

你可能感兴趣的:(Bug合集,java基础,事务,锁,多线程,并发,java)