文章可能会动态更新 csdn博主:孟秋与你 保证结论不误导人 拒绝复制粘贴;
网上可能会列出十几点失效原因,但真正的核心其实为:
所有的事务失效问题 都是对动态代理(bean生命周期)理解不完全导致的,
只有保证了事务是被代理对象执行的,才不会出现事务失效
事务回滚失败分析
1.第一种情况 :调用方加上@Transactional 注解,被调用方未加上注解 (同一个service类)
Exp:
@Transactional
void methodA(){
// insert table1
// 等于 this.methodB(obj)
methodB(obj);
}
void methodB(Object obj){
// insert table2
}
结论:当methodB()出现异常,抛给methodA,A中捕获到异常,进行回滚处理,table1,table2都未插入
2.第二种情况:调用方未加上注解,被调用方加上注解(同一个service类)
Exp:
void methodA(){
// insert table1
methodB(obj);
}
@Transactional
void methodB(Object obj){
// insert table2
}
结论: methodB()非事务执行。 其实是spring的proxy代理对象执行的事务,由于methodA()上面没有注解,代理对象找不到事务开启的标记,所以不会开启事务,不能回滚。
基于上述结论的理解
3.第三种情况:调用方未加上注解(methodA),被调用方加上注解(在不同的service类)
结论:此时被调用方(methodB)在另一个类BserviceImpl,B类代理对象bProxy调用methodB()时 ,methodB上面是有注解的 能开启事务,所以出现异常都会回滚 table1,table2都未插入。
实际场景分析:
存在业务逻辑: 需要批量插入员工信息至公司表与部门表,员工信息要么同时在公司、部门表,要么同时不存在。
因此需要将插入 t_company 和插入 t_dept 进行事务管理,当第二个员工插入部门表出现异常时,第一个已正常插入的员工不能受到影响,第二个员工的信息需要回滚。
我们的要求是不集体回滚,所以此时调用方 methodA(){ for 循环} 上面肯定不能加上注解 ,如果将methodB(){ insert t_company,insert t_dept}写在同一个service中,并加上注解,就会出现上述第二种情况,回滚失败,第二个员工的信息存在公司表 而不存在部门表。
解决方式:
-
写在另一个service (即我们分析的第三种情况)
-
写在同一个service也可以,获取代理对象 通过代理对象调用 methodB() 这样就能执行回滚了, 方式非常多 基本上只要能获取bean来执行就行。
2.1 可以@Autowired本类自己 , 因为@Autowired的是bean对象,在aop之后,bean对象就是代理对象了(要注意循环依赖问题!)
2.2 实现ApplicationContextAware接口
(Autowired注入ApplicationContext也是一样的)private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } public void test(){ YourBean bean = applicationContext.getBean(YourBean .class); }
这里会有个存疑点:
博主关于spring接口的文章中有分析到,ApplicationContextAware执行时机是在初始化前,那么初始化后才生成的代理对象 可以通过applicationContext获取到吗?答案是可以的,博主追踪了一下源码核心调用链路:
大致流程是,bean后置处理器的过程中 会去将原始bean替换成代理对象bean, 而创建的(代理)bean对象,直接存放在bean工厂内, applicationContext.getBean其实也是去bean工厂获取的, 它就相当于一个spring对外提供的接口, 所以后续虽然没有再次执行setApplicationContext方法,依然可以获取代理对象 (此外 我们在第一次调用http请求的时候 调试可以发现 会执行一遍doCreateBean方法 确保bean创建完成, 后续则不再执行 这是spring的缓存机制)
AbstractAutowireCapableBeanFactory#doCreateBean -> AbstractAutowireCapableBeanFactory#initializeBean -> AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization -> AbstractAutoProxyCreator#postProcessBeforeInstantiation -> AbstractAutoProxyCreator#wrapIfNecessary
2.3 或者用下述方法
// 需要先在启动类加上注解 @EnableAspectJAutoProxy(exposeProxy = true) YourServiceImpl yourService = (YourServiceImpl ) AopContext.currentProxy(); yourService.methodB();
-
如果理解bean有困难的同学,可以直接使用transactionTemplate , transactionTemplate里面写的代码都会走事务执行,不需要关心细节,即使this.otherMethod() 去调用,otherMethod里面出现异常也会回滚。
@Autowired
private TransactionTemplate transactionTemplate;
public void method() {
transactionTemplate.execute(
(item)->{
// your code
return null;
}
);
}
事务失效的典型场景:
PostConstruct 事务失效
(题外话 本文最早是博主在2021年初写的,在后来博主在不断学习 又写了多篇关于spring相关的文章,如果有看博主其它博文 应该很容易理解)
@PostConstruct是初始化时执行的,而aop代理对象是初始化后才有的,事务又是代理对象执行的,换句话说初始化的时候 还没代理对象 自然也就不能事务执行了
private方法失效
还是那句话 事务是代理对象执行的,代理对象其实就是继承了原来的类生成的实例 如果是private就无法继承了。
拓展:动态代理执行原理
在通过对aop有了稍微更深入一点点的学习后 ,
(如果有看到这篇文章感到困惑的朋友 可能需要参见我的另一篇文章 spring工作流程 )
突然发现以前的认知不够完善 , 上述结论仍然是正确的 但感觉不够深入 所以进行补充。
- 可以看到 我们在testB方法中 加上了注解 传播级别设置为never ,可是并没有生效(控制台没有日志提示) ,也就是说 这个注解我们写了 相当于没写
- 我们换用bean(该场景下为代理对象)来调用,可以看到达到了我们预期的效果
- 那么问题来了 我们知道事务开启一定是在代理对象执行的,那么下述情况(methodB没加事务) 为什么还会回滚呢
注意:不能用事务传播机制默认为自动加入去理解 这是个初学者(没错 就是初学时的我)容易犯的错误
下面用伪代码分析代理到底做了什么:
class proxy extends TransactionalController{
/**
* bean对象 注意现在正在准备执行的步骤就是动态代理 所以这个bean是指代理前的bean
*/
private TransactionalController transactionalController;
/**
* 代理testA方法
* @author qkj
*/
public void testA(){
// 事务开启
conn.start;
try {
// 被代理的testA方法
transactionalController.testA()->{
// 里面包含了testB方法
transactionalController.testB();
};
// 所以即使testB方法中 出现异常 也能捕获 并回滚
}catch (Exception e){
conn.rockback;
}
conn.commit;
}
}
所以就是单纯因为调用方methodA 捕获到了异常,和事务传播机制没有关系,不要被默认加入传播机制给误导了。
事务的传播机制
既然上面那种情形 不是传播机制 那么传播机制到底怎么体现呢?
我们从REQUIRES_NEW 分析 先看看复制粘贴的话是怎么描述的:
. 粘贴1:如果当前存在事务,就把当前事务挂起;如果当前方法没有事务,就新建事务;
. 粘贴2:不管是否存在事务,业务方法总会自己开启一个事务,如果在已有事务的环境中调用,
已有事务会被挂起,新的事务会被创建,直到业务方法调用结束,已有事务才被唤醒
第二句会比第一句稍微好理解一点点 但还是有些抽象 咱也不知道网上复制一大堆 这些话他们自己到底懂不懂.
模拟一种可能存在的业务,结合业务理解会比较容易:
运行结果:
数据库结果:
摸鱼人员表:
绩效记录表:
这样符合实际业务,摸鱼被抓了 绩效该扣还是得扣 不会因为人员名单插入异常就全部回滚了,
这里因为绩效业务事务传播机制是Propagation.REQUIRES_NEW,外层摸鱼业务有事务,当执行到绩效时,绩效开启一个新的事务 并将旧的事务挂起,直到绩效事务提交后,旧事务继续,这样就保证了绩效是个独立的事务。有个这个分析,其它的传播机制也就比较容易理解了。