目录
1.2.1 @EnableTransactionManagement工作原理
1.3.1.2 方式二:DataSourceTransactionManager
1.3.1.3 方式三:DataSourceTransactionManager+TransactionDefinition
1.3.1.4 方式四:TransactionTemplate
1.3.2 TransactionSynchronizationManager
1.6.1 @Transactional非public修饰的方法上
1.6.3 @Transactional 注解属性propagation设置错误
1.6.4 @Transactional 注解属性 rollbackFor设置错误
1.6.5 同一个类中方法调用导致@Transactional失效
1.7 @Transaction和Mybatis-Plus的@DS问题
1 Spring事务
1.1 定义
1.1.1 事务概念
事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。
如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。
由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。
1.1.2 事务分类
Spring 支持编程式事务
管理和声明式
事务管理两种方式
-
编程式事务
编程式事务管理使用 TransactionTemplate,需要显式执行事务,比如,需要显示调用commit或者rollback方法 -
声明式事务
声明式事务管理建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
优点是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过 @Transactional 注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
1.2 声明式事务
1.2.1 @EnableTransactionManagement工作原理
开启Spring事务本质上就是增加了一个Advisor,但我们使用@EnableTransactionManagement注解来开启Spring事务是,该注解代理的功能就是向Spring容器中添加了两个Bean:
- AutoProxyRegistrar
- ProxyTransactionManagementConfiguration
AutoProxyRegistrar主要的作用是向Spring容器中注册了一个InfrastructureAdvisorAutoProxyCreator的Bean。而InfrastructureAdvisorAutoProxyCreator继承了AbstractAdvisorAutoProxyCreator,所以这个类的主要作用就是开启自动代理的作用,也就是一个BeanPostProcessor,会在初始化后步骤中去寻找Advisor类型的Bean,并判断当前某个Bean是否有匹配的Advisor,是否需要利用动态代理产生一个代理对象。
ProxyTransactionManagementConfiguration是一个配置类,它又定义了另外三个bean:
- BeanFactoryTransactionAttributeSourceAdvisor:一个Advisor
- AnnotationTransactionAttributeSource:相当于BeanFactoryTransactionAttributeSourceAdvisor中的Pointcut
- TransactionInterceptor:相当于BeanFactoryTransactionAttributeSourceAdvisor中的Advice
AnnotationTransactionAttributeSource就是用来判断某个类上是否存在@Transactional注解,或者判断某个方法上是否存在@Transactional注解的。
TransactionInterceptor就是代理逻辑,当某个类中存在@Transactional注解时,到时就产生一个代理对象作为Bean,代理对象在执行某个方法时,最终就会进入到TransactionInterceptor的invoke()方法
1.2.2 实现原理
声明式事务实现原理就是通过AOP/动态代理。
-
在Bean初始化阶段创建代理对象:Spring容器在初始化每个单例bean的时候,会遍历容器中的所有BeanPostProcessor实现类,并执行其postProcessAfterInitialization方法,在执行AbstractAutoProxyCreator类的postProcessAfterInitialization方法时会遍历容器中所有的切面,查找与当前实例化bean匹配的切面,这里会获取事务属性切面,查找@Transactional注解及其属性值,然后根据得到的切面创建一个代理对象,默认是使用JDK动态代理创建代理,如果目标类是接口,则使用JDK动态代理,否则使用Cglib。
一个Bean在执行Bean的创建生命周期时,会经过InfrastructureAdvisorAutoProxyCreator的初始化后的方法,会判断当前Bean对象是否和BeanFactoryTransactionAttributeSourceAdvisor匹配,匹配逻辑为判断该Bean的类上是否存在@Transactional注解,或者类中的某个方法上是否存在@Transactional注解,如果存在则表示该Bean需要进行动态代理产生一个代理对象作为Bean对象 -
在执行目标方法时进行事务增强操作:当通过代理对象调用Bean方法的时候,会触发对应的AOP增强拦截器,声明式事务是一种环绕增强,对应接口为
MethodInterceptor
,事务增强对该接口的实现为TransactionInterceptor
,类图如下
事务拦截器TransactionInterceptor
在invoke
方法中,通过调用父类TransactionAspectSupport
的invokeWithinTransaction
方法进行事务处理,包括开启事务、事务提交、异常回滚
该代理对象在执行某个方法时,会再次判断当前执行的方法是否和BeanFactoryTransactionAttributeSourceAdvisor匹配,如果匹配则执行该Advisor中的TransactionInterceptor的invoke()方法,执行基本流程为:
- 利用所配置的PlatformTransactionManager事务管理器新建一个数据库连接
- 修改数据库连接的autocommit为false
- 执行MethodInvocation.proceed()方法,简单理解就是执行业务方法,其中就会执行sql
- 如果没有抛异常,则提交
- 如果抛了异常,则回滚
1.2.3 Spring事务执行流程图
https://www.processon.com/view/link/5fab6edf1e0853569633cc06
接上图
1.2.4 SpringBoot中不需要声明
在springboot中使用声明式事务时,不需要使用@EnableTransactionManagement就会生效
springboot在启动的时候会自动装配很多配置类,那根据上面的问题,我们自然而然就想到是springboot在启动的时候就装配了相关Transactional 的配置类了。
在spring.factories文件中已经装配了事务相关信息了
1.3 编程式事务
由于声明式事务并不能解决 多线程环境下也能保持事务一致性 的问题,那么就只能求助于编程式事务了。
1.3.1 编程式事务使用方式
1.3.1.1 方式一:DataSource
//如果是多数据源的情况下,需要指定具体是哪一个数据源
@Autowired
private final DataSource dataSource;
public void test() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
DefaultTransactionDefinition transactionDef = new DefaultTransactionDefinition();
transactionDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transaction=transactionManager.getTransaction(transactionDef);
try{
//todo 业务代码
transactionManager.commit(transaction);
}catch(){ //手动回滚
transactionManager.rollback(transaction);
}
}
任务正常都执行完毕,事务进行提交,但是会抛出异常,导致事务回滚:,这里需要再次回顾一下Spring事务实现的小细节:
一次事务的完成通常都是默认在当前线程内完成的,又因为一次事务的执行过程中,涉及到对当前数据库连接Connection的操作,因此为了避免将Connection在事务执行过程中来回传递,我们可以将Connection绑定到当前事务执行线程对应的ThreadLocalMap内部,顺便还可以将一些其他属性也放入其中进行保存,在Spring中,负责保存这些ThreadLocal属性的实现类由
TransactionSynchronizationManager
承担。
1.3.1.2 方式二:DataSourceTransactionManager
使用DataSourceTransactionManager 来注入管理
@Autowired
private DataSourceTransactionManager transactionManager;
public void test(){
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transaction = transactionManager.getTransaction(defaultTransactionDefinition);
try{
//todo 业务代码
transactionManager.commit(transaction);
}catch(){ //手动回滚
transactionManager.rollback(transaction);
}
}
1.3.1.3 方式三:DataSourceTransactionManager+TransactionDefinition
使用 PlatformTransactionManager
这个接口中定义了三个方法 getTransaction
创建事务,commit 提交事务,rollback 回滚事务。它的实现类是 AbstractPlatformTransactionManager
。
@Autowired
DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
TransactionDefinition transactionDefinition;
// 手动创建事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
// 手动提交事务
dataSourceTransactionManager.commit(transactionStatus);
// 手动回滚事务。(最好是放在catch 里面,防止程序异常而事务一直卡在哪里未提交)
dataSourceTransactionManager.rollback(transactionStatus);
1.3.1.4 方式四:TransactionTemplate
如果不想手动的commit或者rollback可以用spring推荐的TransactionTemplate
。 通常情况下,使用 TransactionTemplate
可以在 Spring 环境中执行事务操作,而无需显式地声明 @Transactional
注解。这对于某些情况下无法使用声明式事务管理的场景非常有用,比如在非 Spring 管理的对象中执行事务操作。
以下是使用 TransactionTemplate
的基本示例:
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
public class TransactionalService {
@Autowired
private TransactionTemplate transactionTemplate;
public void doTransactionalOperation() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// 在事务中执行的业务逻辑
// 可能包括数据库操作、文件操作等
// 如果发生异常,事务会被回滚
// 如果正常执行完成,事务会被提交
// 这里只是一个示例
// 实际业务逻辑需要根据需求来编写
// 比如调用其他服务、持久化数据等
} catch (Exception e) {
status.setRollbackOnly(); // 设置事务为回滚状态
throw e; // 抛出异常以使得事务回滚
}
}
});
}
}
在上面的示例中,TransactionalService
类中使用了 TransactionTemplate
进行事务操作。TransactionTemplate
的 execute
方法接受一个 TransactionCallback
对象作为参数,用于执行需要事务管理的操作。在 doInTransactionWithoutResult
方法中,你可以执行需要事务管理的操作,如果出现异常,事务将会被回滚;如果没有异常,则事务将会被提交。
在 Spring 的 TransactionTemplate
中,execute
方法和 executeWithoutResult
方法都用于执行事务操作,但它们之间有一些区别。
execute
方法execute
方法用于执行具有返回值的事务性操作。在execute
方法中,可以执行需要在事务中执行的业务逻辑,并返回一个结果对象。
如果业务逻辑执行成功,返回的结果对象会被execute
方法返回。
如果业务逻辑执行失败,execute
方法会捕获异常并将事务标记为回滚状态。在异常被捕获后,事务会回滚,并且异常会被重新抛出。executeWithoutResult
方法:executeWithoutResult
方法用于执行不需要返回值的事务性操作。在executeWithoutResult
方法中,可以执行需要在事务中执行的业务逻辑,但不需要返回任何结果。
如果业务逻辑执行成功,事务会被提交。
如果业务逻辑执行失败,executeWithoutResult
方法会捕获异常并将事务标记为回滚状态。在异常被捕获后,事务会回滚,但异常不会被重新抛出。相反,异常会被记录到日志中,并通过TransactionTemplate
的setRollbackOnly
方法标记事务为回滚状态。
1.3.2 TransactionSynchronizationManager
TransactionSynchronizationManager
类内部默认提供了下面六个ThreadLocal属性,分别保存当前线程对应的不同事务资源:
保存当前事务关联的资源--默认只会在新建事务的时候保存当前获取到的DataSource和当前事务对应Connection的映射关系--当然这里Connection被包装为了ConnectionHolder
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
事务监听者--在事务执行到某个阶段的过程中,会去回调监听者对应的回调接口(典型观察者模式的应用)---默认为空集合
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
见名知意: 存放当前事务名字
private static final ThreadLocal<String> currentTransactionName =new NamedThreadLocal<>("Current transaction name");
见名知意: 存放当前事务是否是只读事务
private static final ThreadLocal<Boolean> currentTransactionReadOnly =new NamedThreadLocal<>("Current transaction read-only status");
见名知意: 存放当前事务的隔离级别
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =new NamedThreadLocal<>("Current transaction isolation level");
见名知意: 存放当前事务是否处于激活状态
private static final ThreadLocal<Boolean> actualTransactionActive =new NamedThreadLocal<>("Actual transaction active");
那么抛出的异常的原因也就很清楚了,无法在main线程找到当前事务对应的资源,原因如下
开启新事务时,事务相关资源都被绑定到了thread-cache-pool-1
线程对应的threadLocalMap内部,而当执行事务提交代码时,commit内部需要从TransactionSynchronizationManager
中获取当前事务的资源,显然我们无法从main线程对应的threadLocalMap
中获取到对应的事务资源,这也就是异常抛出的原因。
1.3.3 问题分析完如何解决问题
这里给出一个我首先想到的简单粗暴的方法—CopyTransactionResource
—将事务资源在两个线程间来回复制,这里给出解决问题的代码示例:
@Component
@RequiredArgsConstructor
public class MultiplyThreadTransactionManager {
/**
* 如果是多数据源的情况下,需要指定具体是哪一个数据源
*/
private final DataSource dataSource;
/**
* 执行的是无返回值的任务
* @param tasks 异步执行的任务列表
* @param executor 异步执行任务需要用到的线程池,考虑到线程池需要隔离,这里强制要求传
*/
public void runAsyncButWaitUntilAllDown(List<Runnable> tasks, Executor executor) {
if(executor==null){
throw new IllegalArgumentException("线程池不能为空");
}
DataSourceTransactionManager transactionManager = getTransactionManager();
//是否发生了异常
AtomicBoolean ex=new AtomicBoolean();
List<CompletableFuture> taskFutureList=new ArrayList<>(tasks.size());
List<TransactionStatus> transactionStatusList=new ArrayList<>(tasks.size());
List<TransactionResource> transactionResources=new ArrayList<>(tasks.size());
tasks.forEach(task->{
taskFutureList.add(CompletableFuture.runAsync(
() -> {
try{
//1.开启新事务
transactionStatusList.add(openNewTransaction(transactionManager));
//2.copy事务资源
transactionResources.add(TransactionResource.copyTransactionResource());
//3.异步任务执行
task.run();
}catch (Throwable throwable){
//打印异常
throwable.printStackTrace();
//其中某个异步任务执行出现了异常,进行标记
ex.set(Boolean.TRUE);
//其他任务还没执行的不需要执行了
taskFutureList.forEach(completableFuture -> completableFuture.cancel(true));
}
}
, executor)
);
});
try {
//阻塞直到所有任务全部执行结束---如果有任务被取消,这里会抛出异常滴,需要捕获
CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[]{})).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//发生了异常则进行回滚操作,否则提交
if(ex.get()){
System.out.println("发生异常,全部事务回滚");
for (int i = 0; i < tasks.size(); i++) {
transactionResources.get(i).autoWiredTransactionResource();
transactionManager.rollback(transactionStatusList.get(i));
transactionResources.get(i).removeTransactionResource();
}
}else {
System.out.println("全部事务正常提交");
for (int i = 0; i < tasks.size(); i++) {
transactionResources.get(i).autoWiredTransactionResource();
transactionManager.commit(transactionStatusList.get(i));
transactionResources.get(i).removeTransactionResource();
}
}
}
private TransactionStatus openNewTransaction(DataSourceTransactionManager transactionManager) {
//JdbcTransactionManager根据TransactionDefinition信息来进行一些连接属性的设置
//包括隔离级别和传播行为等
DefaultTransactionDefinition transactionDef = new DefaultTransactionDefinition();
//开启一个新事务---此时autocommit已经被设置为了false,并且当前没有事务,这里创建的是一个新事务
return transactionManager.getTransaction(transactionDef);
}
private DataSourceTransactionManager getTransactionManager() {
return new DataSourceTransactionManager(dataSource);
}
/**
* 保存当前事务资源,用于线程间的事务资源COPY操作
*/
@Builder
private static class TransactionResource{
//事务结束后默认会移除集合中的DataSource作为key关联的资源记录
private Map<Object, Object> resources = new HashMap<>();
//下面五个属性会在事务结束后被自动清理,无需我们手动清理
private Set<TransactionSynchronization> synchronizations =new HashSet<>();
private String currentTransactionName;
private Boolean currentTransactionReadOnly;
private Integer currentTransactionIsolationLevel;
private Boolean actualTransactionActive;
public static TransactionResource copyTransactionResource(){
return TransactionResource.builder()
//返回的是不可变集合
.resources(TransactionSynchronizationManager.getResourceMap())
//如果需要注册事务监听者,这里记得修改--我们这里不需要,就采用默认负责--spring事务内部默认也是这个值
.synchronizations(new LinkedHashSet<>())
.currentTransactionName(TransactionSynchronizationManager.getCurrentTransactionName())
.currentTransactionReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())
.currentTransactionIsolationLevel(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
.actualTransactionActive(TransactionSynchronizationManager.isActualTransactionActive())
.build();
}
public void autoWiredTransactionResource(){
resources.forEach(TransactionSynchronizationManager::bindResource);
//如果需要注册事务监听者,这里记得修改--我们这里不需要,就采用默认负责--spring事务内部默认也是这个值
TransactionSynchronizationManager.initSynchronization();
TransactionSynchronizationManager.setActualTransactionActive(actualTransactionActive);
TransactionSynchronizationManager.setCurrentTransactionName(currentTransactionName);
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(currentTransactionIsolationLevel);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(currentTransactionReadOnly);
}
public void removeTransactionResource() {
//事务结束后默认会移除集合中的DataSource作为key关联的资源记录
//DataSource如果重复移除,unbindResource时会因为不存在此key关联的事务资源而报错
resources.keySet().forEach(key->{
if(!(key instanceof DataSource)){
TransactionSynchronizationManager.unbindResource(key);
}
});
}
}
}
测试代码
@SpringBootTest(classes = UserMain.class)
public class Test {
@Resource
private UserMapper userMapper;
@Resource
private SignMapper signMapper;
@Resource
private MultiplyThreadTransactionManager multiplyThreadTransactionManager;
@SneakyThrows
@org.junit.jupiter.api.Test
public void test(){
List<Runnable> tasks=new ArrayList<>();
tasks.add(()->{
userMapper.deleteById(26);
throw new RuntimeException("我就要抛出异常!");
});
tasks.add(()->{
signMapper.deleteById(10);
});
multiplyThreadTransactionManager.runAsyncButWaitUntilAllDown(tasks, Executors.newCachedThreadPool());
}
}
事务都进行了回滚,数据库数据没变。
1.4 Spring事务传播机制
1.4.1 传播机制入门案例
先来看一种情况,a()在一个事务中执行,调用b()方法时需要新开一个事务执行的情况分析:
- 首先,代理对象执行a()方法前,先利用事务管理器新建一个数据库连接a
- 将数据库连接a的autocommit改为false
- 把数据库连接a设置到ThreadLocal中
- 执行a()方法中的sql
- 执行a()方法过程中,调用了b()方法,需要新开一个事务(注意用代理对象调用b()方法)
代理对象执行b()方法前,判断出来了当前线程中已经存在一个数据库连接a了,表示当前线程其实已经拥有一个Spring事务了,则进行挂起
挂起就是把ThreadLocal中的数据库连接a从ThreadLocal中移除,并放入一个挂起资源对象中
挂起完成后,再次利用事务管理器新建一个数据库连接b,将数据库连接b的autocommit改为false
把数据库连接b设置到ThreadLocal中,执行b()方法中的sql
b()方法正常执行完,则从ThreadLocal中拿到数据库连接b进行提交
提交之后会恢复所挂起的数据库连接a,这里的恢复,其实只是把在挂起资源对象中所保存的数据库连接a再次设置到ThreadLocal中 -
a()方法正常执行完,则从ThreadLocal中拿到数据库连接a进行提交
这个过程中最为核心的是:在执行某个方法时,判断当前是否已经存在一个事务,就是判断当前线程的ThreadLocal中是否存在一个数据库连接对象,如果存在则表示已经存在一个事务了。
1.4.2 Spring七个事务传播属性
- PROPAGATION_REQUIRED – 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
- PROPAGATION_SUPPORTS – 支持当前事务,如果当前没有事务,就以非事务方式执行。
- PROPAGATION_MANDATORY – 支持当前事务,如果当前没有事务,就抛出异常。
- PROPAGATION_REQUIRES_NEW – 新建事务,如果当前存在事务,把当前事务挂起。
- PROPAGATION_NOT_SUPPORTED – 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- PROPAGATION_NEVER – 以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED – 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
备注:常用的两个事务传播属性是1和4,即PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW
其中,以非事务方式运行,表示以非Spring事务运行,表示在执行这个方法时,Spring事务管理器不会去建立数据库连接,执行sql时,由Mybatis或JdbcTemplate自己来建立数据库连接来执行sql。
1.4.3 案例分析
情况1
@Component
public class UserService {
@Autowired
private UserService userService;
@Transactional
public void test() {
// test方法中的sql
userService.a();
}
@Transactional
public void a() {
// a方法中的sql
}
}
默认情况下传播机制为REQUIRED,表示当前如果没有事务则新建一个事务,如果有事务则在当前事务中执行。
所以上面这种情况的执行流程如下:
- 新建一个数据库连接conn
- 设置conn的autocommit为false
- 执行test方法中的sql
- 执行a方法中的sql
- 执行conn的commit()方法进行提交
情况2
假如是这种情况:
@Component
public class UserService {
@Autowired
private UserService userService;
@Transactional
public void test() {
// test方法中的sql
userService.a();
int result = 100/0;
}
@Transactional
public void a() {
// a方法中的sql
}
}
所以上面这种情况的执行流程如下:
- 新建一个数据库连接conn
- 设置conn的autocommit为false
- 执行test方法中的sql
- 执行a方法中的sql
- 抛出异常
- 执行conn的rollback()方法进行回滚,所以两个方法中的sql都会回滚掉
情况3
假如是这种情况:
@Component
public class UserService {
@Autowired
private UserService userService;
@Transactional
public void test() {
// test方法中的sql
userService.a();
}
@Transactional
public void a() {
// a方法中的sql
int result = 100/0;
}
}
所以上面这种情况的执行流程如下:
- 新建一个数据库连接conn
- 设置conn的autocommit为false
- 执行test方法中的sql
- 执行a方法中的sql
- 抛出异常
- 执行conn的rollback()方法进行回滚,所以两个方法中的sql都会回滚掉
情况4
如果是这种情况:
@Component
public class UserService {
@Autowired
private UserService userService;
@Transactional
public void test() {
// test方法中的sql
userService.a();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void a() {
// a方法中的sql
int result = 100/0;
}
}
所以上面这种情况的执行流程如下:
- 新建一个数据库连接conn
- 设置conn的autocommit为false
- 执行test方法中的sql
- 又新建一个数据库连接conn2
- 执行a方法中的sql
- 抛出异常
- 执行conn2的rollback()方法进行回滚
- 继续抛异常,对于test()方法而言,它会接收到一个异常,然后抛出
- 执行conn的rollback()方法进行回滚,最终还是两个方法中的sql都回滚了
1.5 Spring事务状态操作
1.5.1 事务强制回滚
正常情况下,a()调用b()方法时,如果b()方法抛了异常,但是在a()方法捕获了,那么a()的事务还是会正常提交的,但是有的时候,我们捕获异常可能仅仅只是不把异常信息返回给客户端,而是为了返回一些更友好的错误信息,而这个时候,我们还是希望事务能回滚的,那这个时候就得告诉Spring把当前事务回滚掉,做法就是:
@Transactional
public void test(){
// 执行sql
try {
b();
} catch (Exception e) {
// 构造友好的错误信息返回
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
public void b() throws Exception {
throw new Exception();
}
1.5.2 事务部分回滚
使用 Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
设置回滚点。
使用 TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
回滚到savePoint。
@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder (){
success();
//只回滚以下异常,
Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
try {
exception();
} catch (Exception e) {
e.printStackTrace();
// 手工回滚事务
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
return ApiReturnUtil.error();
}
return ApiReturnUtil.success();
}
1.5.3 监听事务状态
Spring事务有可能会提交,回滚、挂起、恢复,所以Spring事务提供了一种机制,可以让程序员来监听当前Spring事务所处于的状态
@Component
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private UserService userService;
@Transactional
public void test(){
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void suspend() {
System.out.println("test被挂起了");
}
@Override
public void resume() {
System.out.println("test被恢复了");
}
@Override
public void beforeCommit(boolean readOnly) {
System.out.println("test准备要提交了");
}
@Override
public void beforeCompletion() {
System.out.println("test准备要提交或回滚了");
}
@Override
public void afterCommit() {
System.out.println("test提交成功了");
}
@Override
public void afterCompletion(int status) {
System.out.println("test提交或回滚成功了");
}
});
jdbcTemplate.execute("insert into t1 values(1,1,1,1,'1')");
System.out.println("test");
userService.a();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void a(){
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void suspend() {
System.out.println("a被挂起了");
}
@Override
public void resume() {
System.out.println("a被恢复了");
}
@Override
public void beforeCommit(boolean readOnly) {
System.out.println("a准备要提交了");
}
@Override
public void beforeCompletion() {
System.out.println("a准备要提交或回滚了");
}
@Override
public void afterCommit() {
System.out.println("a提交成功了");
}
@Override
public void afterCompletion(int status) {
System.out.println("a提交或回滚成功了");
}
});
jdbcTemplate.execute("insert into t1 values(2,2,2,2,'2')");
System.out.println("a");
}
}
可以使用 TransactionSynchronizationAdapter 类和 TransactionSynchronization 类
TransactionSynchronization
是一个接口,定义了事务同步的回调方法。TransactionSynchronizationAdapter
是TransactionSynchronization
的抽象适配器类,提供了TransactionSynchronization
接口的默认空实现,使得开发者可以更方便地实现自定义的事务同步逻辑
1.6 声明式事务失效情况
1.6.1 @Transactional非public修饰的方法上
如果Transactional注解应用在非 public 修饰的方法上,Transactional将会失效。
是因为在Spring AOP 代理时,TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的intercept方法 或 JdkDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute方法,获取Transactional 注解的事务配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
1.6.2 方法使用final或者static修饰
有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:
@Service
public class OrderServiceImpl {
@Transactional
public final void cancel(OrderDTO orderDTO) {
// 取消订单
cancelOrder(orderDTO);
}
}
比如:OrderServiceImpl
的cancel取消订单方法被final修饰符修饰,Spring事务底层使用了AOP,也就是通过JDK动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,从而无法添加事务功能。这种情况事务就会在Spring中失效。
根据这个原理可知,如果某个方法是static的,同样无法通过动态代理将方法声明为事务方法。
1.6.3 @Transactional 注解属性propagation设置错误
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
1.6.4 @Transactional 注解属性 rollbackFor设置错误
rollbackFor可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException的异常)或者 Error才回滚事务,其他异常不会触发回滚事务。
Spring默认支持的异常回滚
// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)
若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。
或者在@Transactional 中设置 rollbackFor = Exception.class 。
注意:如果异常被内部catch,程序生吞异常
1.6.5 同一个类中方法调用导致@Transactional失效
被外部调用的公共方法A有两个进行了数据操作的子方法B和子方法C的事务注解说明:
- 被外部调用的公共方法A未声明事务
@Transactional
,子方法B和C若是其他类的方法且各自声明事务,则事务由子方法B和C各自控制 - 被外部调用的公共方法A未声明事务
@Transactional
,子方法B和C若是本类的方法,则无论子方法B和C是否声明事务,事务均不会生效 - 被外部调用的公共方法A声明事务
@Transactional
,无论子方法B和C是不是本类的方法,无论子方法B和C是否声明事务,事务均由公共方法A控制 - 被外部调用的公共方法A声明事务
@Transactional
,子方法运行异常,但运行异常被子方法自己 try-catch 处理了,则事务回滚是不会生效的
类内部访问:A 类的 a1 方法没有标注 @Transactional,a2 方法标注 @Transactional,在 a1 里面调用 a2,则不生效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入字段为 3的数据
*/
this.insertB();
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
这种情况是最常见的一种@Transactional注解失效场景
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务就不能正常回滚了,会抛出异常:
org.springframework.transaction.UnexpectedRollbackException:
Transaction rolled back because it has been marked as rollback-only
1.6.6 数据库不支持事务
Spring事务生效的前提是连接的数据库支持事务,如果底层的数据库都不支持事务,则Spring事务肯定会失效的
例如:使用MySQL数据库,选用MyISAM
存储引擎,因为MyISAM
存储引擎本身不支持事务,因此事务毫无疑问会失效。
1.6.7 多线程
多线程:主线程和子线程的调用,线程抛出异常
下面给出两个不同的姿势,一个是子线程抛异常,主线程 ok;一个是子线程 ok,主线程抛异常
父线程抛出异常,子线程不抛出异常
父线程抛出线程,事务回滚,因为子线程是独立存在,和父线程不在同一个事务中,所以子线程的修改并不会被回滚。
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
testSuccess();
}
}).start();
throw new Exception("测试事务不生效");
}
父线程不抛出异常,子线程抛出异常:
由于子线程的异常不会被外部的线程捕获,所以父线程不抛异常,事务回滚没有生效
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
throw new Exception("测试事务不生效");
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
testSuccess();
}
}).start();
}
1.6.8 @Transaction和@Test联用失效
当在测试类中直接调用Spring Data JPA的接口时,需要需要在测试类中添加注解@Transactional,但是@Transactional和@Test一起连用会导致事务自动回滚,这时候需要指定事务不回滚@Rollback(false)
@Test
@Transactional
@Rollback(false)
public void updateById() {
dao11.updateById("张三",1);
}
1.7 @Transaction和Mybatis-Plus的@DS问题
1.7.1 代码示例
主库
@Service
@DS("Master")
public class MasterService {
@Autowired
UserService userService;
@Autowired
BookService bookService;
/**必须master库方法先执行,才能回滚,达到事务效果*/
@Transactional(rollbackFor = Exception.class)
public void upload(ReqDto reqDto){
userService.save(reqDto); //@DS("Master")
bookService.save(reqDto); //@DS("Slave")
}
}
@Service
@DS("Slave")
public class BookService extends ServiceImpl<BookMapper, Book> {
@Resource
private BookMapper bookMapper;
public void save(ReqDto reqDto) {
bookMapper.save(reqDto);
}
}
1.7.2 问题现象
使用动态数据源(@DS)时,@Transactional使用不当会照成@DS失效。
- BookService的save上面加@Transactional,数据源没有切换
- 开启事务的同时,会从数据库连接池获取数据库连接;
- 如果内层的service使用@DS切换数据源,只是又做了一层拦截,但是并没有改变整个事务的连接;
- 在这个事务内的所有数据库操作,都是在事务连接建立之后,所以会产生数据源没有切换的问题;
- 为了使@DS起作用,必须替换数据库连接,也就是改变事务的传播机制,产生新的事务,获取新的数据库连接
1.7.3 深入分析原因
@Transactional执行流程
- service的 upload方法上添加了 @Transactional 注解,Spring事务就会生效。此时,Spring TransactionInterceptor会通过AOP拦截该方法,创建事务。而创建事务,势必就会获得数据源。那么,TransactionInterceptor (事务拦截器) 会使用 Spring DataSourceTransactionManager (数据源事务管理) 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。而事务信息,就包括事务对应的 Connection 连接。所以还没走到 Mapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 Mapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 Mapper 添加 @DS 注解所对应的 DataSource 所对应的 Connection 。
- 现在可以把问题聚焦到 DataSourceTransactionManager 是怎么获取 DataSource 从而获得 Connection 的了。对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用 dynamic-datasource-spring-boot-starter 时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。
而 DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了master、slave 两个数据源,并且默认使用 master 数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于 @DS 获得数据源名,从而获得对应的 DataSource ,如果在 Service 方法上没有添加 @DS 注解,所以它只好返回默认数据源,也就是 master
当BookService的save上面加@Transactional(propagation =Propagation.REQUIRES_NEW),这样在调用另一个事务方法时,TransactionInterceptor会将原事务挂起,开启一个新事务,暂时性的将原事务信息和当前线程解绑
开启新事物对原来外部事务影响:内影响外,外不影响内
- REQUIRES_NEW 会新开启事务,外层事务不会影响内部事务的提交/回滚
- REQUIRES_NEW 的内部事务的异常,会影响外部事务的回滚
1.7.4 解决方法
去除MasterService.upload上面的@Transactional,数据源切换正常,虽然可以解决,但是事务无效。
BookService的save上面加@Transactional(propagation=Propagation.REQUIRES_NEW),数据源切换,且事务有效。完美解决。它会重新创建新事务,获取新的数据库连接,从而得到@DS的数据源
最终代码如下,只需要修改的是bookService,其他不变
@Service
@DS("Slave")
public class BookService extends ServiceImpl<BookMapper, Book> {
@Resource
private BookMapper bookMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(ReqDto reqDto) {
bookMapper.save(reqDto);
}
}
注意:
- Slave数据库的操作,需要在master之后,这样当bookService.save失败,会使得userService回滚;
- 如果Slave的操作在前,那当userService失败,无法使bookService回滚,这是因为Propagation.REQUIRES_NEW原因
所以会有调用顺序限制,那么使用@DSTransactional
注解,就可以解决调用顺序和事务回滚问题,但是@DSTransactional
只适用于单服务,对于分布式服务可以用springcloud里面的seata框架
1.8 分布式事务
1.9 自定义事务
1.9.1 Spring事务
Spring事务的定义包括:begin、commit、rollback、close、suspend、resume等动作。
- begin(事务开始): 可以认为存在于数据库的命令中,比如Mysql的
start transaction
命令,但是在JDBC编程方式中不存在。 - close(事务关闭):Spring事务的close()方法,是把
Connection
对象归还给数据库连接池,与事务无关。关闭一个数据库连接,这已经不再是事务的方法了 - suspend(事务挂起):Spring中事务挂起的语义是:需要新事务时,将现有的
Connection
保存起来(还有尚未提交的事务),然后创建新的Connection2
,Connection2
提交、回滚、关闭完毕后,再把Connection1
取出来继续执行。 - resume(事务恢复): 嵌套事务执行完毕,返回上层事务重新绑定连接对象到事务管理器的过程。
实际上,只有commit、rollback、close是在JDBC真实存在的,而其他动作都是应用的语意,而非JDBC事务的真实命令。因此,事务真实存在的方法是:setAutoCommit()
、commit()
、rollback()
。
1.9.2 自定义管理事务
为了保证在多个数据源中事务的一致性,我们可以手动管理Connetion
的事务提交和回滚。考虑到不同ORM框架的事务管理实现差异,要求实现自定义事务管理不影响框架层的事务。
这可以通过使用装饰器设计模式,对Connection
进行包装重写commit和rolllback屏蔽其默认行为,这样就不会影响到原生Connection
和ORM框架的默认事务行为。其整体思路如下图所示:
1.9.3 定义多事务注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransaction {
String transactionManager() default "multiTransactionManager";
// 默认数据隔离级别,随数据库本身默认值
IsolationLevel isolationLevel() default IsolationLevel.DEFAULT;
// 默认为主库数据源
String datasourceId() default "default";
// 只读事务,若有更新操作会抛出异常
boolean readOnly() default false;
业务方法只需使用该注解即可开启事务,datasourceId
指定事务用到的数据源,不指定默认为主库。
1.9.4 包装Connection
自定义事务我们使用包装过的Connection
,屏蔽其中的commit&close
方法。这样我们就可以在主事务里进行统一的事务提交和回滚操作。
public class ConnectionProxy implements Connection {
private final Connection connection;
public ConnectionProxy(Connection connection) {
this.connection = connection;
}
@Override
public void commit() throws SQLException {
// connection.commit();
}
public void realCommit() throws SQLException {
connection.commit();
}
@Override
public void close() throws SQLException {
//connection.close();
}
public void realClose() throws SQLException {
if (!connection.getAutoCommit()) {
connection.setAutoCommit(true);
}
connection.close();
}
@Override
public void rollback() throws SQLException {
if(!connection.isClosed())
connection.rollback();
}
...
}
这里commit&close
方法不执行操作,rollback执行的前提是连接执行close才生效。这样不管是使用哪个ORM框架,其自身事务管理都将失效。事务的控制就交由MultiTransaction
控制了。
1.9.4 事务上下文管理
public class TransactionHolder {
// 是否开启了一个MultiTransaction
private boolean isOpen;
// 是否只读事务
private boolean readOnly;
// 事务隔离级别
private IsolationLevel isolationLevel;
// 维护当前线程事务ID和连接关系
private ConcurrentHashMap<String, ConnectionProxy> connectionMap;
// 事务执行栈
private Stack<String> executeStack;
// 数据源切换栈
private Stack<String> datasourceKeyStack;
// 主事务ID
private String mainTransactionId;
// 执行次数
private AtomicInteger transCount;
// 事务和数据源key关系
private ConcurrentHashMap<String, String> executeIdDatasourceKeyMap;
}
每开启一个事物,生成一个事务ID并绑定一个ConnectionProxy
。事务嵌套调用,保存事务ID和lookupKey在栈中,当内层事务执行完毕执行pop。这样的话,外层事务只需在栈中执行peek即可获取事务ID和lookupKey。
1.9.5 数据源兼容处理
为了不影响原生事务的使用,需要重写javax.sql.DataSource下的getConnection
方法。当前线程没有启动自定义事务,则直接从数据源中返回连接。
@Override
public Connection getConnection() throws SQLException {
TransactionHolder transactionHolder = MultiTransactionManager.TRANSACTION_HOLDER_THREAD_LOCAL.get();
if (Objects.isNull(transactionHolder)) {
return determineTargetDataSource().getConnection();
}
ConnectionProxy ConnectionProxy = transactionHolder.getConnectionMap()
.get(transactionHolder.getExecuteStack().peek());
if (ConnectionProxy == null) {
// 没开跨库事务,直接返回
return determineTargetDataSource().getConnection();
} else {
transactionHolder.addCount();
// 开了跨库事务,从当前线程中拿包装过的Connection
return ConnectionProxy;
}
}
1.9.6 切面处理
切面处理的核心逻辑是:维护一个嵌套事务栈,当业务方法执行结束,或者发生异常时,判断当前栈顶事务ID是否为主事务ID。如果是的话这时候已经到了最外层事务,这时才执行提交和回滚。详细流程如下图所示:
具体代码示例:
package com.github.mtxn.transaction.aop;
@Aspect
@Component
@Slf4j
@Order(99999)
public class MultiTransactionAop {
@Pointcut("@annotation(com.github.mtxn.transaction.annotation.MultiTransaction)")
public void pointcut() {
if (log.isDebugEnabled()) {
log.debug("start in transaction pointcut...");
}
}
@Around("pointcut()")
public Object aroundTransaction(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
// 从切面中获取当前方法
Method method = signature.getMethod();
MultiTransaction multiTransaction = method.getAnnotation(MultiTransaction.class);
if (multiTransaction == null) {
return point.proceed();
}
IsolationLevel isolationLevel = multiTransaction.isolationLevel();
boolean readOnly = multiTransaction.readOnly();
String prevKey = DataSourceContextHolder.getKey();
MultiTransactionManager multiTransactionManager = Application.resolve(multiTransaction.transactionManager());
// 切数据源,如果失败使用默认库
if (multiTransactionManager.switchDataSource(point, signature, multiTransaction)) return point.proceed();
// 开启事务栈
TransactionHolder transactionHolder = multiTransactionManager.startTransaction(prevKey, isolationLevel, readOnly, multiTransactionManager);
Object proceed;
try {
proceed = point.proceed();
multiTransactionManager.commit();
} catch (Throwable ex) {
log.error("execute method:{}#{},err:", method.getDeclaringClass(), method.getName(), ex);
multiTransactionManager.rollback();
throw ExceptionUtils.api(ex, "系统异常:%s", ex.getMessage());
} finally {
// 当前事务结束出栈
String transId = multiTransactionManager.getTrans().getExecuteStack().pop();
transactionHolder.getDatasourceKeyStack().pop();
// 恢复上一层事务
DataSourceContextHolder.setKey(transactionHolder.getDatasourceKeyStack().peek());
// 最后回到主事务,关闭此次事务
multiTransactionManager.close(transId);
}
return proceed;
}
}
注意:这种方式只适用于单体架构的应用。因为多个库的事务参与者都是运行在同一个JVM进行。如果是在微服务架构的应用中,则需要使用分布式事务管理(譬如:Seata)。