每天进步一点点,大家好,我是大龄码农。
今天我们聊聊事务源码。
在事务和事务传播这两篇文章中,我们使用@Transactional通过实例展示了实际效果,那么今天我们来讲讲它是如何实现的。
大家现在对Spring中的事务写法已经很熟悉了,对比Java中的事务写法,我们发现它缺少了三部分:数据库的连接、DML操作的提交或者回滚、数据库的关闭。但是,它依然完成了事务操作,说明Spring做了一些额外的事情。我们已经学过了AOP,@Transactional就是一个很好的AOP应用。
通过以前的源码分析的文章,想必大家已经对AOP的处理过程比较熟悉了,所以,这里我只列出关于事务处理的部分
注:源码分析可以参考 AOP之全注解源码探查
一、几个重要的类
1、ProxyTransactionManagementConfiguration
基于注解事务,在Spring容器中注册一些基础的Bean
注:在@EnableTransactionManagement注解中,引入了TransactionManagementConfigurationSelector类,通过不同的代理方式指定不同的处理类

说明:继承关系图


说明1:在处理@Bean注解时,会创建对象。@Role(BeanDefinition.ROLE_INFRASTRUCTURE)注解指明这是一个后台的角色,与终端用户无关
说明2:这里面有三个类
BeanFactoryTransactionAttributeSourceAdvisor
TransactionAttributeSource
保存了切入点信息
TransactionInterceptor
拦截器类,用于代理实际的调用方法
2、TransactionInterceptor
这个类是处理@Transactional注解的核心类

说明:上面是继承关系图

说明:调用invoke()方法,实际使用的是其父类的方法,TransactionAspectSupport#invokeWithinTransaction()
3、TransactionAspectSupport 
说明:先来看看事务管理对象是如何获得的

说明1:这是determineTransactionManager()方法中,对于默认事务管理对象的处理方式
说明2:还记得在配置类中定义的JdbcTransactionManager类么

下面再来看看如何获得数据库连接

说明:因为满足if的第2个判断条件,所以进入分支。下面看看createTransactionIfNecessary()方法都做了什么

说明:在getTransaction()方法中,进行了数据库的连接与相关类的初始化操作,最后调用prepareTransactionInfo()方法

说明:又见到了真正干活的doXXX()方法,这里是为了获取数据源信息

说明1:从resources字段获取,TransactionSynchronizationManager类定义了很多ThreadLocal类的属性,后面还会看到。
说明2:使用ThreadLocal类进行线程隔离,对ThreadLocal不熟悉的同学猛戳这里

说明:从名字就能看出,这是对已有事务连接的处理方法

说明:大家眼熟吧,事务传播的处理,这里的判断就不一一说了,还是回到主线,其实最后调用的方法几乎是一样的

说明:这里有两个很重要的步骤
挂起交易

说明:可以看到,TransactionSynchronizationManager类设置了很多属性值,这个就是前面说的它的很多ThreadLocal类的属性

开始一个新的交易

说明:注意newSynchronization这个变量的值,后面很多判断会用到
先来看下doBegin()方法

说明:到这里就不用多说了,看到了Connection的设置
再看下prepareSynchronization()方法

说明:如果是新的同步信息,就对其进行相关的设置
好了,回到TransactionAspectSupport#createTransactionIfNecessary()方法的最后一步,继续调用prepareTransactionInfo()方法

说明:设置当前交易信息的状态

说明:将交易信息绑定到线程上

说明:保存已存在的交易对象,为了当前交易完成后的恢复处理

说明:上面有介绍,这里就不多说了
好了,回到TransactionAspectSupport#invokeWithinTransaction()方法

说明:构造一个try-catch-finally的块,通过环绕通知的方式,继续调用实际的代理方法
先看发生异常的处理

说明:只对设置的异常才会进行回滚操作,即属性rollbackFor设置的值,对于不满足的情况,依然会提交,这里就不截图了
再看finally中的处理


说明:还记得前面绑定线程的处理么,这里就是恢复操作
最后,如果程序没有抛出异常,会执行提交事务的操作

说明:这些都是TransactionAspectSupport类提供的模板方法
这里最后再说下如何关闭连接
AbstractPlatformTransactionManager类实现了事务的提交与回滚方法
commit()--->processCommit()
rollback()--->processRollback()
这两个方法在finally块中都会调用cleanupAfterCompletion()方法

说明:最后的清理工作。将当前事务状态设置为已完成

说明:最后的清理工作。释放之前绑定到当前线程的事务属性

说明:最后的清理工作。重置并释放连接(如果配置的是连接池,就是将连接放回连接池)

说明:最后的清理工作。恢复此前被挂起的资源和事务属性。
4、TransactionManagementConfigUtils
配置一些常量,用于Spring在创建bean时,缓存Map中的key
二、针对配置类的对象创建进行简单的说明

说明:这是AbstractAutowireCapableBeanFactory#createBeanInstance()方法中,比较靠前的一个判断,通过这次配置类的形式,把这个拿出来说一下

说明:以这个为例,此时,beanName是“getDruidDataSource”, mbd.getFactoryMethodName()是“MyDbConfig”。尽管在类的定义中,getDruidDataSource()是MyDbConfig类的一个方法,但是由于@Bean注解的加持,它就是一个独立的对象。在后续的处理中也进行了说明

说明:在ConstructorResolver#instantiateUsingFactoryMethod()方法中处理的
三、关于数据库的信息是如何处理的

说明1:在ConfigurationClassParser#doProcessConfigurationClass()方法中处理的
说明2:如果是使用xml配置的,是通过PropertySourcesPlaceholderConfigurer类处理的

说明1:在ContextNamespaceHandler类注册的
说明2:关于数据库配置文件在注解中是如何填充属性的,或者是在配置文件中如何替换占位符并填充属性的部分,有兴趣的同学,可以自己去看下。
说明3:关注PropertySourcesPropertyResolver类及其父类AbstractPropertyResolver
最后的最后,说下事务失效的几种场景
Bean未被Spring容器管理
加上@Component注解或者在xml文件中配置
数据库不支持事务
比如:MySql中的MyISAM引擎
未开启事务
启用事务注解处理。正确配置事务管理器,注意与数据源的兼容性,同时对于跨多个数据源时,要使用分布式事务管理器
非public修饰的方法或者使用final修饰的方法
cglib动态代理的实现原理
方法内部调用
普通方法调用同类中的一个带有注解的方法,其实内部是使用this进行调用的。最简单的解决方法是将方法拆分到不同的类中
事务方法内启动新线程
不在同一个线程中,不是同一个数据库连接,所以也就不会是同一个事务
事务传播属性问题
使用错误的事务传播属性,比如:Propagation.NEVER
调用方和被调用方的事务传播属性不一致
异常处理不得当
使用try-catch捕获异常后未再次抛出,或者抛出的异常与rollbackFor设置的异常不匹配
AOP代理失效
可能因为存在循环依赖导致,通过延迟依赖注入、手动获取代理对象、使用AspectJ模式、使用编程式事务等方式,来绕过Spring代理的限制
事务隔离级别设置不得当
在并发环境中,当多个线程同时操作共享资源时可能会发生数据的一致性问题。通过适当加锁并尽量减少事务的持续时间等方式,来减少并发操作的影响。
夜雨聆风