spring 事务传播REQUIRED 与 NESTED的区别


总结

NESTED 似乎与REQUIRED 是一样的,但是他们是不同的。

若a 调用b 方法。a方法为REQUIRED,且在a中捕获b方法异常。注意 a ,b 方法不要在一个service中,不然事务不生效。

若b方法为REQUIRED

@Transactional(propagation=Propagation.REQUIRED)
a(){
       a插入数据库;
    try {
            b();
        } catch (Exception e) {
            e.printStackTrace();
        }

}
@Transactional(propagation=Propagation.REQUIRED)
b(){
    b插入数据库;
   int xx=1/0;//抛出异常
}

 则a,b的插入都会回滚,即使a中加了捕获b()异常的代码。同时a方法会抛出异常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only,异常原因可参考:Spring事务管理报错:Transaction rolled back because it has been marked as rollback-only_科西嘉狮子的博客-CSDN博客

若b方法为NESTED

@Transactional(propagation=Propagation.REQUIRED)
a(){
       a插入数据库;
    try {
            b();
        } catch (Exception e) {
            e.printStackTrace();
        }

}
@Transactional(propagation=Propagation.NESTED)
b(){
    b插入数据库;
   int xx=1/0;//抛出异常
}

上述代码 则只有b的插入回滚,a不回滚。原因是在进入NESTED方法时事务保存了当前b的savepoint,b异常只会回滚到b的savepoint。

同时若在a方法中抛出异常,则a,b插入都会回滚

@Transactional(propagation=Propagation.REQUIRED)
a(){
       a插入数据库;
    try {
            b();
        } catch (Exception e) {
            e.printStackTrace();
        }
   int xx=1/0;//抛出异常

}
@Transactional(propagation=Propagation.NESTED)
b(){
    b插入数据库;

}

参考:

Spring嵌套事务异常Transaction rolled back because it has been marked as rollback-only_Black·Tea的博客-CSDN博客_spring事务嵌套异常处理

 

 

亲测代码示例

@Service
public class Test3Service {
	@Autowired
	private TestMapper testMapper;
	@Autowired
	private Test4Service test4Service;
	   @Transactional(propagation = Propagation.REQUIRED)
	   public void test1() {
		   testMapper.add3();
		   try {
			  //add3,add4 都回滚,同时当test1()方法抛出异常Transaction rolled back because it has been marked as rollback-only"
			   test4Service.add4();
			  //add3不会滚,add4_1回滚
//			   test4Service.add4_1();
			 //add3不会滚,add4_2也不回滚
//			   test4Service.add4_2();
			} catch (Exception e) {
				e.printStackTrace();
			}
	   }
	   
	   @Transactional(propagation = Propagation.REQUIRED)
	   public void test2() {
		   testMapper.add3();
		   test4Service.add4_0();
		   //add3 add4_0都回滚
		   int i=1/0;
	   }
}
@Service
public class Test4Service {
	@Autowired
	private TestMapper testMapper;
	 @Transactional(propagation = Propagation.REQUIRED)
	   public void add4() {
		 testMapper.add4();//插入mysql
		 int a=1/0;
	   }
	 @Transactional(propagation = Propagation.NESTED)
	   public void add4_0() {
		 testMapper.add4();//插入mysql
	   }
	   @Transactional(propagation = Propagation.NESTED)
	   public void add4_1() {
		 testMapper.add4();//插入mysql
		 int a=1/0;
	   }
	   //不要事务
	   public void add4_2() {
		 testMapper.add4();//插入mysql
		 int a=1/0;
	   }
}
@Mapper
@Repository
public interface TestMapper {
	@Insert("insert into test3 (name) values('2')")
	int add3();
	@Insert("insert into test4 (name) values('2')")
	int add4();
}

关于spring事务传播机制的解释

播行为意义
PROPAGATION_REQUIRES表示当前方法必须在一个事务中运行。如果一个现有事务正在进行中,该方法将在那个事务中运行,否则就要开始一个新事务。
PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务则以非事务方式执行

外围方法未开启事务,插入用户表和用户角色表的方法以非事务的方式独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚,所以两个记录都插入失败。

PROPAGATION_MANDATORY表示该方法必须运行在一个事务中。如果当前没有事务正在发生,将抛出一个异常。
PROPAGATION_REQUIRES_NEW表示当前方法必须在它自己的事务里运行。一个新的事务将被启动,而且如果有一个现有事务在运行的话,则将在这个方法运行期间被挂起。
PROPAGATION_NOT_SUPPORTED

以非事务的方式执行,如果当前有事务则把当前事务挂起。

外围方法未开启事务,插入用户表和用户角色表的方法在自己的事务中独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,两个数据新增成功。

PROPAGATION_NEVER表示当前的方法不应该在一个事务中运行。如果一个事务正在进行,则会抛出一个异常。
PROPAGATION_NESTED表示如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRES一样。

Spring支持7中事务传播行为

一个场景:假设外层方法里面包含二个新增用户和新增角色的方法,二个方法后面还会抛一个异常。

propagation_required(需要传播)


当前没有事务则新建事务,有则加入当前事务。

外围方法未开启事务,插入用户表和用户角色表的方法在自己的事务中独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚,所以两个记录都插入失败。

propagation_supports(支持传播)


支持当前事务,如果当前没有事务则以非事务方式执行

外围方法未开启事务,插入用户表和用户角色表的方法以非事务的方式独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚,所以两个记录都插入失败。

propagation_mandatory(强制传播)


使用当前事务,如果没有则抛出异常

外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚,所以两个记录都插入失败。 外围方法没有开启事务,两张表数据都为空,在调用用户新增方法时候已经报错了,所以两个表都没有新增数据。

propagation_nested(嵌套传播)


如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则执行需要传播行为。

外围方法开启事务抛出异常,父事务最后回滚,二个方法也进行回滚的,两个表都没有新增数据。 外层方法未开启事务,插入用户表和用户角色表的方法在自己的事务中独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。

propagation_never(绝不传播)


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

外围方法开启事务则抛出异常,两个表都没有新增数据 外层方法未开启事务会以非事务的方式运行,两个表新增成功;

propagation_requires_new(传播需要新的)


新建事务,如果当前有事务则把当前事务挂起,创建新的事务。

无论当前存不存在事务,都创建新事务,所以两个数据新增成功。

propagation_not_supported(不支持传播)


以非事务的方式执行,如果当前有事务则把当前事务挂起。

外围方法未开启事务,插入用户表和用户角色表的方法在自己的事务中独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,两个数据新增成功。
 

我来通过实验证明NESTED和REQUIRED的区别

这个例子是基于 spring 事务管理之事务传播行为之实践REQUIRED(一) - 简书 这个文章的代码

首先,InsertUsers和InsertCuser方法上都申明了REQUIRED,让他们属于同一个事务。将引发异常的语句 int i = 1/0; 放到 InsertCuser方法里

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void InsertUsers(Users users) {
        jdbcTemplate.update("INSERT INTO users(id,name, age, email) VALUES (?, ?, ?, ?);", users.getId(), users.getName(), users.getAge(), users.getEmail());
        //调用service中另一个方法
        Cuser cuser = new Cuser(users.getId(), users.getName(), users.getAge(), users.getEmail());
        //打印事务名
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID( );");
        System.out.println(maps + TransactionSynchronizationManager.getCurrentTransactionName());
        //对InsertCuser抛出的异常进行捕获处理,并且不再向上抛出
        try {
            cuserService.InsertCuser(cuser);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  @Transactional(propagation=Propagation.REQUIRED)
    @Override
    public void InsertCuser(Cuser cuser) {
 
        jdbcTemplate.update("INSERT INTO cuser(id,name, age, email) VALUES (?, ?, ?, ?);", cuser.getId(), cuser.getName(), cuser.getAge(), cuser.getEmail());
        //打印事务名
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID( );");
        System.out.println(maps + TransactionSynchronizationManager.getCurrentTransactionName());
        int i = 1/0;
    }

注意 为什么要加try/catch包裹cuserService.InsertCuser(cuser);语句?

为了杜绝InsertCuser中抛出的异常影响InsertUsers方法的实验结果

try {
            cuserService.InsertCuser(cuser);
        } catch (Exception e) {
            e.printStackTrace();
        }

程序运行,结果是InsertCuser中出现异常,导致事务回滚、users表和cuser表均无数据插入。由于两个方法被纳入同一个事务,因此两者都会回滚。即使在cuserService.InsertCuser(cuser);上使用try/catch捕获并不抛出异常也没用(此方法能保证调用者方法中的独立事务不受被调用者抛出的异常影响而回滚)

我们再来看,将上面环境的InsertCuser方法传播行为改成NESTED

   @Transactional(propagation=Propagation.NESTED)
    @Override
    public void InsertCuser(Cuser cuser) {
 
        jdbcTemplate.update("INSERT INTO cuser(id,name, age, email) VALUES (?, ?, ?, ?);", cuser.getId(), cuser.getName(), cuser.getAge(), cuser.getEmail());
        //打印事务名
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID( );");
        System.out.println(maps + TransactionSynchronizationManager.getCurrentTransactionName());
        int i = 1/0;
    }

 再次运行,可以看到日志的打印情况。两方法的事务的id一致,说明的确是相同事务

users表中插入了数据说明InsertUsers方法提交成功,cuser表中没有数据说明InsertCuser方法回滚

那么现在就有一个问题了,既然两个方法使用同一个事务,为什么没有一起回滚?

这就是NESTED嵌套事务的奥秘之处-----它能让事务部分回滚

我在网上找到了一句话:

NESTED申明在被调用方法上,若调用者方法有开启事务。此时NESTED会开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务。 潜套事务开始执行时, 它将取得一个 savepoint。 如果这个嵌套事务失败, 我们将回滚到此 savepoint。 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

这段话中提到的 savepoint 其实是mysql的innodb引擎的特性,为了去了解它我在mysql客户端对它进行了简单使用,可以看看这篇文章mysql 事务之使用savepoint部分回滚 - 简书 。 总之它就是一个保存点,生成一个保存点就是生成一个数据镜像。然后无论经过了什么sql操作,只要使用回滚至此保存点的命令即可恢复至创建保存点的数据状态。

那么上面代码的演示结果也就说的通了。即使InsertUsers和InsertCuser方法属于同一个事务,被NESTED嵌套事务申明的InsertCuser方法出现异常也没导致REQUIRED申明的InsertUsers的全部回滚,只是部分回滚到了调用InsertCuser方法之前。因为在调用InsertCuser方法时会自动生成一个savepoint

 InsertUsers方法里出现异常会导致InsertCuser方法嵌套事务回滚吗?

将出现异常的代码行放到这里,结果都回滚了,毕竟是同一个事务

总结下NESTED的回滚特性

  • 主事务和嵌套事务属于同一个事务
  • 嵌套事务出错回滚不会影响到主事务
  • 主事务回滚会将嵌套事务一起回滚了

进一步证明NESTED嵌套事务的savepoint机制

可以通过阅读spring源码的方式来验证NESTED是不是使用savepoint机制来实现的,我现在的水平还不足以去阅读源码。但是我会很快就有这个能力的,我相信! 不过有简友已经分析过了,可以去看看。写的很好~
Spring 采用保存点(Savepoint)实现嵌套事务原理 - 简书

不同点的源码解读

业务同步单子的逻辑很简单,把原单重复发起,我们这边做下幂等返回库中的数据就好,其实这里已经差不多能看出原因了,处理逻辑中套了两层事务,我们知道@Transaction这个注解是基于切面实现的,类似于下面的代码

public void testTransactional(){
        DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
        defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus transaction = transactionManager.getTransaction(defaultTransactionDefinition);
        try{
            //do something
        } catch (Exception e) {
            transactionManager.rollback(transaction);
            throw e;
        }
        transactionManager.commit(transaction);
    }


最外面一层采用的默认REQUIRED传播方式,内层插入也采用这种插入方式,并且内外层是一个事务,当内层事务抛出异常的时候会回滚整个事务,所以下面查询库中信息就有问题了,事务已经回滚了,因此抛出上述异常。

NESTED事务跟REQUIRED事务区别就在这里,NESTED事务是回滚到回滚点,而回滚点生成是在进入内嵌事务的时候,外面事务是不会回滚的

具体创建回滚点代码是在这里

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            if (!isNestedTransactionAllowed()) {
                throw new NestedTransactionNotSupportedException(
                        "Transaction manager does not allow nested transactions by default - " +
                        "specify 'nestedTransactionAllowed' property with value 'true'");
            }
            if (debugEnabled) {
                logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
            }
            if (useSavepointForNestedTransaction()) {
                // Create savepoint within existing Spring-managed transaction,
                // through the SavepointManager API implemented by TransactionStatus.
                // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
                DefaultTransactionStatus status =
                        prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
                status.createAndHoldSavepoint();
                return status;
            }
            else {
                // Nested transaction through nested begin and commit/rollback calls.
                // Usually only for JTA: Spring synchronization might get activated here
                // in case of a pre-existing JTA transaction.
                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, null);
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }
}


处理回滚点是在processRollback中

           

     if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        logger.debug("Rolling back transaction to savepoint");
                    }
                    status.rollbackToHeldSavepoint();
                }
                else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction rollback");
                    }
                    doRollback(status);
                }

@Override
    public void rollbackToSavepoint(Object savepoint) throws TransactionException {
        ConnectionHolder conHolder = getConnectionHolderForSavepoint();
        try {
            conHolder.getConnection().rollback((Savepoint) savepoint);
        }
        catch (Throwable ex) {
            throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
        }
    }

@Override
    protected void doRollback(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
        }
        try {
            con.rollback();
        }
        catch (SQLException ex) {
            throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
        }
    }


 

 参考:

spring事务传播机制NESTED和REQUIRED的区别_wit_cx的博客-CSDN博客_nested和required的区别

Spring事务传播方式 REQUIRED 与 NESTED 踩坑_Carisy的博客-CSDN博客

  • 13
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

life1024

你的鼓励将是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值