浅谈spring事务失效之谜
作者:互联网
引言:
每当我们使用Spring声明式事务时,我们只需要在类或方法上声明@Transactional就可以了,但是我们有没有遇到事务失效的时候呢?
spring事务失效六种大的原因
- 如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB。
- 如果使用了spring+mvc,则context:component-scan重复扫描问题可能会引起事务失败。
- @Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。
- @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。
- Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装(但是放在接口上面也是可以实现事务滴,我还没搞清)。
- 一个事务方法调用同一个类里面的另一个事务方法,被调用的方法的事务失效。(使用了原生对象,使得代理失效)
Spring的父子容器(必须放到listener里加载,而不是webMVC的配置里)
父容器:Root WebAplicationContext
子容器:Servlet WebAplicationContext
如图,可以看到子容器有什么需要的是可以向父容器要的,但是父容器是没法向子容器要的。所以当我们是使用Spring+SpringMVC进行web应用开发的时候,Spring负责扫描DAO和Service层,SpringMVC负责扫描controller就行了(请不要在扫描DAO和Service层,自己做自己的事情就可以了,虽然这样做不会出错,但是浪费了内存空间)。
这张图也解释了为什么我们在SpringMVC中配置了事务扫描却不生效------》一般做事务处理都是在Service层,我们把事务扫描配置在SpringMVC中,我们的父容器是无法得到的,这样就导致了事务失效。。。。。
一个事务方法调用同一个类里面的另一个事务方法,被调用的方法的事务失效
(根本原因–》JDK代理或Cglib代理失效,使用了原生对象)
@Service
public class CityServiceImpl implements CityService {
private final static Logger LOGGER = LoggerFactory.getLogger(CityServiceImpl.class);
@Autowired
private CityDao cityDao;
private CityServiceImpl proxy;
@Override
@Transactional
public void parent() {
LOGGER.info("======================insertParent()===================");
//这种情况child事务失效
//根据动态代理分析 此处的child()不是由 AopProxy调用的 而是 this对象
try {
//com.tengxvincent.transaction.SpringTransaction.service.impl.CityServiceImpl
System.out.println("child invoked object :"+this.getClass().getName());
child();
}catch (Exception e){
LOGGER.error("parent catch child execption ",e);
}
//以下代码为Parent业务
City city= new City();
city.setProvinceId(Long.valueOf(99));
city.setCityName("parent");
city.setDescription("parentparentparentparent");
cityDao.insertCity(city);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void child() {
LOGGER.info("======================insertChild()===================");
City city= new City();
city.setProvinceId(Long.valueOf(99));
city.setCityName("Child");
city.setDescription("ChildChildChildChildChildChild");
cityDao.insertCity(city);
int a=1/0;//此处异常
}
我们运行这段代码以后会发现,当Child发生异常时,插入的数据却并没有回滚,这时就发生了事务失效。 接下来我们通过我上面给出的Spring事务失效的原因来分析,假设我们并没有犯了前五条的错误,那么这时候我们的问题来了,到底是什么造成了Child方法事务失效呢?
我们知道SpringAop和事务的实现都是通过JDK或Cglib动态代理来实现的,那么会不会是他们失效了呢?
然后我们开始打印
System.out.println(“parent invoked object :”+this.getClass().getName());
System.out.println(“child invoked object :”+this.getClass().getName());
我们发现parent返回的是代理对象,child返回的是原生对象。
失效的真正原因
目前Spring实现动态代理的方式有两种,一种是cglib,一种是jdk的,两个的实现方式不一样,但是事务失效原因是一样的。接下来我们以JDK动态代理为例
到这,我们终于知道了,为什么parent方法调用同一个类的Child方法会使Child的事务失效。----》想要使用事务,就必须使用代理对象。
那么我们该如何解决上面这个问题呢?
- 最简单的办法就是不要写在一个类里面。。。。
- 从当前线程的AopContext中获取
CityServiceImpl proxy=(CityServiceImpl)AopContext.currentProxy();
proxy.child()
- 通过spring应用上下文 获取 代理对象(ApplicationContext 在ioc容器中是单例的)
@Autowired
private ApplicationContext context;
@PostConstruct
public void init(){
proxy=context.getBean(CityServiceImpl.class);
}
proxy.child()
需要注意的事项
Spring的Transactional的API文档:
If no rules are relevant to the exception, it will be treated like DefaultTransactionAttribute (rolling back on runtime exceptions).
在业务代码中,有如下两种情况,比如:
throw new RuntimeException(“xxxxxxxxxxxx”); 事务回滚
throw new Exception(“xxxxxxxxxxxx”); 事务没有回滚
spring内部catch的就是 RuntimeException, service抛出RuntimeException可以回滚
如果抛出Exception,就不回滚….
1. Spring的AOP即声明式事务管理默认是针对unchecked exception回滚。
也就是默认对RuntimeException()异常或是其子类进行事务回滚;checked异常,即Exception可try{}捕获的不会回滚,如果使用try-catch捕获抛出的unchecked异常后没有在catch块中采用页面硬编码的方式使用spring api对事务做显式的回滚,则事务不会回滚, “将异常捕获,并且在catch块中不对事务做显式提交=生吞掉异常” ,要想捕获非运行时异常则需要如下配置:
解决办法:
1.在针对事务的类中抛出RuntimeException异常,而不是抛出Exception。
2.在txAdive中增加rollback-for,里面写自己的exception,例如自己写的exception:
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" rollback-for="com.cn.untils.exception.XyzException"/>
</tx:attributes>
</tx:advice>
或者定义不会滚的异常
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="update*" no-rollback-for="IOException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
2. spring的事务边界是在调用业务方法之前开始的,业务方法执行完毕之后来执行commit or rollback(Spring默认取决于是否抛出runtime异常).
如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
一般不需要在业务方法中catch异常,如果非要catch,在做完你想做的工作后(比如关闭文件等)一定要抛出runtime exception,否则spring会将你的操作commit,这样就会产生脏数据.所以你的catch代码是画蛇添足。
例如:
try {
//bisiness logic code
} catch(Exception e) {
//handle the exception
}
由此可以推知,在spring中如果某个业务方法被一个 整个包裹起来,则这个业务方法也就等于脱离了spring事务的管理,因为没有任何异常会从业务方法中抛出!全被捕获并吞掉,导致spring异常抛出触发事务回滚策略失效。
注:不过,如果在catch代码块中采用页面硬编码的方式使用spring api对事务做显式的回滚,这样写也未尝不可。
3. 基于注解的事务:
Transactional的异常控制,默认是Check Exception 不回滚,unCheck Exception回滚
如果配置了rollbackFor 和 noRollbackFor 且两个都是用同样的异常,那么遇到该异常,还是回滚
rollbackFor 和noRollbackFor 配置也许不会含盖所有异常,对于遗漏的按照Check Exception 不回滚,unCheck Exception回滚
如果只是@Transactional失效的话,可以考虑改成:@Transactional(rollbackFor=Exception.class)
例子如下,在类或方法上的加入:
@Transactional(rollbackFor=Exception.class)
标签:回滚,浅谈,spring,Transactional,事务,catch,失效,Exception,之谜 来源: https://blog.csdn.net/weixin_44046437/article/details/99712344