声明式事务和编程式事务对比:
- 声明式事务:
- 使用注解或XML配置的方式,在代码中声明事务的属性和行为。
- 通过AOP和代理模式实现,将事务管理与业务逻辑代码解耦。
- 适用于大多数情况,简化了代码,提高了可维护性和可读性。
- 常用的注解是@Transactional,可以应用在方法或类级别。
- 编程式事务:
- 在代码中显式编写事务管理的代码逻辑。
- 需要手动控制事务的开启、提交和回滚等操作。
- 适用于需要更细粒度控制事务的情况,或者在声明式事务不适用的情况下使用。
- 可以使用编程式事务的API,如Spring的TransactionTemplate或JDBC的事务API。
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的。对于纯 JDBC 操作数据库,想要用到事务,可以按照以下步骤进行(编程式事务同理)
1、获取连接 Connection con = DriverManager.getConnection()
2、开启事务 con.setAutoCommit(true/false);
3、执行 CRUD
4、提交事务/回滚事务 con.commit() / con.rollback();
5、关闭连接 conn.close();
当使用Spring的声明式事务后,我们可以只要写CRUD即可,其他由Spirng 自动完成。
本文主要讲解声明式事务
生效原理
下面是一个简单的示例,用于说明 Spring 事务的生效原理:
@Service
public class MyService {
@Transactional
public void transferMoney(Account sourceAccount, Account targetAccount, double amount) {
//省略逻辑处理
}
}
声明式事务就是使用我们常见的@Transactional注解完成的,声明式事务优点就在于让事务代码与业务代码解耦,通过Spring中提供的声明式事务使用,我们也可以发觉我们只需要编写业务代码即可,而事务的管理基本不需要我们操心,Spring就像使用了魔法一样,帮我们自动完成了。
之所以那么神奇,本质还是依靠Spring框架提供的Bean生命周期相关回调接口和AOP结合完成的,简述如下:
这里思考下:如何保证同个线程请求时数据库多个操作使用的是同个Connection?
- 通过自动代理创建器依次尝试为每个放入容器中的bean尝试进行代理
- 尝试进行代理的过程对于事务管理来说,就是利用事务管理涉及到的增强器advisor,即TransactionAttributeSourceAdvisor
- 判断当前增强器是否能够应用与当前bean上,怎么判断呢? —> advisor内部的pointCut喽 !
- 如果能够应用,那么好,为当前bean创建代理对象返回,并且往代理对象内部添加一个TransactionInterceptor拦截器。
- 此时我们再从容器中获取,拿到的就是代理对象了,当我们调用代理对象的方法时,首先要经过代理对象内部拦截器链的处理,处理完后,最终才会调用被代理对象的方法。(这里其实就是责任链模式的应用)
- 当TransactionInterceptor的invoke()方法被执行时,它会先开启一个事务,然后再调用目标方法。如果目标方法执行成功,那么就会提交事务,否则就会回滚事务。
- 在整个事务过程中,
Spring会保证操作数据库时使用的是同一个连接。它是通过将一个数据库连接与一个ThreadLocal对象绑定起来,然后在需要操作数据库时通过该ThreadLocal对象来获取连接的。这样就可以避免多个连接导致的数据不一致问题。
这里说下SpringBoot底层实现有一个AutoProxyRegistrar,简单来说,AutoProxyRegistrar顾名思义就是开启了AOP代理 ProxyTransactionManagementConfiguration是一个配置类,它又定义了另外三个bean:
- BeanFactoryTransactionAttributeSourceAdvisor:一个Advisor就是一个切面
- AnnotationTransactionAttributeSource:相当于Pointcut
- TransactionInterceptor:相当于中的 Advice,事务的拦截方法
AnnotationTransactionAttributeSource就是用来判断某个类上是否存在@Transactional注解, 或者判断某个方法上是否存在@Transactional注解的。
TransactionInterceptor就是代理逻辑,当某个类中存在@Transactional注解时,到时就产生一个 代理对象作为Bean,代理对象在执行某个方法时,最终就会进入到TransactionInterceptor的 invoke()方法。
对于被事务增强器TransactionAttributeSourceAdvisor代理的bean而言,代理对象内部会存在一个TransactionInterceptor,该拦截器内部构造了一个事务执行的模板流程:
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
//TransactionAttributeSource内部保存着当前类某个方法对应的TransactionAttribute---事务属性源
//可以看做是一个存放TransactionAttribute与method方法映射的池子
TransactionAttributeSource tas = getTransactionAttributeSource();
//获取当前事务方法对应的TransactionAttribute
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
//定位TransactionManager
final TransactionManager tm = determineTransactionManager(txAttr);
.....
//类型转换为局部事务管理器
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
//TransactionManager根据TransactionAttribute创建事务后返回
//TransactionInfo封装了当前事务的信息--包括TransactionStatus
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
//继续执行过滤器链---过滤链最终会调用目标方法
//因此可以理解为这里是调用目标方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
//目标方法抛出异常则进行判断是否需要回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
//清除当前事务信息
cleanupTransactionInfo(txInfo);
}
...
//正常返回,那么就正常提交事务呗(当然还是需要判断TransactionStatus状态先)
commitTransactionAfterReturning(txInfo);
return retVal;
}
...
事务拦截器(TransactionInterceptor)的处理过程总结如下:
-
方法调用前的处理:事务拦截器在目标方法调用前执行一些预处理操作。这包括检查当前是否存在事务上下文(TransactionContext),如果不存在则创建新的事务上下文。事务上下文包含了事务的相关信息,如事务的传播行为、隔离级别、超时设置等。事务拦截器还会根据事务定义的属性决定是否开启新的事务或者加入已有的事务。
-
方法调用:事务拦截器将目标方法调用传递给实际的目标对象,并执行目标方法。
-
方法调用后的处理:在目标方法执行完成后,事务拦截器会根据方法的执行结果决定是提交事务还是回滚事务。如果方法执行过程中发生了异常,事务拦截器会触发事务回滚,将事务恢复到调用该方法之前的状态。如果方法执行成功,事务拦截器会触发事务提交,将事务的操作永久保存到数据库。
事务拦截器通过使用事务管理器(TransactionManager) 来协调和控制事务的开始、提交和回滚操作。事务管理器负责和底层的数据访问框架(如JDBC、Hibernate)进行交互,确保事务的一致性和隔离性。
事务拦截器是通过配置切面(Aspect)和切入点(Pointcut) 的方式与目标方法进行关联。切面定义了在何时何地应用事务拦截器,而切入点则定义了哪些方法会被事务拦截器拦截。通过配置切面和切入点,可以灵活地控制事务的应用范围。