事务的传播行为&&嵌套事务的使用
一、前言:
事务的传播行为(propagation)就是为了解决外层方法调用内层事务方法的各个情况的。
接下来要说的嵌套事务的使用是基于Spring声明式事务管理中的注解@Transactional 方式的。
二、事务的传播行为:
- @Transactional(propagation=Propagation.REQUIRED) :如果外层调用方法本身有事务, 那么就加入到该事务中, 没有的话新建一个(这是默认的设置项)
- @Transactional(propagation=Propagation.NOT_SUPPORTED) :以非事务方式运行,如果外层调用方法存在事务,则把当这个事务挂起。
- @Transactional(propagation=Propagation.REQUIRES_NEW) :不管外层调用方法否存在事务,都创建一个自己的事务,外层调用方法的事务挂起,自己的执行完毕,再执行调用方事务
- @Transactional(propagation=Propagation.MANDATORY) :如果外层调用方法存在事务,则加入该事务;如果外层调用方法没有事务,则抛出异常
- @Transactional(propagation=Propagation.NEVER) :以非事务方式运行,如果外层调用方法存在事务,则抛出异常。
- @Transactional(propagation=Propagation.SUPPORTS) :如果外层调用方法存在事务,则加入该事务;如果外层调用方法没有事务,则以非事务的方式继续运行。
- @Transactional(propagation=Propagation.NESTED) :如果外层调用方法存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果外层调用方法没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED
三、关于事务传播行为:
传播行为就是一个约定:“别的方法调用自己的时候会以怎样的方式开启事务”。
当你给一个方法指定传播行为的时候这时这个方法本身肯定是支持事务的方法,然而调用你的方法却不一定。
调用你的方法可能本身是个事务方法(service事务方法a调用service事务方法b,也可能不是(controller调用service事务方法b / service非事务方法a调用service事务方法b)
然后就看传播行为了。
Spring默认的是PROPAGATION_REQUIRED
事务的传播行为我们一般都是用来解决嵌套事务的,所以我们一般使用最多的是上面加黑的三种:
四、嵌套事务:
嵌套事务:就是事务方法A调用事务方法B,外层调用方法和内层被调用方法都是事务方法的情况。
一般我们不关心外层调用方法的事务传播行为(用默认的(不指定就行))。而只关心内层被调用方法的传播行为。
我们一般情况下,会有以下三种需求:
- 外层调用方法和内层被调用方法,有异常一起回滚,没问题一起提交。(共用一个事务)
- 内层被调用方法回滚与否,不会影响外层调用方法。而外层调用方法出异常回滚,也不会回滚内层被调用方法(两个独立的事务)
- 内层被调用方法回滚与否,不会影响外层调用方法。而外层调用方法出异常回滚,也会回滚内层被调用方法(嵌套事务)
这三种情况正好对应三种最常用的传播行为
1----->@Transactional(propagation=Propagation.REQUIRED) :
内外层方法共用外层方法的事务
2----->@Transactional(propagation=Propagation.REQUIRES_NEW) :
当执行内层被调用方法时,外层方法的事务会挂起。两个事务相互独立,不会相互影响。
3----->@Transactional(propagation=Propagation.NESTED) :
理解Nested的关键是savepoint。他与PROPAGATION_REQUIRES_NEW的区别是,PROPAGATION_REQUIRES_NEW另起一个事务,将会与他的父事务相互独立,
而Nested的事务和他的父事务是相依的,他的提交是要等和他的父事务一块提交的。也就是说,如果父事务最后回滚,他也要回滚的。
它看起来像这样
class ServiceA { public void methodA() { // 数据库操作等其他代码 try { // savepoint(虚拟的) ServiceB.methodB(); // PROPAGATION_NESTED 级别 } catch (SomeException) { // 执行其他业务, 如ServiceC.methodC(); } // 其他操作代码 } }
也就是说ServiceB.methodB失败回滚,那么ServiceA.methodA也会回滚到savepoint点上,ServiceA.methodA可以选择另外一个分支,比如
ServiceC.methodC,继续执行,来尝试完成自己的事务。
五、嵌套事务的使用:
关于使用我的代码放到了我的github上了。
1、propagation=Propagation.REQUIRED的情况
内层被调用事务方法
@Transactional(propagation=Propagation.REQUIRED) public void testRequired(User inner) { testDAO.insertUser(inner); }
外层调用方法
@Override //@Transactional(propagation=Propagation.REQUIRED) // 调用方法可以是事务方法也可以是非事务方法 public void testRequired(User outer, User inner) { testDAO.insertUser(outer); try{ innerBean.testRequired(inner); } catch(RuntimeException e){ log.error("内层方法出现异常回滚",e); } }
抛异常是通过,插入的User对象的UserName重复控制的,然后观察数据库就可以看到相应的情况结果。(你可以把代码下载下来自己跑一下)
测试Main方法如下
public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml"); OuterBean outerBean = (OuterBean) ctx.getBean("outerBeanImpl"); /**你能通过控制插入的数据的UserName重复产生异常*/ User outer = new User(); outer.setUsername("009"); outer.setName("zjl"); User inner = new User(); inner.setUsername("010"); inner.setName("zjl"); /** 选择代码进行注释,完成你想要的测试*/ outerBean.testRequired(outer, inner); // outerBean.testRequiresNew(outer,inner); //outerBean.testNested(outer,inner); }
这种传播行为能实现:外层调用方法和内层被调用方法,有异常一起回滚,没问题一起提交
2、propagation=Propagation.REQUIRES_NEW
内层被调用事务方法
@Override @Transactional(propagation=Propagation.REQUIRES_NEW) public void testRequiresNew(User inner) { testDAO.insertUser(inner); }
外层调用方法
@Override @Transactional(propagation=Propagation.REQUIRED) public void testRequiresNew(User outer, User inner) { testDAO.insertUser(outer); try{ innerBean.testRequiresNew(inner); } catch(RuntimeException e){ log.error("内层方法出现异常回滚",e); } }
测试方法相同
这种传播行为能实现:内层被调用方法回滚与否,不会影响外层调用方法。而外层调用方法出异常回滚,也不会回滚内层被调用方法
3、propagation=Propagation.NESTED
内层被调用事务方法
@Override @Transactional(propagation=Propagation.NESTED) public void testNested(User inner) { testDAO.insertUser(inner); }
外层调用方法
@Override @Transactional(propagation=Propagation.REQUIRED) public void testNested(User outer, User inner) { testDAO.insertUser(outer); try{ innerBean.testNested(inner); } catch(RuntimeException e){ log.error("内层方法出现异常回滚",e); } }
测试方法相同
这种传播行为能实现:内层被调用方法回滚与否,不会影响外层调用方法。而外层调用方法出异常回滚,也会回滚内层被调用方法
六、使用中的注意事项:
1、外层调用内层方法是两个事务的都要try catch 住调用内层方法的代码块。共用一个事务的不要try catch 住(要不就出下面2那个异常)。
因为Spring声明式事务处理是基于Aop的,默认情况下他会在方法抛出运行时异常时,拦截异常回滚事务,然后会继续向上抛出。 所以你要try catch 住要不外层调用方法会用相应异常,那传播行为就没有用了。
// 就类似这种 @Override @Transactional(propagation=Propagation.REQUIRED) public void testNested(User outer, User inner) { testDAO.insertUser(outer); try{ innerBean.testNested(inner); } catch(RuntimeException e){ log.error("内层方法出现异常回滚",e); } }
2、“Transaction rolled back because it has been marked as rollback-only ”异常的出现
出现场景:这种异常一般是在,嵌套事务使用中,内层事务使用默认的事务传播行为(Propagation.REQUIRED),内外共用一个事务时,外层方法把内层方法try catch 住了,就会出现。
原因:内层方法出异常了,会向上抛异常,SpringAOP拦截到,就会把事务标志为rollback only,就是准备要回滚。
由于内外方法共用一个事务,这时要是外层方法把这个异常捕获了,外层方法就继续提交。但是事务标记已经置了,那就会抛这个异常。
3、同一的类的事务方法是无法直接调用的,如果 ServiceA.methodA调用 Service.methodB,会使被调用方法的事务失效
因为spring的事务是基于代理类来实现的。在controller里的service其实是代理对象,所以b方法的事务有效。,而在同一个类中ServiceA.methodA调用 Service.methodB,你拿到的不是代理后的methodB,所以事务会失效
解决方法很简单,在methodA方法类中获取当前对象的代理对象1 ServiceA proxy =(ServiceA)AopContext.currentProxy(); 2 proxy.b();