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

5. REQUIRED

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

先修改配置文件:

<tx:attributes>  
    <tx:method name="insertSuperTable" propagation="REQUIRED"/> 
    <tx:method name="insertSubTable" propagation="REQUIRED"/> 
</tx:attributes>

父事务类:

@Component
public 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========");
    }
}

子事务类:

@Component
public 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========");
    }
}

然后运行测试方法

wKiom1cIaC3AlOl9AAEunIP7Uao075.png

wKioL1cIaN2AXPrRAAAJBEkhP1w981.png

wKiom1cIaC7C3WqWAAAIUXKTxKs213.png

如果有存在的事务,跟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标记的方法会新建事务,如果当前存在事务,把当前事务挂起,等待新开事务完成后,被挂起的事务再恢复执行。

先修改配置文件:

<tx:attributes>  
    <tx:method name="insertSuperTable" propagation="REQUIRED"/> 
    <tx:method name="insertSubTable" propagation="REQUIRES_NEW"/> 
</tx:attributes>

直接执行测试方法:

wKiom1cIx0qz18XLAAHJ3Z_-A8A169.png

wKiom1cIgFjBKyHiAAAITNcJ9Lk342.png

wKiom1cIgJLgSefAAAAIAPFXkYA552.png

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

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

①RuntimeException

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

子事务类:

@Component
public 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========");
    }
}

运行测试类:

wKiom1cIxu3Rjh--AAG9TqbFfR0511.png

wKioL1cIwYHhtLAnAAAH-NXZjTU609.png

wKiom1cIwNGwpsUjAAAHvRIscf8440.png

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

父事务类:

@Component
public 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========");
    }
}

运行测试方法:

wKioL1cIx1CRro8oAAHM3T9ccz0323.png

wKiom1cIxqCDLOHQAAAJaeNRzeo455.png

wKioL1cIx1HhNKCYAAAHcrqj4UQ952.png

可以很明显的看出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机制,我们在后面会详细说明,我们先来看看它的正常使用效果。

先修改配置文件:

<tx:attributes>  
    <tx:method name="insertSuperTable" propagation="REQUIRED"/> 
    <tx:method name="insertSubTable" propagation="NESTED"/> 
</tx:attributes>

父事务类:

@Component
public 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========");
    }
}

子事务类:

@Component
public 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========");
    }
}

运行测试方法:

wKioL1cI5YGRtysZAAFLhlTmowA832.png

wKiom1cI5NHjvEv1AAAI1RxS5s0570.png

wKiom1cI5NHwKFC3AAAHkyAMbHQ005.png

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

①RuntimeException

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

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

父事务类:

@Component
public 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========");
    }
}

子事务类:

@Component
public 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========");
    }
}

运行测试方法:

wKioL1cI8JLyi3tWAAFvFYcmBS4157.png

wKiom1cI7-GiiSVBAAAJQCpRZY4264.png

wKioL1cI8JKTOvivAAAIEaRahk4759.png

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

父事务类:

@Component
public 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========");
    }
}

子事务类:

@Component
public 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========");
    }
}

执行测试方法:

wKiom1cJDESCTnRwAAFtf1vMqgc900.png

wKioL1cJDPWjadNdAAAImGzqd90845.png

wKiom1cJDESDdI1DAAAHmHQ5o94281.png

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

②Throwable和Exception

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


实例详解Spring的事务传播机制(一)

实例详解Spring的事务传播机制(二)