最后这篇文章我们来讨论开发中最常用的剩下三种事务传播机制:REQUIRED、REQUIRES_NEW和NESTED

5. REQUIRED

REQUIRED是我们最常用的传播机制。如果当前有存在的事务则加入该事务,如果没有则新开一个事务。

先修改配置文件:

      
     
 

父事务类:

@Componentpublic class TransactionSuper {    @Autowired    TransactionSub transactionSub;    String insertSuperTable1 = "insert into super_table values (1, 'super1')";    String insertSuperTable2 = "insert into super_table values (2, 'super2')";        public void insertSuperTable(AbstractApplicationContext ctx) throws Exception{    	System.out.println("========insertSuperTable start========");    	JdbcTemplate jt = (JdbcTemplate)ctx.getBean("jdbcTemplate");    	jt.execute(insertSuperTable1);    	transactionSub.insertSubTable(ctx);    	jt.execute(insertSuperTable2);    	System.out.println("========insertSuperTable end========");    }}

子事务类:

@Componentpublic class TransactionSub {    String insertSubTable1 = "insert into sub_table values (1, 'sub1')";    String insertSubTable2 = "insert into sub_table values (2, 'sub2')";    public void insertSubTable(AbstractApplicationContext ctx) throws Exception {        System.out.println("========insertSubTable start========");        JdbcTemplate jt = (JdbcTemplate) ctx.getBean("jdbcTemplate");        jt.execute(insertSubTable1);        jt.execute(insertSubTable2);        System.out.println("========insertSubTable end========");    }}

然后运行测试方法

如果有存在的事务,跟MANDATORY效果一样,也是加入该事务,并且共用一个connection,所以父子方法对数据库的修改都是相互可见的。没有事务的情况这里就不再演示了,也就是insertSubTable方法自己新开一个事务。

我们来看看发生异常时的回滚,其实基本跟MANDATORY一样,只是子方法可以自己开启事务,而不一定要加入其它事务之中

①RuntimeException

如果有已经存在的事务,跟MANDATORY一样,REQUIRED标注的方法是直接加入父事务,成为父事务的一部分,他们共享一个connection。所以不管是子事务还是父事务抛出RuntimeException的时候,父子事务都会回滚。如果没有事务,就只回滚子方法新开事务中的操作。

②Throwable和Exception

MANDATORY一样,如果不配置rollback-for属性,抛出Throwable和Exception都不会导致父子事务回滚,而是在哪儿出异常就在哪儿提交,就有可能出现部分提交的现象。

我们可以用rollback-for属性来让抛Throwable和Exception时也回滚。由于REQUIRED标注的方法是直接加入父事务,所以子类发生Throwable或Exception,就会父子一起回滚。


6. REQUIRES_NEW

REQUIRES_NEW标记的方法会新建事务,如果当前存在事务,把当前事务挂起,等待新开事务完成后,被挂起的事务再恢复执行。

先修改配置文件:

      
     
 

直接执行测试方法:

从上面可以很明显的看出insertSubTable方法新开了一个connection并重启了一个新事务,insertSuperTable和insertSubTable是两个不同的connection中的两个不同的事务。所以父方法对数据库的修改是否对子方法可见,取决于数据库的事务隔离级别。

REQUIRES_NEW由于开了新连接和新事务,所以异常回滚跟其他事务传播机制有很大区别,下面我们结合具体实例来说明。

①RuntimeException

我们先让子方法insertSubTable抛出RuntimeException。

子事务类:

@Componentpublic class TransactionSub {    String insertSubTable1 = "insert into sub_table values (1, 'sub1')";    String insertSubTable2 = "insert into sub_table values (2, 'sub2')";    public void insertSubTable(AbstractApplicationContext ctx) throws Exception {        System.out.println("========insertSubTable start========");        JdbcTemplate jt = (JdbcTemplate) ctx.getBean("jdbcTemplate");        jt.execute(insertSubTable1);        ExceptionUtils.throwRuntimeException(); //抛出RuntimeException        jt.execute(insertSubTable2);        System.out.println("========insertSubTable end========");    }}

运行测试类:

可以看到虽然两个方法运行在不同connection的事务里,但还是都发生了回滚。如果我们在父方法把子方法抛出的RuntimeException捕获住呢?修改父方法为:

父事务类:

@Componentpublic class TransactionSuper {    @Autowired    TransactionSub transactionSub;    String insertSuperTable1 = "insert into super_table values (1, 'super1')";    String insertSuperTable2 = "insert into super_table values (2, 'super2')";        public void insertSuperTable(AbstractApplicationContext ctx) throws Exception{    	System.out.println("========insertSuperTable start========");    	JdbcTemplate jt = (JdbcTemplate)ctx.getBean("jdbcTemplate");    	jt.execute(insertSuperTable1);    	//捕获RuntimeException    	try{    	    transactionSub.insertSubTable(ctx);    	}catch(RuntimeException re){    	    re.printStackTrace();    	}    	jt.execute(insertSuperTable2);    	System.out.println("========insertSuperTable end========");    }}

运行测试方法:

可以很明显的看出insertSubTable方法回滚了,而insertSuperTable方法却被成功执行了。同样的,如果在子事务方法执行完毕之后,父事务方法再抛出RuntimeException,那子事务也不会再回滚,因为子事务已经提交。这点跟采用加入当前事务的传播机制不一样,如果采用加入当前事务的方式,子事务的RuntimeException就算被父事务方法捕获父事务也一样被回滚,因为他们都在同一个connection的同一个事务之中。同理,由于他们在一个事务之中,就算子事务方法执行完毕后,父事务方法再抛出RuntimeException,子事务也一样会跟父事务一起回滚

②Throwable和Exception

在没有配置rollback-for属性的时候跟其他事务传播机制是一样的效果,都是在哪儿出异常就在哪儿提交。唯一不同的是:如果是加入当前事务的方式,只要整个事务中其中一个方法发生Exception并且其也配置了rollback-for属性,那整个事务都会回滚。而被REQUIRES_NEW标注的方法发生Exception并且配置了rollback-for属性只会回滚本方法,其他方法则在发生异常的地方提交(如果其他方法没配置rollback-for属性的前提下)。


7. NESTED

最后一个事务传播机制是NESTED,如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作,也就是新开事务。从字面上看好像跟REQUIRED没有什么区别,不就是有就加入,没有就新开吗?从这点来说他们确实大同小异,但他们最大的区别就是在发生Exception的处理上,NESTED引入了JDBC3.0的savepoint机制,我们在后面会详细说明,我们先来看看它的正常使用效果。

先修改配置文件:

      
     
 

父事务类:

@Componentpublic class TransactionSuper {    @Autowired    TransactionSub transactionSub;    String insertSuperTable1 = "insert into super_table values (1, 'super1')";    String insertSuperTable2 = "insert into super_table values (2, 'super2')";        public void insertSuperTable(AbstractApplicationContext ctx) throws Exception{    	System.out.println("========insertSuperTable start========");    	JdbcTemplate jt = (JdbcTemplate)ctx.getBean("jdbcTemplate");    	jt.execute(insertSuperTable1);    	transactionSub.insertSubTable(ctx);    	jt.execute(insertSuperTable2);    	System.out.println("========insertSuperTable end========");    }}

子事务类:

@Componentpublic class TransactionSub {    String insertSubTable1 = "insert into sub_table values (1, 'sub1')";    String insertSubTable2 = "insert into sub_table values (2, 'sub2')";    public void insertSubTable(AbstractApplicationContext ctx) throws Exception {        System.out.println("========insertSubTable start========");        JdbcTemplate jt = (JdbcTemplate) ctx.getBean("jdbcTemplate");        jt.execute(insertSubTable1);        jt.execute(insertSubTable2);        System.out.println("========insertSubTable end========");    }}

运行测试方法:

上面说到insertSubTable方法虽然表面上是开启了一个nested的事务,但实际上还是加入了insertSuperTable方法开启的事务。下面我们就用异常回滚来看看是否如此。

①RuntimeException

被NESTED标注的方法如果产生RuntimeException会导致事务回滚,但跟REQUIRED不同的是如果我们在父方法中捕获了这个异常,让父方法不在抛出RuntimeException,它就不会导致父事务的回滚,而REQUIRED就算我们进行捕获,父事务还是会回滚,因为他们都在一个事务里。那NESTED是怎么做到父子都在一个事务里,而不让父事务回滚的呢。答案就是:savepoint,如果子方法出现RuntimeException就会回滚到开始子事务的那个savepoint,如果父方法不再抛出,那么父方法就不会回滚,并可以继续执行。

我们这里让insertSubTable方法抛出RuntimeException异常,然后在insertSuperTable方法里进行捕获。我们来看看效果。

父事务类:

@Componentpublic class TransactionSuper {    @Autowired    TransactionSub transactionSub;    String insertSuperTable1 = "insert into super_table values (1, 'super1')";    String insertSuperTable2 = "insert into super_table values (2, 'super2')";        public void insertSuperTable(AbstractApplicationContext ctx) throws Exception{    	System.out.println("========insertSuperTable start========");    	JdbcTemplate jt = (JdbcTemplate)ctx.getBean("jdbcTemplate");    	jt.execute(insertSuperTable1);    	//捕获RuntimeException    	try{    	    transactionSub.insertSubTable(ctx);    	}catch(RuntimeException re){    	    re.printStackTrace();    	}    	jt.execute(insertSuperTable2);    	System.out.println("========insertSuperTable end========");    }}

子事务类:

@Componentpublic class TransactionSub {    String insertSubTable1 = "insert into sub_table values (1, 'sub1')";    String insertSubTable2 = "insert into sub_table values (2, 'sub2')";    public void insertSubTable(AbstractApplicationContext ctx) throws Exception {        System.out.println("========insertSubTable start========");        JdbcTemplate jt = (JdbcTemplate) ctx.getBean("jdbcTemplate");        jt.execute(insertSubTable1);        ExceptionUtils.throwRuntimeException(); //抛出RuntimeException        jt.execute(insertSubTable2);        System.out.println("========insertSubTable end========");    }}

运行测试方法:

我们这次不在insertSubTable方法里面抛RuntimeException,而是在insertSuperTable里面抛RuntimeException,看看会有什么效果:

父事务类:

@Componentpublic class TransactionSuper {    @Autowired    TransactionSub transactionSub;    String insertSuperTable1 = "insert into super_table values (1, 'super1')";    String insertSuperTable2 = "insert into super_table values (2, 'super2')";        public void insertSuperTable(AbstractApplicationContext ctx) throws Exception{    	System.out.println("========insertSuperTable start========");    	JdbcTemplate jt = (JdbcTemplate)ctx.getBean("jdbcTemplate");    	jt.execute(insertSuperTable1);    	transactionSub.insertSubTable(ctx);    	jt.execute(insertSuperTable2);    	ExceptionUtils.throwRuntimeException(); //抛出RuntimeException    	System.out.println("========insertSuperTable end========");    }}

子事务类:

@Componentpublic class TransactionSub {    String insertSubTable1 = "insert into sub_table values (1, 'sub1')";    String insertSubTable2 = "insert into sub_table values (2, 'sub2')";    public void insertSubTable(AbstractApplicationContext ctx) throws Exception {        System.out.println("========insertSubTable start========");        JdbcTemplate jt = (JdbcTemplate) ctx.getBean("jdbcTemplate");        jt.execute(insertSubTable1);        jt.execute(insertSubTable2);        System.out.println("========insertSubTable end========");    }}

执行测试方法:

可以看到在insertSuperTable方法抛出RuntimeException后,insertSubTable方法里的操作也回滚了。这就说明这两个方法的操作是在同一个事务里。NESTED开启的事务只不过是一个savepoint。

②Throwable和Exception

在没有配置rollback-for属性的时候跟其他事务传播机制是一样的效果,都是在哪儿出异常就在哪儿提交。唯一不同的是:如果是加入当前事务的方式,只要整个事务中其中一个方法发生Exception并且其也配置了rollback-for属性,那整个事务都会回滚。而被NESTED标注的方法发生Exception并且配置了rollback-for属性只会回滚到开始这个方法事务的savepoint,其他方法则在发生异常的地方提交(如果其他方法没配置rollback-for属性的前提下)。