Spring Transaction 之 不离不弃生死相依
ShawJie

Spring Transaction 之 不离不弃生死相依

事务是什么?小A排了很长很长一条多米诺骨牌序列,小C拿着相机表示:“只要你排的没有岔子,这一定是一条超酷的素材”。诶,这就是事务,不可分割,结果只有成功或者失败,没有成功了一部分之说,如果小A的骨牌序列是完美的,那小C就能得到他想要的素材,但是只要中间除了定点岔子,小A小C就啥也没有了。

前言

​ 引子的举例可能不是那么的准确,所以还是来看看Wikipedia对事务的定义:

Transaction processing is information processing in computer science that is divided into individual, indivisible operations called transactions. Each transaction must succeed or fail as a complete unit;

事务处理是计算机科学中的信息处理,原子的、不可分割的操作被称作事务,每个完整单元有且只有成功或失败两个状态。

​ 事务的具体操作是数据库层面的内容,本篇内容不会有过多提及,而Spring Transaction更像是一个DB上层的管家,这次主要关注的是基于SpringAOPSpring Transaction如何使我们无感知使用事务的同时肩负起事务的开启、回滚、提交操作。上次已经对Spring AOP的实现有过相关分析,所以也请各位对AOP门道还不是太清晰的小朋友移步**SpringAOP篇**先看看,以获得本篇更愉快的阅读体验。

​ Ps: 本篇内容所有逻辑分析是基于Spring-Boot (v2.2.9.RELEASE) 的somke-test-data-jdbc项目。Git地址: https://github.com/spring-projects/spring-boot.git

北京猿人辈儿的事务实现

​ 在正式开始接触Spring Transaction之前,我们还是得按老规矩,先来看看北京猿人辈儿,纯JDBC时代,是怎么样开启、回滚、提交一个事务的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void updateSomething(Connection con, Coffee coffee, MarketGoods goods) {
// 定义需要执行的语句
String updateCoffeeSql = "UPDATE coffee SET price = ? WHERE type = ?";
String updateMarketSql = "UPDATE market SET sale = sale + ? WHERE coffee_type = ?";
try (
PreparedStatement updateCoffee = new PreparedStatement(updateCoffeeSql);
PreparedStatement updateMarket = new PreparedStatement(updateCoffeeSql)
) {
// 将数据库连接设置为手动提交(即开启事务)
con.setAutoCommit(false);
// 对Sql参数进行赋值
updateCoffee.setBigDecimal(1, coffee.getPrice());
updateCoffee.setString(2, coffee.getString());
// 执行更新操作
updateCoffee.executeUpdate();

...doSomethingOthers();

updateMarket.setInt(1, goods.getSale());
updateMarket.setString(2, goods.getCoffeeType());
updateMarket.executeUpdate();

// 完成所有操作后对进行提交动作
con.commit();
} catch (SQLException e) {
// 执行逻辑失败后对事务进行回滚操作
logger.warn("execute transaction wrong", e);
if (con != null) {
try {
// 回滚操作
con.rollback();
} catch (SQLException excep) {
logger.warn("rollback failed", e);
}
}
}
}

Spring Transaction的具体实现

在上边儿咱们过了一下基于原生JDBC的事务实现逻辑,整体来看还是挺冗长的,从局部的逻辑来看设置为手动提交,以及在执行完成或执行失败时提交和回滚操作都是通用的,我们只需要关系具体的数据操作实现。那么作为事务管家的Spring Transaction是如何完成这部分的工作的呢?

准备阶段:管家报道

​ 咱在前言里稍稍提到了一嘴,说Spring Transaction是基于AOP进行实现的,那么肯定有一个Advice作为事务逻辑的具体执行者。先给大家透个底,具体执行者是一个叫TransactionInterceptorMethodInterceptor类,它是哪来的呢?别急,我们慢慢看。

​ 在“万物”溯源皆可溯到的spring-boot-autoconfigure的项目中,spring.factories文件里我们可以看到两个类配置,TransactionAutoConfigurationDataSourceTransactionManagerAutoConfiguration,在DataSourceTransactionManagerAutoConfiguration自动配置类里,会根据我们应用的数据源,向IoC容器中注册事务管理器DateSourceTransactionManager,而TransactionAutoConfiguration则会在事务管理器完成注册后执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

...

@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
public static class CglibAutoProxyConfiguration {

}

}

​ 根据Spring自动装配的执行逻辑,CglibAutoProxyConfiguration会被认为是符合配置规则的配置类,撇开配置类的定义注解和配置规则的定义注解,那么现在我们能关注点就是在@EnableTransactionManagement这个注解上。嗯?你问我Spring自动装配是什么?去看看这个,虽然挺长的,但或许能解开你那一星半点儿的疑惑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
boolean proxyTargetClass() default false;

AdviceMode mode() default AdviceMode.PROXY;

int order() default Ordered.LOWEST_PRECEDENCE;
}

// TransactionManagementConfigurationSelector
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

​ 这个注解里引入了TransactionManagementConfigurationSelector配置选择类,默认的切面模式是PROXY,在配置选择类中根据切面模式,会加载一个叫ProxyTransactionManagementConfiguration配置类。当然你也可以不使用Spring默认的PROXY模式,转而使用ASPECTJ模式,那么Spring Transaction的切入方式就会由AspectJ进行实现,本篇主要关注的是SpringAOP实现的事务关理,所有关于AspectJ的事务实现就不过多分析了。

​ 在ProxyTransactionManagementConfiguration的配置里,我们终于找到了最初的那个目标TransactionInterceptor,还有另一个小伙伴AnnotationTransactionAttributeSource,它俩相辅相成,但是我们可以晚点再说它。

认领服务对象

​ 找到了事务逻辑的执行者后,我们得知道它是怎么和我们应用内需要事务执行的类做绑定的。上回书说道:

Spring在从缓存里捞出所有的Advice后,会将Advice列表和Bean元信息交由AopUtils#findAdvisorsThatCanApply进行查找筛选并返回可匹配的Advice子集,其内部匹配逻辑由org.aspectj.weaver.patterns.Pointcut#match负责。Pointcut#match方法会判断目标类中的每个方法是否适配切面的要求,只要有一个方法符合要求,则会对目标类进行增强操作。

​ 找到包裹了我们TransactionInterceptorAdviceBeanFactoryTransactionAttributeSourceAdvisor其成员属性pointcut的match()方法。

1
2
3
4
5
@Override
public boolean matches(Method method, Class<?> targetClass) {
TransactionAttributeSource tas = getTransactionAttributeSource();
return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}

​ 在getTransactionAttribute中,会对检查过的方法进行缓存,在后期具体执行事务逻辑时,若缓存中方法被标记为NULL_TRANSACTION_ATTRIBUTE,则会跳过事务开启逻辑。由于篇幅限制,整个getTransactionAttribute方法中,我们主要关注其中的一小段,感兴趣的小朋友可以前往阅读剩余逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
/*
* 由于使用的是Cglib基于子类的增强逻辑,所以无法继承父类的私有方法
* 因此要由Spring Transaction托管的方法也不能是私有方法
* Ps: 至于为啥不能是protected和package-private等级的方法
* 大概是SpringAOP的cglib实现要和jdkproxy一致吧
*/
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}

/*
* 方法的定义及实现可能在接口上,实现类并没有给出方法的重写实现
* 因此需要从定义方法的目标类上重新获得方法,
* E.g ICustomerService.saveCustomer() 方法有default实现,
* 但是CustomerServiceImpl并未给出saveCustomer()的重写实现
*/
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

// 在特殊方法上查找是否持有@Transactional注解属性(如果没有特殊方法则会在当前方法上查找
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}

// 在特殊方法的目标类上查找@Transactional的注解属性
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}

if (specificMethod != method) {
// 回退到原方法上查找@Transactional注解属性
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// 回退到原方法定义类上查找@Transactional注解属性
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}

return null;
}

​ 在computeTransactionAttribute方法中,我们可以看到Spring会在目标类上搜索@Transactional注解,若注解存在,则会将配置信息在内部解析器中构建为RuleBasedTransactionAttribute对象并缓存进attributeCache对象中。而底层使用的则是java.lang.reflect反射获取类注解信息,当Spring查找到类/方法上标记了@Transactional注解时,就会将事务拦截器添加进目标类的切面链中,以进行后续的事务包装操作。

一点点小尾巴ノ)゚Д゚( 

  在SpringFramwork2.0.x版本的官方文档中有过这么一段描述:"The Spring team's recommendation is that you only annotate concrete classes with the @Transactional annotation, as opposed to annotating interfaces. You certainly can place the @Transactional annotation on an interface (or an interface method), but this will only work as you would expect it to if you are using interface-based proxies. The fact that annotations are not inherited means that if you are using class-based proxies then the transaction settings will not be recognised by the class-based proxying infrastructure and the object will not be wrapped in a transactional proxy (which would be decidedly bad)",简单翻译一下就是:“Spring团队建议将@Transactional注解标记在具体的实现类而非接口上,虽然注解可以被标记在接口或接口方法上,但只在其为基于接口代理时才生效(即JDKProxy),由于注解的不可继承性,在使用基于类代理时(即CglibProxy)无法识别事务设置,以至于对象不会被事务管理器所管理”。这也是现在网络上现存的大部分博客对@Transactional注解进行提要时,会有“不建议在接口上进行标记”的原因所在。

  但是依据阿消(指自己)的测试和源码逻辑阅读,在SpringBoot2.2.9.RELEASE(SpringFramework5.2.8.RELEASE)版本中,Spring的事务托管并不受代理实现是Cglib或是JDKProxy的影响,之和能否通过getTransactionAttribute()在目标方法上获取到@Transactional注解元信息有关。由于目前Github上SpringFramework的源码版本最早只能追溯到3.0.x版本,所以依然无法再了解当时SpringTransaction的实现,有了解的朋友也欢迎分享一下。虽然确实依旧还是不建议把@Transactional注解标记在接口上(因为接口只是抽象,而是否需要事务是实现应该关心的),但是原因确实和是基于类的代理实现还是基于接口的代理实现没关系了。(在SpringFramwork5的文档上已经找不到相关描述了,当然如果是我的测试步骤有问题也希望大家指正)

事务执行:管家干活

在上一小节,我们知道了SpringTransaction的事务管理器是怎么来的,以及它是怎么找到它需要服务的对象的。对象都找到了,那么作为资本家(bushi)就需要唆使打工人TransactionManager开始干活了。由于前文关于SpringAOP的流程分析是基于Cglib增强的,所以这部分也会按照Cglib的流程走,本篇可能会有较多的源码内容,也请大家耐心阅读。JDKProxy的流程在大体上是相似的,所以也不再赘述了。

​ 在Spring应用启动完成之后,IoC容器中存放着我们被增强后的类实例,而我们需要以事务方式执行的类或者方法,则会执行时被拦截器中的TransactionInterceptor事务拦截器截获,进行Spring给我们抽象好的事务包装操作。让我来看看TransactionInterceptor方法。

1
2
3
4
5
6
7
8
9
10
11
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取到被拦截的方法的目标类
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

/*
* 调用父类TransactionAspectSupport的invokeWithinTransaction方法
* 而在完成事务执行的需求判断和事务开启逻辑后,回调用invocation#proceed()方法
* 以进行方法后续的逻辑运行,并等待方法的正常或异常返回,进行后续的事务提交/回滚处理
*/
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

TransactionInterceptor中的逻辑很简单,主要的核心方法就是TransactionAspectSupport#invokeWithinTransaction,该方法内容比较多,整体挪过来也比较占篇幅,所以我会把部分我觉得重要的点提溜出来,剩余一些辅助性质的逻辑感兴趣的小朋友也可以自己进到方法里去看一看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

/*
* 还记得我们之前提到过会对方法的@Transactional注解信息进行缓存的TransactionAttributeSource么
* 在这会根据方法和之前在TransactionInterceptor的invoke方法中获取到的目标类进行缓存的读取
* 因为在增强逻辑判断中只要有一个方法符合事务的注入要求,就会对类进行增强
* 所以这里也会对剩余方法进行事务执行必要的判断并缓存
*/
TransactionAttributeSource tas = getTransactionAttributeSource();
/*
* 方法之前被扫描过,则会直接从缓存中读取注解信息,若方法被标记为NULL_TRANSACTION_ATTRIBUTE则会于此返回空
* 反之则会在这个节点进行一次注解的扫描动作,并缓存返回
*/
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
/*
* 若方法有@Tranactional注解信息,并且配置了需求的事务管理器名称
* 则会在IoC容器中查找指定的事务管理器,反之则会在Ioc容器中查找TransactionManager对象
* 若只存在一个,则配置为默认事务管理器并返回
* 若存在多个,则需要指定一个为主要事务管理器,若不指定会直接返回空
*/
final TransactionManager tm = determineTransactionManager(txAttr);

/*
* 响应式事务的实现...
* 不是我们关注的重点...这次先略过
*/

// 将transactionManager做一个中心接口的转换,保证多种类型的事务管理器执行流程是统一的
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
// 获取且入方法的限定名,作为之后创建事务的默认名称
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// 进行具体的事务创建逻辑,若事务属性为空,则不会进行事务的创建执行操作
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// 因为连接器是链式操作,所以于此调用进行方法的后续流程执行并等待之后的流程返回
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 当方法后续流程执行出错时,则会完结当前事务,并进行一些一场事务处理逻辑,譬如回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 重置事务信息(涉及嵌套事务逻辑)
cleanupTransactionInfo(txInfo);
}
// 当事务正常返回时进行一些后续操作,譬如提交
commitTransactionAfterReturning(txInfo);
// 返回流程的处理结果
return retVal;
} else {
/*
* CallbackPreferringPlatformTransactionManager事务管理器
* 通过事务流程中的回调对事务进行提交回滚等操作
* 不是本文关心重点 于此略过
*/
}
}

​ 整个事务的执行流程我们大致的先过了一遍,流程不是很复杂,相关的方法我都补充了注释,从注释当中我们能拎出几个核心方法:

  • TransactionAspectSupport#createTransactionIfNecessary创建事务逻辑
  • TransactionAspectSupport#completeTransactionAfterThrowing 事务异常流程处理逻辑
  • TransactionAspectSupport#commitTransactionAfterReturning 事务正常流程处理逻辑

​ 我们就按顺序一个一个往下看。

创建事务 - 起个好头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// TransactionAspectSupport#createTransactionIfNecessary
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
// 如果在@Transactional注解上没有指定事务的名称,则会使用方法限定名作为事务名称
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
/*
* 当存在方法的事务相关标记时,并且应用内存在事务管理器则会通过
* PlatformTransactionManager#getTransaction进行事务示例的创建获取
*/
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
// 将事务信息封装进transactionInfo对象并与先当前线程绑定
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

​ 当方法不存在事务属性配置时,根据运行逻辑,不会进行相关的事务创建,而事务的创建过程getTransaction,则被抽象到了AbstractPlatformTransactionManager类上,该方法中会获取方法事务配置的事务传播等级、隔离等级,是否只读、过期时间等配置,而后会根据事务的传播等级进行相关的逻辑操作,譬如PROPAGATION_MANDATORY等级会在没有事务存在时抛出异常。具体每个传播等级所对应执行的逻辑操作这次就不细说了,我们主要来看看其中的startTranaction以及其中的doBegin逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// AbstractPlatformTransactionManager#startTransaction
private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
/*
* 获取当前事务管理器是否是同步执行的,如果是,则会在后续的prepareSynchronization方法中
* 将事务属性绑定进当前线程中
*/
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 具体的事务开启逻辑
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}

/*
* 我们就来看看somke-test-data-jdbc项目中默认的TransactionManager
* DataSourceTransactionManager的事务开启流程是如何操作的
* DataSourceTransactionManager#doBegin
*/
protected void doBegin(Object transaction, TransactionDefinition definition) {
// 获取事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;

try {
// 尝试从事务对象中获取数据库连接,若不存在连接,则会创建一个新的连接并绑定到事务上
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

// 标记为同步执行事务
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
// 获取连接
con = txObject.getConnectionHolder().getConnection();

// 对连接进行一些前缀配置,如设置连接只读,设置连接隔离等级之类的操作
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
// 将隔离等级、是否只读信息复写回事务上
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());

/*
* 若数据库连接的配置为自动提交,则于此改成手动提交以开启事务
* 至于为什么不直接修改而是要加一层判断,Spring团队的说法是将连接设置为手动提交
* 对于JDBC驱动来说是非常重的操作,他们希望在不必要的时候不进行该设置
*/
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
// 设置连接开启事务
con.setAutoCommit(false);
}

/*
* 若事务为只读事务,且事务管理器配置类显示指定只读
* 则会于此开启一个会话并执行 'SET TRANSACTION READ ONLY' 显示设置事务只读
*/
prepareTransactionalConnection(con, definition);
// 标记事务为开启状态
txObject.getConnectionHolder().setTransactionActive(true);

// 设置事务过期时间
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// 若事务为新开启的事务,则将连接,数据源信息绑定到当前线程中
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}

catch (Throwable ex) {
// 事务开启异常时对连接进行释放
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}

​ 在doBegin方法中,我们最初想要找的,在内容开头写的那个“北京猿人辈儿”的事务开启逻辑,已经出现了。还有一个小重点,SpringTransaction中对数据源、连接、事务信息、方法切入点等信息绑定到线程的操作,调用的TransactionSynchronizationManager逻辑,都是通过ThreadLocal对象进行实现的,这也就是在标记为事务的方法上通过异步逻辑调用其它事务方法,会导致事务失效的原因。ThreadLocal对象的分析我们之后有机会再说,看到这个点的时候,或许…可以考虑一下在多线程执行事务方法时,如何保证事务不失效的逻辑。

回滚事务 - 诶呀,出错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// TransactionAspectSupport#completeTransactionAfterThrowing
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
// 获取方法事务配置并判断异常是否需要回滚
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// 进行具体回滚操作
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (Exception ex2) {
// ...
}
}
else {
// 若不需要根据配置异常回滚,则仍会进行提交操作
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (Exception ex2) {
// ...
}
}
}
}

/*
* 在执行回滚操作时,spring会对状态进行一系列检测,譬如事务状态是否为已完成,事务是否有保存点等等
* 执行完成回滚操作后,spring会将绑定在线程上的事务信息进行重置操作,我们来看看回滚的具体操作逻辑
* DataSourceTransactionManager#doRollback
*/
protected void doRollback(DefaultTransactionStatus status) {
// 获取事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
// 获取连接
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}
try {
// 执行熟悉的rollback操作
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
}
}

​ 在事务的回滚逻辑中还有一个点需要注意,即TransactionAttribute#rollbackOn方法的判定是否需要回滚,由于SpringTransactionAnnotationParser默认构建的是RuleBasedTransactionAttribute,在解析时会将rollbackForrollbackForClassName封装成RollbackRuleAttribute对象,将noRollbackFornoRollbackForClassName封装成NoRollbackRuleAttribute对象并塞进rules列表中,进行后续的回滚判断操作依据,所以我们就来看看它的回滚逻辑是怎样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// RuleBasedTransactionAttribute#rollbackOn
public boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;

if (this.rollbackRules != null) {
// 获取所有的回滚规则
for (RollbackRuleAttribute rule : this.rollbackRules) {
// 比对回滚规则并获取深度
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
// 将深度最小且匹配的异常作为优选者
deepest = depth;
winner = rule;
}
}
}

/*
* 当没有异常匹配时,则进行默认的回滚判断
* 即若异常是RuntimeException或是Error则进行回滚操作
*/
if (winner == null) {
return super.rollbackOn(ex);
}

// 若最终优选者是不需要回滚的异常类型,则不进行回滚操作,反之则进行回滚操作
return !(winner instanceof NoRollbackRuleAttribute);
}

​ 从以上逻辑能获取几个信息:

  1. 所有的RuntimeException和Error都会进行回滚,除非RuntimeException被配置进了noRollbackFor列表中
  2. 回采取优胜者回滚模式进行回滚,譬如子类异常配置为不回滚,父类异常配置为回滚,因为子类异常深度更小,所以会采取子类异常的不回滚操作

提交事务 - 事情做完惹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// TransactionAspectSupport#commitTransactionAfterReturning
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
// 事务管理器的提交逻辑
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}

/*
* 执行提交操作的前置逻辑倒是不算复杂,会释放事务的保存点,然后进行具体的提交操作
* 在提交完成后,会重置绑定在线程上的事务对象属性,但若在提交时出现RuntimeException或Error
* 则仍会进行回滚操作
* DataSourceTransactionManager#doCommit
*/
protected void doCommit(DefaultTransactionStatus status) {
// 获取事务信息
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
// 获取连接
Connection con = txObject.getConnectionHolder().getConnection();
try {
// 执行具体提交操作
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}

尾巴

​ 至此,我大概是把SpringTransaction讲清楚了吧,从头看到现在的你辛苦了,也希望你通过这个流程,知道了Spring事务在执行过程中为什么会失效,譬如没有作为SpringBeanIoC容器所管理的Bean不会被进行增强;在本类中进行this.xxx方法调用也不会触发事务逻辑,因为使用的是本类对象而不是增强对象;@Transactional注解被打在了非Public方法上,因为Cglib不会对非Public方法进行增强操作;异常被你用try/catch捕捉了导致没有触发回滚,因为TransactionInterceptor是在最外层进行的catch中进行的回滚逻辑判断。

​ 总之,Spring是个怪兽,我不期望我有一天会把它理解的透彻,只希望在哪天碰到了什么问题,我会知道问题出在哪里,我该怎么解决。

​ Ps: 最近挺忙的,但是总算还是把这篇内容写完了,很长,我也知道能真真儿的读完实属不易,这篇内容写完我就要准备回家过年的事情了,也不知道有多少人会留在工作地过年…无论如何,新年快乐,还有啊…祝你在新的一年里,有人能与你共悲欢。我是阿消,新年快乐。