【前言】事务传播(Transactional Propagation)是多个事务方法相互调用时,事务如何在这些方法间传播的过程机制。本文将针对Spring中事务的七种传播行为机制分别展开介绍。

PART1、事务传播枚举

Spring事务传播类型枚举Propagation定义了七种类型,分别是:

  • REQUIRED:如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。
  • SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续执行。
  • MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
  • REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在事务,则把当前事务挂起。
  • NEVER:以非事务的方式执行操作,如果当前存在事务,则抛出异常。
  • NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前不存在事务,则创建一个新的事务。如果主事务提交,则提交所有的嵌套事务。如果任何一个嵌套事务失败,则回滚所有的嵌套事务,但不影响主事务的提交。

在Spring源码中这七种类型被定义为了枚举,源码在org.springframework.transaction.annotation包下的Propagation中可以看到:
在这里插入图片描述
我们以向两张不同的表插入数据为例,创建insertA和insertB两个方法:

public void insertA(Data a) {
	// 插入tableA的操作
	insertToA(a);
} 

public void insertB(Data b) {
    // 插入tableB的操作
	insertToB(b);
}

注意:在Spring中,事务是基于AOP实现的,即使用代理类。如果同一类中的没有@Transactional注解的方法A内部调用有@Transactional注解的方法B,则有@Transactional注解的方法B的事务会失效。原因是在自调用过程中,是类自身调用,而不是代理对象去调用,那么就不会产生AOP,即事务会失效。只有当@Transactional注解的方法在类以外被调用的时候,Spring事务管理才生效。

接下接,为了防止类内部方法调用,未通过代理调用,导致事务失效的问题,我们通过使用AopContext.currentProxy()来获取代理类再调用的方式,创建测试类:

// 前置:@EnableAspectJAutoProxy(exposeProxy = true) 配置exposeProxy
@Component
public class Test{
    
	public void testMain(){
		insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB(); 
	}

	public void testB(){
		// 参数b1插入tableB的操作
    	insertToB(b);
    	throw new RuntimeExecption();
    	// 参数b2插入tableB的操作
    	insertToB(b2);
	}
}

在以上伪代码中,未加任何事务的情况下,调用testMain()测试方法,数据a和数据b分别插入A表和B表中,但是数据b2无法插入B表中,在具体的应用场景下,会带来数据一致性等问题,所以事务的重要性不言而喻。

PART2、传播机制举例详解

1、REQUIRED(Spring默认的事务传播类型)

如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。

	/**
	 * Support a current transaction, create a new one if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>This is the default setting of a transaction annotation.
	 */
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

这个图可以方便我们理解REQUIRED传播机制:
在这里插入图片描述

继续复用上面场景,示例1:

@Component
public class Test{
     
	public void testMain(){
		insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB(); 
	}
    
	@Transcational(propagation = Propagation.REQUIRED)
	public void testB(){
		// 参数b1插入tableB的操作
    	insertToB(b);
    	throw new RuntimeExecption();
    	// 参数b2插入tableB的操作
    	insertToB(b2);
	}
}

此时testMain方法上没有事务,testB方法上声明了事务,且传播行为是REQUITED,则testMain调用testB时,会创建一个新的事务

所以,testMain中插入A表数据a会成功执行,而向B表存储数据b,会因为异常,导致事务回滚,b和b2无法存储到B表中。

示例2:

public class Test{
	
    @Transcational(propagation = Propagation.REQUIRED)
	public void testMain(){
		insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB();  
	}
    
	@Transcational(propagation = Propagation.REQUIRED)
	public void testB(){
		// 参数b1插入tableB的操作
    	insertToB(b);
    	throw new RuntimeExecption();
    	// 参数b2插入tableB的操作
    	insertToB(b2);
	}
}

此时testMain方法和testB方法上都声明了事务,且传播行为是REQUITED。则testMain执行时,会创建一个新的事务,而testB在执行的时候,因为当前存在事务,则加入到testMain的事务中

所以,testMain中储存数据a和testB中存储数据b,会因为异常,进行事务回滚,导致相关数据不会在数据库中进行存储改变。

2、SUPPORT

如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续执行。

	/**
	 * Support a current transaction, execute non-transactionally if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>Note: For transaction managers with transaction synchronization,
	 * {@code SUPPORTS} is slightly different from no transaction at all,
	 * as it defines a transaction scope that synchronization will apply for.
	 * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
	 * will be shared for the entire specified scope. Note that this depends on
	 * the actual synchronization configuration of the transaction manager.
	 * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
	 */
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

继续复用上面场景,示例:

public class Test{
    
	public void testMain(){
		insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB();  
	}
    
	@Transcational(propagation = Propagation.SUPPORT)
	public void testB(){
		// 参数b1插入tableB的操作
    	insertToB(b);
    	throw new RuntimeExecption();
    	// 参数b2插入tableB的操作
    	insertToB(b2);
	}
}

此时testMain方法未声明事务,而testB方法上声明了事务,且传播行为是SUPPORT。当testMain调用testB时,因为当前事务不存在,则testB将以非事务的方式继续执行。

所以整体不存在事务回滚,a数据和b数据将被存储,b2数据因为异常无法存储。

其他情形:如果testMain上也声明有事务标志,且为默认的传播方式,则所有的数据都将回滚。

3、MANDATORY

如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。

	/**
	 * Support a current transaction, throw an exception if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

示例:

public class Test{
    
	public void testMain(){
		insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB();  
	}
    
	@Transcational(propagation = Propagation.MANDATORY)
	public void testB(){
		// 参数b1插入tableB的操作
    	insertToB(b);
    	throw new RuntimeExecption();
    	// 参数b2插入tableB的操作
    	insertToB(b2);
	}
}

此时数据a将存储成功,当执行testB时因为testMain不存在事务,则会抛出异常,数据b和b2都不会入库存储。

其他情形:如果testMain上也声明有事务标志,且为默认的传播方式,则testB会加入testMain的事务中,所有的数据都将回滚。

4、REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。

在这里插入图片描述

该传播方式换句话说,就是无论当前事务是否存在,都会创建一个新的事务。

	/**
	 * Create a new transaction, and suspend the current transaction if one exists.
	 * Analogous to the EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 * on all transaction managers. This in particular applies to
	 * {@link org.springframework.transaction.jta.JtaTransactionManager},
	 * which requires the {@code jakarta.transaction.TransactionManager} to be
	 * made available to it (which is server-specific in standard Jakarta EE).
	 * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

示例:

public class Test{
 
    @Transcational(propagation = Propagation.REQUIRED)
    public void testMain(){
        insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB(); 
        throw new RuntimeExecption();
    }

    @Transcational(propagation = Propagation.REQUIRES_NEW)
    public void testB(){
        // 参数b1插入tableB的操作
        insertToB(b);
        // 参数b2插入tableB的操作
        insertToB(b2);
    }
}

此时testMain和testB都声明了事务,testMain的传播行为是REQUIRED,testB的传播行为是REQUIRES_NEW,当testMain调用testB时,testB会创建一个新的事务,该事务是独立于testMain的事务的。

所以,上述示例中,数据b和b2会储存成功,而数据a因为testMain方法中的异常,会进行数据回滚,导致数据a存储失败。

5、NOT_SUPPORTED

以非事务的方式执行操作,如果当前存在事务,则把当前事务挂起。

这个传播方式表明了当前方法不支持事务回滚。在执行时,不论当前是否存在事务,都会以非事务的方式运行。

	/**
	 * Execute non-transactionally, suspend the current transaction if one exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 * on all transaction managers. This in particular applies to
	 * {@link org.springframework.transaction.jta.JtaTransactionManager},
	 * which requires the {@code jakarta.transaction.TransactionManager} to be
	 * made available to it (which is server-specific in standard Jakarta EE).
	 * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
	 */
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

示例:

public class Test{
 
    @Transcational(propagation = Propagation.REQUIRED)
    public void testMain(){
        insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB();    
    }

    @Transcational(propagation = Propagation.NOT_SUPPORTED)
    public void testB(){
        // 参数b1插入tableB的操作
        insertToB(b);
        throw new RuntimeExecption();
        // 参数b2插入tableB的操作
        insertToB(b2);
    }
}

在上述示例中,因为testB事务的传播方式为NOT_SUPPORTED,所以该方法不执行事务回滚,在testB执行时,当完成数据b的存储后,会抛出一个异常,导致testMain方法的事务回滚,但是testB不会进行回滚。

所以,数据b存储成功,数据a和b2因为事务回滚和异常中断导致存储失败。

6、NEVER

以非事务的方式执行操作,如果当前存在事务,则抛出异常。

这个传播方式严格标注了当前方法绝不支持事务的特点,与MANDATORY恰好相反。

	/**
	 * Execute non-transactionally, throw an exception if a transaction exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	NEVER(TransactionDefinition.PROPAGATION_NEVER),

示例如下:

public class Test{

    @Transcational
    public void testMain(){
        insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB();   
    }

    @Transcational(propagation = Propagation.NEVER)
    public void testB(){
        // 参数b1插入tableB的操作
        insertToB(b);
        // 参数b2插入tableB的操作
        insertToB(b2);
    }
}

由于testMain存在事务声明,而testB事务传播方式为NEVER,不允许当前存在事务。

所以上述示例会抛异常,所有数据存储失败。

7、NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前不存在事务,则创建一个新的事务。

如果主事务提交,则提交所有的嵌套事务。如果任何一个嵌套事务失败,则回滚所有的嵌套事务,但不影响主事务的提交。

示例1:

public class Test{

    @Transcational(propagation = Propagation.REQUIRED)
    public void testMain(){
        insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        test.testB(); 
        throw new RuntimeExecption();
    }

    @Transcational(propagation = Propagation.NESTED)
    public void testB(){
        // 参数b1插入tableB的操作
        insertToB(b);
        // 参数b2插入tableB的操作
        insertToB(b2);
    }
}

testB声明了嵌套事务传播方式,testMain在执行最后抛出异常会导致主事务回滚,testB中的嵌套事务会伴随回滚。

所以所有数据都未存储成功。

示例2:

public class Test{

    @Transcational(propagation = Propagation.REQUIRED)
    public void testMain(){
        insertA(a); 
        Test test = (Test)AopContext.currentProxy();
        try {
        	test.testB(); 
        } catch (Exception e) {
        	// ...
        }
        
    }

    @Transcational(propagation = Propagation.NESTED)
    public void testB(){
        // 参数b1插入tableB的操作
        insertToB(b);
        throw new RuntimeExecption();
        // 参数b2插入tableB的操作
        insertToB(b2);
       
    }
}

testB声明了嵌套事务传播方式,testB在执行最后抛出异常会导致嵌套事务回滚,但不影响主事务的回滚。

所以,a数据存储成功,b数据和b2数据存储失败。

其他情况:该示例中,如果我们把testB的传播类型改为REQUIRED,即使testMain调用时catch了异常,整个事务还是会回滚,因为,调用方和被调方共用的同一个事务中。

总结

@Transactional注解可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有public方法将都具有该类型的事务属性。当作用在方法级别时,会覆盖类级别的定义。当作用在接口和接口方法时,则只有在使用基于接口的代理时它才会生效,也就是JDK代理。

在Spring中,事务的传播类型对方法执行顺序和结果都有影响。调用者和被调用者方法所定义的事务传播类型共同决定了代码的结果。

ps.两个问题:

Q1:类内部方法调用时,避免@Transcational失效的获取代理类方法待验证。

Q2: MANDATORY传播和NEVER传播,满足传播机制的异常触发条件时,是在testMain执行时抛异常,还是调用testB的时候抛异常?待验证。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐