• 因为一次服务器卡顿,再来好好理解Spring事务传播


    事故描述

    在某天的一个下午,突然用户报系统使用特别卡顿。有些操作不能执行。查看数据库发现运行的线程数开始增加,每秒执行的事务次数也开始增加。

    数据库运行的线程数开始增加
    在这里插入图片描述
    每秒执行的事务次数也开始增加
    在这里插入图片描述

    问题排查

    一开始很痛苦,查不到是什么原因。
    查慢sql,查不到。查执行的process,只看到某个process一直在执行,但看不到sql语句,居然看不到具体是哪个sql,懵逼了

    SELECT * from performance_schema.events_statements_history order by LOCK_TIME desc limit 10;
    SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX order by trx_started asc
    
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS where lock_trx_id = 6037541544;
    
    SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX where trx_id = 6037541544;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    后来根据process开始的时间点,分析错误日志,才定位到此问题。

    事故原因

    spring手动事务没能关闭,而spring默认的事务传播机制是:PROPAGATION_REQUIRED,支持当前事务;如果不存在,创建一个新的。导致后面的Spring事务都往这个事务里面去,还一直提交不了,导致系统崩溃。

    @Autowired
      private PlatformTransactionManager txManager;
     
      @Autowired
      private ShopGroupBuyDao shopGroupBuyDao;
     
      @GetMapping(value = "/transactionDemo")
      public void ceshi() {
     
        // 手动开启事务  start
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus status = txManager.getTransaction(def);
        // 手动开启事务  end
     	 ShopGroupBuy shopGroupBuy = shopGroupBuyDao.selectOne(new LambdaQueryWrapper<ShopGroupBuy>()
                                                                    .eq(ShopGroupBuy::getGroupBuyId, 505));
     
          shopGroupBuy.setGroupBuyTheme("wulin11");
     
          int i = shopGroupBuyDao.updateById(shopGroupBuy);
     
          int a = 1 / 0;
        try {
          // 手动提交事务  start
          txManager.commit(status);
          // 手动提交事务  end
     
          if (i > 0) {
            System.out.println("更新成功");
          } else {
            System.out.println("更新失败");
          }
     
        } catch(Exception e) {
     
          e.printStackTrace();
     
          // 手动回滚事务  start
          txManager.rollback(status);
          // 手动回滚事务  end
          
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在这里插入图片描述

    Spring事务的基本原理

    Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

    1. 获取连接 Connection con = DriverManager.getConnection()
    2. 开启事务con.setAutoCommit(true/false);
    3. 执行CRUD
    4. 提交事务/回滚事务 con.commit() /con.rollback()
    5. 关闭连接 conn.close();

    使用Spring的事务管理功能后,我们可以不再写步骤2和4的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子。

    1. 配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
    2. spring在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
    3. 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

    事务的传播与嵌套

    所谓spring事务的传播属性,就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在TransactionDefinition中定义,具体常量的解释见下表:
    在这里插入图片描述
    通过上面的理论知识的铺垫,我们大致知道了数据库事务和spring事务的一些属性和特点,接下来我们通过分析一些嵌套事务的场景,来深入理解spring事务传播的机制。

    假设外层事务 ServiceA的Method A() 调用内层ServiceB的Method B()

    PROPAGATION_REQUIRED (Spring 默认)

    如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候Spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。

    假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。

    这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。

    PROPAGATION_REQUIRES_NEW

    比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。

    那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。

    它与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。

    PROPAGATION_SUPPORTS

    假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。

    PROPAGATION_NESTED

    开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.

    PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED之间的比较

    ServiceA {  
        /** 
         * 事务属性配置为 PROPAGATION_REQUIRED 
         */  
        @Transactional(propagation=Propagation.REQUIRED) // 1
        void methodA() {
            insertData(); //2
            try {
                ServiceB.methodB();   //3
            } catch (SomeException) {  
                // 执行其他业务, 如 ServiceC.methodC();   //5
            }
            updateData(); //6
        }  
    }
    ServiceB {
        
        @Transactional(propagation=Propagation.NESTED)
        void methodB(){
            //
            updateData(); //4
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在上面的1,将开起新事务A,2的时候会插入数据,此时事务A挂起,没有commit,3的时候,使用PROPAGATION_NESTED传播,将在3点的时候新建一个savepoint保存2插入的数据,不提交。

    1. 如果methodB出现异常,将回滚4的操作,不影响2的操作,同时可以处理后面的5,6逻辑,最后一起commit: 2,5,6
    2. 如果methodB没有出现异常,那么将一起commit: 2,4,6。
    3. 假如methodB使用的PROPAGATION_REQUIRES_NEW,那么B异常,会commit: 2,5,6,和NESTED一致,如果methodB没有出现异常,那么会先commit4,再commit:6,那么事务将分离开,不能保持一致,假如执行6报错,2和6将回滚,而4却没有被回滚,不能达到预期效果。

    为什么要手动开启事务

    开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。这是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
    解决方法如下:

    1. 注入自身(比较快捷,后面建议采用这种形式)
    2. 拆成两个类(略有点麻烦)
    3. AopContext.currentProxy()获取代理类(使用起来有点麻烦)
    4. 使用手写事务(容易出现事务未关闭的情况)

    注入自身的示例:

    @Service
    public class AreaService {
        @Autowired
        private AreaMapper areaMapper;
        @Autowired
        private AreaService service;//将自身service注入
        //没有加事务
    	public void insertDelete(Area area){
            service.insert(area);
            int abc = 1/0; //出现异常,程序结束
            service.deleteById(area.getId());//不会执行
        }
    	@Transactional(propagation =Propagation.REQUIRES_NEW)
        public int insert(Area area) {
            area.setDelFlag(0);
            area.setCreateTime(LocalDateTime.now());
            int insert = areaMapper.insert(area);
            int a = 1/0;//出现异常,事务回滚
            return insert;
        }
    	@Transactional(propagation =Propagation.REQUIRES_NEW)
        public int deleteById(Long id) {
            Long[] ids = {id};
            return areaMapper.updateDelFlagByIds(ids);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
  • 相关阅读:
    【双指针】盛水最多的容器
    CAD有生僻字如何打出来、如何提交软件相关问题或建议?
    状态模式
    Java:为什么使用Java而不是python?
    创建数据库表提示Working outside of application context.
    OLED显示模块的工作原理、结构特点、应用领域和发展趋势
    Unity设置TextMeshPro文本超出范围显示...
    java计算机毕业设计空闲教室查询系统MyBatis+系统+LW文档+源码+调试部署
    数据库实践 Hw09
    Spring MVC中@RequestParam注解的功能是什么呢?
  • 原文地址:https://blog.csdn.net/qq_27575627/article/details/127569914