关于Spring事务管理之默认事务间调用问题

发布于:2024-04-24 ⋅ 阅读:(21) ⋅ 点赞:(0)

由事务的传播行为我们知道, 如果将方法配置为默认事务REQUIRED在执行过程中Spring会为其新启事务REQUIRES_NEW, 作为一个独立事务来执行. 由此存在一个问题。

如果使用不慎, 会引发org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it

具体原因见以下demo简例:部分关键代码DemoService1

public class DemoService1Impl implements DemoService1 {
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	@Resource
	private DemoDao demoDao;
	@Resource
	private DemoService2 demoService2;
	/**
	* 业务逻辑 , 默认事务, 事务回滚异常 : Exception
	*/
	@Override
	@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
	public void doService() {
		HashMap<String, Integer> param = new HashMap<>(2);
		param.put("applId", 19);
		param.put("code", 19);
		demoDao.insert1(param);
		try {
			demoService2.doService();
		} catch (Exception e) {
			logger.error("业务2处理异常,{}", e.getMessage());
		}
	}
}

DemoService2

public class DemoService2Impl implements DemoService2 {
	@Resource
	private DemoDao demoDao;
	/**
	* 业务逻辑, 默认事务, 事务回滚异常 : Exception
	*/
	@Override
	@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
	public void doService() {
		HashMap<String, Integer> param = new HashMap<>(2);
		param.put("applId", 10);
		param.put("code", 10);
		demoDao.insert2(param);
		throw new RuntimeException("因为一些原因,我处理失败了.");
	}
}

单元测试

public class DemoService1ImplTest extends BaseTest {
	@Resource
	private DemoService1 demoService1;
	@Test
	public void doService() {
		demoService1.doService();
	}
}

说明: 这里用到的事务配置为注解方式, 目前我们项目开发过程中使用配置文件方式, 一般为以下方式。 这种方式的事务配置, 更容易引起问题

<tx:advice id="txAdvice" transaction-
manager="transactionManager">
<tx:attributes>
...
<tx:method name="do*" />
<tx:method name="doNew*" propaga-
tion="REQUIRES_NEW" />
...
</tx:attributes>
</tx:advice>

执行结果

27:38 [DEBUG] -
[com.erayt.cms.cms.dao.DemoDao.insert1] prepare
sql:[ insert into ...
27:38 [DEBUG] -
[com.erayt.cms.cms.dao.DemoDao.insert1] prepare
parameters:[19, 19] ...
27:38 [DEBUG] - {pstm-100001} Executing State-
ment: insert into ...
27:38 [DEBUG] - {pstm-100001} Types: [java.lang.Integer, java.lang.Integer] ...
27:38 [DEBUG] - [com.erayt.cms.cms.dao.DemoDao.insert2] prepare sql:[ insert into ...
27:38 [DEBUG] - [com.erayt.cms.cms.dao.DemoDao.insert2] prepare parameters:[10, 10] ...
27:38 [DEBUG] - {conn-100002} Preparing Statement: insert into ...
27:38 [DEBUG] - {pstm-100003} Types: [java.lang.Integer, java.lang.Integer] ...
27:38 [ERROR] - 业务2处理异常,因为一些原因,我处理失败了.
27:38 [WARN ] - Caught exception while allowing TestExecutionListener ...
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been
marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit ...
at org.springframework.test.context.transaction.TransactionContext.endTransaction ...
at org.springframework.test.context.transaction.TransactionalTestExecutionListener.afterTestMethod ...
at org.springframework.test.context.TestContextManager.afterTestMethod ...

问题分析: 问题出现的代码为

try {
	demoService2.doService();
} catch (Exception e) {
	logger.error("业务2处理异常,{}", e.getMessage());
}

问题原因是因为两个service中的方法doService均为默认事务REQUIRED, 默认事务再被调用时, 如外层方法无事务, 自身会新启事务。

此时#demoService1.doService() 的事务则为新启事务REQUIRES_NEW), 之后再被调用的方法#demoService2.doService() 会加入到调用者 #demoService1.doService() 事务中。

又由于spring的事务回滚依托在异常之上, 当demoService2.doService()出现异常后它将事务标记为回滚。异常抛出后被catch

demoService1.doService没有接受到里面抛出的异常, 方法继续执行, 执行结束后, 事务提交。

但当demoService1在做commit的时候检测到事务被标记为回滚, 与预期不符, 也就是Unexpected意想不到的UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

在这里插入图片描述

事务的传播定义

下面列举了各公司框架使用到的亊务传播部分说明,还有些不常用传播行为,因为实际使用的少,大家在网上了解下就行了。

传播行为 意义
PROPAGATION_REQUIRED 表示当前方法必须运行在一个事务中,如果当前存在一个事务,那么该方法运行在这个事务中,否则,将创建一个新的事务
PROPAGATION_REQUIRES_NEW 新建事务,表示当前方法必须运行在自己的事务中,如果当前存在一个事务,那么这个事务将在该方法运行期间被挂起
PROPAGATION_NESTED 表示如果当前事务存在,则方法应该运行在一个嵌套事务中。否则,它看起来和PROPAGATION_REQUIRED看起来没什么俩样

主子事务存在嵌套行为,嵌套是子事务套在父事务的一部分,在进入事务之前,父事务建立一个回滚点,叫save point,然后执行子亊务,这个子亊务的执行也算是父亊务的一部分,然后子亊务执行结束,父亊务继续执行。重点就在二那个save point。下面癿几个问题加深下大家的理解,对二嵌套亊务问题说明:
【1】如果子亊务回滚,会发生什么? 父亊务会回滚到进入子亊务前建立的save point,然后尝试其它的亊务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。
【2】如果父亊务回滚,会収生什么? 父亊务回滚,子亊务也会跟着回滚!为什么呢,因为父亊务结束之前,子亊务是不会提交的,我们说子亊务是父亊务的一部分,正是这个道理。
【3】亊务癿提交癿顺序什么? 父亊务先提交,然后子亊务提交,还是子亊务先提交,父亊务再提交?还是那句话,子亊务是父亊务的一部分,由父亊务统一提交。

数据库的隔离级别有哪几种?
【1】读未提交(Read Uncommitted): 最低级别的隔离级别,允许一个事务读取另一个事务尚未提交的数据。这种隔离级别可能导致脏读(Dirty Read)问题。
【2】读已提交(Read Committed): 在一个事务读取数据时,只能读取已经提交的数据。这种隔离级别可以避免脏读,但可能会导致不可重复读Non-Repeatable Read问题。
【3】可重复读(Repeatable Read): 在一个事务读取数据时,保证多次读取同一数据时,读取的结果保持一致。这种隔离级别可以避免脏读和不可重复读,但可能会导致幻读Phantom Read问题。
【4】串行化(Serializable): 最高级别的隔离级别,通过对事务进行串行化执行,避免了脏读、不可重复读和幻读的问题。但这种隔离级别可能会导致并发性能下降。
这些隔离级别的选择取决于应用程序的需求和对数据一致性的要求。不同的数据库管理系统可能对隔离级别的实现有所不同。