1.问题描述
系统在运行过程中,时不时就出现线程获取事务锁超时的异常,通过观察日志发现,超时的sql都涉及到几条特定数据的改动,初步怀疑是这数据被其他事务加上了行锁,且长时间不释放
2.排查过程
通过查询 information_schema 库中 INNODB_TRX (记录当前mysql运行中的事务)表发现,有一个从昨晚到今早都还运行的事务,该事务锁了14张表和110行记录。
根据这个现象,怀疑是程序中开启了事务,但是却没有提交引起。通过搜索发现可疑代码
结合日志发现,在问题事务运行前,确实抛出过名称存在的异常。基本可以确定是该代码引起的。
3.解决问题
将通过 DataSourceTransactionManager 获取获取事务的代码下移到,抛出业务异常代码之后。并在finally里执行commit或者rollback操作。
改完重新发布测试,问题解决。
4.原理分析
通过分析问题产生的原理,能有效的帮助我们加深这一块的相关知识,避免之后再犯相关的错误。简单描述下上述用到的三个事务相关的类的作用:
了解这三个类的作用,对了解Spring事务有极大的帮助,从这三个类入口,我们深入了解下产生这次事故的深层次原理,当通过 DataSourceTransactionManager 管理事务时,有以下主要步骤:
通过上述执行流程的分析,明确了问题产生的原因: 线程第一次执行时,在开启事务后(使用 ThreadLocal 完成 ConnectionHolder 与当前线程的绑定),由于业务异常退出,没有执行 commit 或者 rollback 操作。由于使用的是线程池,线程执行完不会销毁,因此 ThreadLocal 中的值不会被清除,当线程第二次获取事务(从 ThreadLocal 中获取)时,由于使用的 事务传播机制 为 REQUIRED ,所以不会产生新的事务,此时线程执行 commit 或者 rollback 操作,都不会使 Connection 真正的 commit 或者 rollback。 随着该线程不断的执行业务逻辑,该事务锁住的表和记录也会不断的上升。
5.优化思路
TransactionDefinition 接口中,可以设置事务运行的超时时间,但是这得依赖于数据库底层的实现,有的数据库不支持该方式,因此不建议使用
使用声明式事务只需要在启动类中加上 @EnableTransactionManagement, 然后在需要使用事务的方法加上 @Transactional 注解即可,需要注意的是,在使用 @Transactional 注解时需要尽可能的指定 rollbackFor 的异常类型,且使用 @Transactional 在以下场景中会导致事务失效 ,
@Transactional 作用于方法上,如果需要在一个方法内部做更细化的事务控制,会比较麻烦,这个时候可以考虑使用编程式事务