由事务的传播行为我们知道, 如果将方法配置为默认事务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): 最高级别的隔离级别,通过对事务进行串行化执行,避免了脏读、不可重复读和幻读的问题。但这种隔离级别可能会导致并发性能下降。
这些隔离级别的选择取决于应用程序的需求和对数据一致性的要求。不同的数据库管理系统可能对隔离级别的实现有所不同。