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

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

微服务
2022-11-25 10:22:43
408
0

1.问题描述

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

2.排查过程

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

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

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

3.解决问题

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

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

4.原理分析

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

  • DataSourceTransactionManager 事务管理器,负责管理事务的行为,比如开始,提交,回滚等操作
  • TransactionDefinition 是对事务属性的相关定义,比如设置事务的隔离传播机制,超时时间等。
  • TransactionStatus 表示一个事务的状态,接口提供了控制事务执行和查询事务状态的方法, 比如当前调用栈中之前已经存在了一个事物,那么就是通过该接口来判断的, TransactionStatus 接口可以让事务管理器 DataSourceTransactionManager 控制事务的执行。
  • 了解这三个类的作用,对了解Spring事务有极大的帮助,从这三个类入口,我们深入了解下产生这次事故的深层次原理,当通过 DataSourceTransactionManager 管理事务时,有以下主要步骤:

  • doGetTransaction
  • 获取一个 DataSourceTransactionObject 对象,其中持有 ConnectionHolder (从 ThreadLocal 中获取,可能已经存在这个对象)
  • isExistingTransaction
  • 判断获取到的 DataSourceTransactionObject 对象是否持有 ConnectionHolder, ConnectionHolder 中的连接是否活跃
  • 如果当前线程已经持有了 ConnectionHolder ,则根据不同的传播行为做不同的逻辑
  • 不管是 commit 方法或者是 rollback 方法,在 finally 中都会执行 cleanupAfterCompletion 方法
  • 只有符合 isNewTransaction 条件时,才会执行资源的解绑(将 ConnectionHolder 与当前线程解绑)
  • 通过上述执行流程的分析,明确了问题产生的原因: 线程第一次执行时,在开启事务后(使用 ThreadLocal 完成 ConnectionHolder 与当前线程的绑定),由于业务异常退出,没有执行 commit 或者 rollback 操作。由于使用的是线程池,线程执行完不会销毁,因此 ThreadLocal 中的值不会被清除,当线程第二次获取事务(从 ThreadLocal 中获取)时,由于使用的 事务传播机制 REQUIRED ,所以不会产生新的事务,此时线程执行 commit 或者 rollback 操作,都不会使 Connection 真正的 commit 或者 rollback。 随着该线程不断的执行业务逻辑,该事务锁住的表和记录也会不断的上升。

    5.优化思路

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

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

  • @Transactional 注解应用到非 public 可见度的方法上
  • 同一个类中方法调用,会导致 @Transactional 失效
  • 方法内部 try-catch 异常,事务不会回滚
  • 数据库引擎不支持事务
  • @Transactional 作用于方法上,如果需要在一个方法内部做更细化的事务控制,会比较麻烦,这个时候可以考虑使用编程式事务

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