searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Spring编程式事务使用不当,引起的事务不提交

2022-11-25 10:22:43
462
0

1.问题描述

系统在运行过程中,时不时就出现线程获取事务锁超时的异常,通过观察日志发现,超时的sql都涉及到几条特定数据的改动,初步怀疑是这数据被其他事务加上了行锁,且长时间不释放

2.排查过程

通过查询information_schema库中INNODB_TRX(记录当前mysql运行中的事务)表发现,有一个从昨晚到今早都还运行的事务,该事务锁了14张表和110行记录。

根据这个现象,怀疑是程序中开启了事务,但是却没有提交引起。通过搜索发现可疑代码

结合日志发现,在问题事务运行前,确实抛出过名称存在的异常。基本可以确定是该代码引起的。

3.解决问题

将通过DataSourceTransactionManager 获取获取事务的代码下移到,抛出业务异常代码之后。并在finally里执行commit或者rollback操作。

改完重新发布测试,问题解决。

4.原理分析

通过分析问题产生的原理,能有效的帮助我们加深这一块的相关知识,避免之后再犯相关的错误。简单描述下上述用到的三个事务相关的类的作用:

  • DataSourceTransactionManager 事务管理器,负责管理事务的行为,比如开始,提交,回滚等操作
  • TransactionDefinition 是对事务属性的相关定义,比如设置事务的隔离传播机制,超时时间等。
  • TransactionStatus 表示一个事务的状态,接口提供了控制事务执行和查询事务状态的方法, 比如当前调用栈中之前已经存在了一个事物,那么就是通过该接口来判断的,TransactionStatus接口可以让事务管理器DataSourceTransactionManager 控制事务的执行。

了解这三个类的作用,对了解Spring事务有极大的帮助,从这三个类入口,我们深入了解下产生这次事故的深层次原理,当通过DataSourceTransactionManager 管理事务时,有以下主要步骤:

  1. doGetTransaction
  • 获取一个DataSourceTransactionObject对象,其中持有ConnectionHolder(从ThreadLocal中获取,可能已经存在这个对象)

      

  1. isExistingTransaction
  • 判断获取到的DataSourceTransactionObject对象是否持有ConnectionHolder,ConnectionHolder中的连接是否活跃
  • 如果当前线程已经持有了ConnectionHolder,则根据不同的传播行为做不同的逻辑

     

     

  1. doBegin
  • 从数据源中获取一个Connection,将Connection绑定到ConnectionHolder上,已被嵌套的事务获取
  • 设置事务的隔离级别,并开启事务
  • 使用ThreadLocal将当前线程与ConnectionHolder绑定起来

       

      

  1. doCommit
  • 在执行doCommit前,会判断当前的事务是否是新开的事务,只有是新的事务,才会执行Connectioncommit操作

     

  1. doRollback
  • 只有符合isNewTransaction条件时,才会执行Connectionrollback操作  

     

  1. cleanupAfterCompletion
  • 不管是commit方法或者是rollback方法,在finally中都会执行cleanupAfterCompletion方法
  • 只有符合isNewTransaction条件时,才会执行资源的解绑(将ConnectionHolder与当前线程解绑)

     

7.总结

通过上述执行流程的分析,明确了问题产生的原因: 线程第一次执行时,在开启事务后(使用ThreadLocal完成ConnectionHolder与当前线程的绑定),由于业务异常退出,没有执行commit或者rollback操作。由于使用的是线程池,线程执行完不会销毁,因此ThreadLocal中的值不会被清除,当线程第二次获取事务(从ThreadLocal中获取)时,由于使用的事务传播机制REQUIRED,所以不会产生新的事务,此时线程执行commit或者rollback操作,都不会使Connection真正的commit或者rollback。随着该线程不断的执行业务逻辑,该事务锁住的表和记录也会不断的上升。

5.优化思路

  1. 设置事务超时时间(不推荐)

TransactionDefinition接口中,可以设置事务运行的超时时间,但是这得依赖于数据库底层的实现,有的数据库不支持该方式,因此不建议使用

  1. 使用Spring声明式事务(推荐)

 使用声明式事务只需要在启动类中加上@EnableTransactionManagement,然后在需要使用事务的方法加上@Transactional注解即可,需要注意的是,在使用@Transactional注解时需要尽可能的指定rollbackFor的异常类型,且使用@Transactional在以下场景中会导致事务失效

  • @Transactional 注解应用到非 public 可见度的方法上
  • 同一个类中方法调用,会导致@Transactional失效
  • 方法内部try-catch异常,事务不会回滚
  • 数据库引擎不支持事务

@Transactional作用于方法上,如果需要在一个方法内部做更细化的事务控制,会比较麻烦,这个时候可以考虑使用编程式事务

  1. 使用TransactionTemplate编程式事务(推荐)

TransactionTemplate的使用非常简单,将我们需要事务控制的方法,放入带execute方法中执行,并将结果返回即可。

@Autowired
TransactionTemplate transactionTemplate;

public Long execute() {
    transactionTemplate.execute((status)-> {
        //do something
        return result;
    })
}

使用TransactionTemplate可以很方便进行细粒度的事务控制,在execute方法内部,事务的提交与回滚都已经封装好,用户只需要关注自己的业务逻辑即可,也不用担心在使用事务时,忘记commit或者rollback而引起的问题

0条评论
0 / 1000
韦****生
2文章数
0粉丝数
韦****生
2 文章 | 0 粉丝
韦****生
2文章数
0粉丝数
韦****生
2 文章 | 0 粉丝
原创

Spring编程式事务使用不当,引起的事务不提交

2022-11-25 10:22:43
462
0

1.问题描述

系统在运行过程中,时不时就出现线程获取事务锁超时的异常,通过观察日志发现,超时的sql都涉及到几条特定数据的改动,初步怀疑是这数据被其他事务加上了行锁,且长时间不释放

2.排查过程

通过查询information_schema库中INNODB_TRX(记录当前mysql运行中的事务)表发现,有一个从昨晚到今早都还运行的事务,该事务锁了14张表和110行记录。

根据这个现象,怀疑是程序中开启了事务,但是却没有提交引起。通过搜索发现可疑代码

结合日志发现,在问题事务运行前,确实抛出过名称存在的异常。基本可以确定是该代码引起的。

3.解决问题

将通过DataSourceTransactionManager 获取获取事务的代码下移到,抛出业务异常代码之后。并在finally里执行commit或者rollback操作。

改完重新发布测试,问题解决。

4.原理分析

通过分析问题产生的原理,能有效的帮助我们加深这一块的相关知识,避免之后再犯相关的错误。简单描述下上述用到的三个事务相关的类的作用:

  • DataSourceTransactionManager 事务管理器,负责管理事务的行为,比如开始,提交,回滚等操作
  • TransactionDefinition 是对事务属性的相关定义,比如设置事务的隔离传播机制,超时时间等。
  • TransactionStatus 表示一个事务的状态,接口提供了控制事务执行和查询事务状态的方法, 比如当前调用栈中之前已经存在了一个事物,那么就是通过该接口来判断的,TransactionStatus接口可以让事务管理器DataSourceTransactionManager 控制事务的执行。

了解这三个类的作用,对了解Spring事务有极大的帮助,从这三个类入口,我们深入了解下产生这次事故的深层次原理,当通过DataSourceTransactionManager 管理事务时,有以下主要步骤:

  1. doGetTransaction
  • 获取一个DataSourceTransactionObject对象,其中持有ConnectionHolder(从ThreadLocal中获取,可能已经存在这个对象)

      

  1. isExistingTransaction
  • 判断获取到的DataSourceTransactionObject对象是否持有ConnectionHolder,ConnectionHolder中的连接是否活跃
  • 如果当前线程已经持有了ConnectionHolder,则根据不同的传播行为做不同的逻辑

     

     

  1. doBegin
  • 从数据源中获取一个Connection,将Connection绑定到ConnectionHolder上,已被嵌套的事务获取
  • 设置事务的隔离级别,并开启事务
  • 使用ThreadLocal将当前线程与ConnectionHolder绑定起来

       

      

  1. doCommit
  • 在执行doCommit前,会判断当前的事务是否是新开的事务,只有是新的事务,才会执行Connectioncommit操作

     

  1. doRollback
  • 只有符合isNewTransaction条件时,才会执行Connectionrollback操作  

     

  1. cleanupAfterCompletion
  • 不管是commit方法或者是rollback方法,在finally中都会执行cleanupAfterCompletion方法
  • 只有符合isNewTransaction条件时,才会执行资源的解绑(将ConnectionHolder与当前线程解绑)

     

7.总结

通过上述执行流程的分析,明确了问题产生的原因: 线程第一次执行时,在开启事务后(使用ThreadLocal完成ConnectionHolder与当前线程的绑定),由于业务异常退出,没有执行commit或者rollback操作。由于使用的是线程池,线程执行完不会销毁,因此ThreadLocal中的值不会被清除,当线程第二次获取事务(从ThreadLocal中获取)时,由于使用的事务传播机制REQUIRED,所以不会产生新的事务,此时线程执行commit或者rollback操作,都不会使Connection真正的commit或者rollback。随着该线程不断的执行业务逻辑,该事务锁住的表和记录也会不断的上升。

5.优化思路

  1. 设置事务超时时间(不推荐)

TransactionDefinition接口中,可以设置事务运行的超时时间,但是这得依赖于数据库底层的实现,有的数据库不支持该方式,因此不建议使用

  1. 使用Spring声明式事务(推荐)

 使用声明式事务只需要在启动类中加上@EnableTransactionManagement,然后在需要使用事务的方法加上@Transactional注解即可,需要注意的是,在使用@Transactional注解时需要尽可能的指定rollbackFor的异常类型,且使用@Transactional在以下场景中会导致事务失效

  • @Transactional 注解应用到非 public 可见度的方法上
  • 同一个类中方法调用,会导致@Transactional失效
  • 方法内部try-catch异常,事务不会回滚
  • 数据库引擎不支持事务

@Transactional作用于方法上,如果需要在一个方法内部做更细化的事务控制,会比较麻烦,这个时候可以考虑使用编程式事务

  1. 使用TransactionTemplate编程式事务(推荐)

TransactionTemplate的使用非常简单,将我们需要事务控制的方法,放入带execute方法中执行,并将结果返回即可。

@Autowired
TransactionTemplate transactionTemplate;

public Long execute() {
    transactionTemplate.execute((status)-> {
        //do something
        return result;
    })
}

使用TransactionTemplate可以很方便进行细粒度的事务控制,在execute方法内部,事务的提交与回滚都已经封装好,用户只需要关注自己的业务逻辑即可,也不用担心在使用事务时,忘记commit或者rollback而引起的问题

文章来自个人专栏
java
2 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0