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

2023-04-12

文章目录

  • 事故描述
  • 问题排查
  • 事故原因
  • Spring事务的基本原理
  • 事务的传播与嵌套
    • PROPAGATION_REQUIRED (Spring 默认)
    • PROPAGATION_REQUIRES_NEW
    • PROPAGATION_SUPPORTS
    • PROPAGATION_NESTED
    • PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED之间的比较
  • 为什么要手动开启事务
    • 注入自身的示例:
事故描述

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


数据库运行的线程数开始增加

每秒执行的事务次数也开始增加


问题排查

一开始很痛苦,查不到是什么原因。
查慢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;

后来根据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()
                                                                .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
      
    }
  }


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,将开起新事务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);
    }
}

本文仅代表作者观点,版权归原创者所有,如需转载请在文中注明来源及作者名字。

免责声明:本文系转载编辑文章,仅作分享之用。如分享内容、图片侵犯到您的版权或非授权发布,请及时与我们联系进行审核处理或删除,您可以发送材料至邮箱:service@tojoy.com