5. Transaction
- 默认情况下spring中的事务处理只对RuntimeException方法进行回滚,所以,如果此处将RuntimeException替换成普通的Exception不会产生回滚效果。
-
Spring中的声明式事务是以aop为基础的(对原方法进行前后拦截)
。编程式事务则是使用TransactionTemplate - 在ApplicationContext从BeanFactory中加载所有Bean时,Spring创建被代理的对象(
ApplicationContext非延迟加载,可以在启动时知道bean是否能被加载成功
)。这时候,所有符合在pointcut中配置的类的相关方法就会被织入切面代码,并且返回相关的动态代理对象。实现BeanPostProcessor后,在getBean时,spring会遍历所有已经注册的BeanPostProcessor,调用其postProcessAfterInitialization方法
,在该方法中会寻找已经注册过的事务的增强器。 - 当代理类被调用时会调用这个类的增强方法,也就是bean的Advise,又因为在定义解析事务标签时spring把TransactionInterceptor类型的bean注入到了BeanFactoryTransactionAttributeSourceAdvisor中,所以在调用事务增强器增强的代理类时会首先执行TransactionInterceptor进行增强,同时,也就是在
TransactionInterceptor类中的invoke
方法中完成了整个事务的逻辑(在调用到被@Transactional注释的方法时才会调用到invoke方法)。 - @Transactional注解,如果实现类方法中存在事务属性,则使用实现类方法上的属性,否则使用所在类上的属性,如果实现类方法所在类的属性上还是没有搜索到对应的事务属性,那么再搜索被实现接口中的方法,如果再没有的话,最后尝试搜索被实现接口的接口类上面的声明。spring代码中就是通过反射,按照这样的顺序查找的。
5.1 声明式事务处理流程
在Spring中支持两种事务处理的方式,分别是声明式事务处理与编程式事务处理,两者相对于开发人员来讲差别很大,但是对于Spring中的实现来讲,大同小异。在invoke中我们也可以看到这两种方式的实现。对于声明式的事务处理主要有以下几个步骤。
- 获取事务的属性。
对于事务处理来说,最基础或者说最首要的工作便是获取事务属性了,这是支撑整个事务功能的基石,如果没有事务属性,其他功能也无从谈起。 - 加载配置中配置的TransactionManager。
- 不同的事务处理方式使用不同的逻辑。(编程式的事务处理是不需要有事务属性)
- 在目标方法执行前获取事务(
createTransactionIfNecessary
)并收集事务信息。
事务信息与事务属性并不相同,也就是TransactionInfo与TransactionAttribute并不相同, TransactionInfo中包含TransactionAttribute信息,但是,除了TransactionAttribute外还有其他事务信息,例如PlatformTransactionManager以及TransactionStatus相关信息。 - 执行目标方法。(
这里的执行目标方法,还会去继续调用拦截器链上的其他方法,事务相当于一个around advice
) - 一旦出现异常,尝试异常处理。
并不是所有异常,Spring都会将其回滚,默认只对RuntimeException回滚。
- 提交事务前的事务信息清除。
- 提交事务。
// TransactionInterceptor#invoke -> TransactionAspectSupport#invokeWithinTransaction
// 声明式事务处理
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建TransactionInfo
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal;
try {
// 执行拦截器链中的其他方法,事务相当于一个around advice
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 异常回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 清除信息
cleanupTransactionInfo(txInfo);
}
// 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
5.1.1 创建事务createTransactionIfNecessary
createTransactionIfNecessary整体流程:
- 获取事务,如果当前线程存在事务,则转向嵌套事务处理;
- 按事务传播行为进行区分:
a. PROPAGATION_REQUIRES_NEW表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则在这个方法运行期间被挂起。对于PROPAGATION_REQUIRES_NEW,将原事务挂起并将信息记录在TransactionStatus中,对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复;(通过TransactionStatus(后面被设置进TransactionInfo中)进行回滚
)
b. 对于PROPAGATION_NESTED,则使用JDBC 3.0提供的Savepoints API设置保存点
。被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像PROPAGATION_REQUIRES_NEW。(通过Savepoints进行回滚
)对于嵌入式事务的处理,Spring中主要考虑了两种方式的处理。
i. Spring中允许嵌入事务的时候,则首选设置保存点的方式作为异常处理的回滚。
ii. 对于其他方式,比如JTA无法使用保存点的方式,那么处理方式与PROPAGATION_REQUIRES_NEW相同,而一旦出现异常,则由Spring的事务异常处理机制去完成后续操作。
- 如果不存在则从jdbc获取数据库连接,新建事务,设置事务隔离级别(spring默认的事务隔离级别是跟JDBC相同的,即默认情况Spring不设置事务隔离级别);
- 取消事务自动提交,由spring控制;
- 设置TransactionInfo,这个实例中包含了目标方法开始前的所有状态信息(
将TransactionStatus设置进去
),一旦事务执行失败,spring会通过TransactionInfo中的信息来进行回滚等后续工作。(事务信息记录在当前线程中。绑定在一个theadlocal中)
5.1.2 invocation.proceedWithInvocation()
继续执行拦截器链,当有其他拦截器match待执行方法时,则执行该拦截器方法,然后return。如果没有match则递归调用。
5.1.3 回滚处理completeTransactionAfterThrowing
判断抛出异常的类型,如果是RuntimeException或Error则会回滚。默认情况下Exception不会回滚,数据仍然会被commit;若想要Exception也被回滚,则可以使用@Transaction(rollbackFor=Exception.class);
- 当之前已经保存的事务信息中有保存点信息时,使用保存点Savepoints信息进行回滚。常用于嵌入式事务,对于嵌入式的事务的处理,内嵌的事务异常并不会引起外部事务的回滚。使用JDBC的connection根据Savepoints进行回滚,具体的操作在由使用的数据库连接池封装。回滚后会清除Savepoints;
- 当之前已经保存的事务信息中的事务为新事物,那么直接回滚。常用于单独事务的处理。
对于没有保存点的回滚(有TransactionStatus),Spring同样是使用底层数据库连接提供的API来操作的
。由于我们使用的是DataSourceTransactionManager,那么doRollback函数会使用此类中的实现 - 当前事务信息中表明是存在事务的,又不属于以上两种情况,多数用于JTA,
只做回滚标识,等到提交的时候统一不提交
。 - 进行回滚后信息的清除。(
释放数据库连接,恢复数据库连接的自动提交属性,如果在本事务执行前有事务挂起的,则当前事务执行完毕后会恢复挂起的事务
)
5.1.4 事务提交commitTransactionAfterReturning
- 在真正的数据提交之前,还需要做个判断。在事务异常处理规则的时候,当某个事务既没有保存点又不是新事物,
Spring对它的处理方式只是设置一个回滚标识。这个回滚标识在这里就会派上用场了
,主要的应用场景如下。 - 某个事务是另一个事务的嵌入事务,但是,这些事务又不在Spring的管理范围内,或者无法设置保存点,那么Spring会通过设置回滚标识的方式来禁止提交。首先当某个嵌入事务发生回滚的时候会设置回滚标识,而等到外部事务提交时,一旦判断出当前事务流被设置了回滚标识,则由外部事务来统一进行整体事务的回滚。
- 在提交过程中也并不是直接提交的,而是考虑了诸多的方面,符合提交的条件如下。
只有是新事务才会直接提交(没有设置保存点)
a. 当事务状态中有保存点信息的话便不会去提交事务。
b. 当事务非新事务的时候也不会去执行提交事务操作。 - 此条件主要考虑内嵌事务的情况,对于内嵌事务,在Spring中正常的处理方式是将内嵌事务开始之前设置保存点,一旦内嵌事务出现异常便根据保存点信息进行回滚,但是如果没有出现异常,
内嵌事务并不会单独提交,而是根据事务流由最外层事务负责提交,所以如果当前存在保存点信息便不是最外层事务,不做保存操作
,对于是否是新事务的判断也是基于此考虑。 - 如果程序流通过了事务的层层把关,最后顺利地进入了提交流程,那么同样,
Spring会将事务提交的操作引导至底层数据库连接的API
,进行事务提交。 - 如果提交事务的过程中出现也异常,也会进行回滚。
5.2 Spring事务传播行为
Spring中事务默认的传播行为:REQUIRED
嵌套事务:ServiceA的方法A调用ServiceB的方法B
REQUIRED:A、B无论哪个发生异常,都A、B的操作都将被回滚
REQUIRES_NEW,
内层事务与外层事务就像两个独立的事务一样
,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。如果B异常,A捕获了该异常,则A不会回滚,如果A没有捕获该异常,则A会回滚。A事务异常,B事务是不会回滚的。
a. REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。NESTED,
外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚
,它是一个真正的嵌套事务。DataSourceTransactionManager使用jdbc的savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTATrasactionManager实现可能有不同的支持方式。
a. NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint。嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
5.3 内部方法调用事务不生效
@Slf4j
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Autowired
private UserService userService;
/**
* 方法1 在调用类上添加事务
*/
@Transactional(rollbackFor = Exception.class)
public void testRollback() {
User user = new User();
user.setPassword("password1");
user.setUsername("username1");
userMapper.insert(user);
/**
* 方法2
* @Autowired
* private UserService userService;
* 自己注入自己,事务可以生效
*/
userService.testRollback2();
/**
* 方法3
* 将内部事务暴露出来,事务可以生效
* spring boot启动类上添加注解@EnableAspectJAutoProxy(exposeProxy = true),或使用xml配置
*/
((UserService)AopContext.currentProxy()).testRollback2();
/**
* 直接调用,事务不生效
*/
testRollback2();
}
@Transactional(rollbackFor = Exception.class)
public void testRollback2() {
User user = new User();
user.setPassword("password2");
user.setUsername("username2");
userMapper.insert(user);
throw new RuntimeException("haha");
}
}
在spring内部调用可能会存在事务失效的问题,主要是spring事务是通过代理模式实现的,在一个service里面如果方法A(
未开启事务
)调用方法B(有事务注解@Transactional)时方法B的事务是不起作用的,这种情况是因为方法A未开启事务没有触发代理,在内部调用方法B,this.B()这种调用,也不会触发代理对象去增强B方法,结果就是方法B事务失效。调用A方法时,内部方法B的事务@Transactional不会生效,A的事务会生效;
a. 因为事务基于aop实现,aop是方法级别的增强,在调用A方法时,检测到有Transactional注解,会被拦截器拦截,A方法会被代理,也是就在A方法执行前后会有增强advice,但是B方法没有被代理还是原来的方法,所以Transactional注解不会生效。
b. 但是直接执行B方法,Transactional是会生效的,因为被代理了;
c. 而将类本身注入后,则调用service.B()则可以使用事务;方法1 在调用方法testRollback上添加事务,testRollback2的事务还是不会生效,只是testRollback的事务生效了。username1和username2都会回滚
如果exposeProxy = true,AopContext.currentProxy() 用threadLocal记录当前的代理类
事务回滚后,再次插入记录时会发现mysql注解id已经自增过一次了
使用方法3时,需要添加依赖
org.aspectj
aspectjweaver
1.9.1
- spring自己注入自己时,去缓存中拿到自己,就不会无限调用getBean。