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,则根据不同的传播行为做不同的逻辑
- doBegin
- 从数据源中获取一个Connection,将Connection绑定到ConnectionHolder上,已被嵌套的事务获取
- 设置事务的隔离级别,并开启事务
- 使用ThreadLocal将当前线程与ConnectionHolder绑定起来
- doCommit
- 在执行doCommit前,会判断当前的事务是否是新开的事务,只有是新的事务,才会执行Connection的commit操作
- doRollback
- 只有符合isNewTransaction条件时,才会执行Connection的rollback操作
- cleanupAfterCompletion
- 不管是commit方法或者是rollback方法,在finally中都会执行cleanupAfterCompletion方法
- 只有符合isNewTransaction条件时,才会执行资源的解绑(将ConnectionHolder与当前线程解绑)
7.总结
通过上述执行流程的分析,明确了问题产生的原因: 线程第一次执行时,在开启事务后(使用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编程式事务(推荐)
TransactionTemplate的使用非常简单,将我们需要事务控制的方法,放入带execute方法中执行,并将结果返回即可。
@Autowired
TransactionTemplate transactionTemplate;
public Long execute() {
transactionTemplate.execute((status)-> {
//do something
return result;
})
}
使用TransactionTemplate可以很方便进行细粒度的事务控制,在execute方法内部,事务的提交与回滚都已经封装好,用户只需要关注自己的业务逻辑即可,也不用担心在使用事务时,忘记commit或者rollback而引起的问题