前言
最近面试有被问到,Spring中事务的传播机制是什么?如果两个事务的隔离级别不一致,会有什么影响?这一块平时用的少,看了Spring中事务的传播机制相关知识,发现竟然有7种,这一下子哪里能记得住,所以想通过代码实践来加深一下自己的印象。
理论
参考文章:
Transaction Propagation and Isolation in Spring @Transactional
查看源码知道有这7种传播机制
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
Spring中这七种Propagation类的事务属性详解:
REQUIRED:(默认值)支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。
代码实践
每次测试都重置一下数据
@BeforeEach
void setUp() {
Account account1 = new Account(1, "jack", BigDecimal.valueOf(4));
Account account2 = new Account(2, "rose", BigDecimal.valueOf(6));
accountMapper.updateByPrimaryKeySelective(account1);
accountMapper.updateByPrimaryKeySelective(account2);
assert 4 == getBalance(1L);
assert 6 == getBalance(2L);
log.info("启动检测通过");
}
检验项目的事务是否正常
@Transactional
public void test1() {
// 事务里面给账户1扣除1块钱,账户2加上1块钱。但是2/0会异常,所以事务会回滚。
accountService.minus1Money();
int i = 2 / 0;
accountService.plus2Money();
}
@Test
void test1() {
log.info("开始测试当前项目配置下,事务是否正常开启");
try {
testTransactionService.test1();
} catch (Exception e) {
log.info("捕捉到异常: " + e.getMessage());
}
// 测试下来,数据应该被回滚了
assert 4 == getBalance(1L);
assert 6 == getBalance(2L);
}
测试默认的传播机制
/**
* REQUIRED (默认传播行为)
* 支持当前事务,如果当前没有事务,就新建一个事务
*/
@Test
void test_REQUIRED() {
log.info("事务嵌套, 传播级别都是默认的");
List<Long> sessionIds = testTransactionService.test3();
log.info(sessionIds.toString());
// 默认传播机制下,加入第一个事务,所以session id是同一个
assert sessionIds.get(0).equals(sessionIds.get(1));
}
//以下是testTransactionService.test3()
@Transactional
public List<Long> test3() {
accountService.minus1Money();
Long sessionId1 = getSessionId();
Long sessionId2 = self.plus2Money();
return Arrays.asList(sessionId1, sessionId2);
}
@Transactional
public Long plus2Money() {
accountService.plus2Money();
return getSessionId();
}
测试REQUIRES_NEW传播机制
/**
* REQUIRES_NEW
* 开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起
*/
@Test
void test_REQUIRES_NEW() {
log.info("测试嵌套事务,传播机制为REQUIRES_NEW");
List<Long> sessionIds = testTransactionService.test4();
log.info(sessionIds.toString());
// REQUIRES_NEW传播机制下,创建新事务,所以session id不是同一个
assert !sessionIds.get(0).equals(sessionIds.get(1));
}
//下面是testTransactionService.test4()
@Transactional()
public List<Long> test4() {
accountService.minus1Money(); // 开启了第一个事务
Long sessionId1 = getSessionId();
Long sessionId2 = self.plus2Money_REQUIRES_NEW();//创建新事务,并提交
return Arrays.asList(sessionId1, sessionId2);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Long plus2Money_REQUIRES_NEW() {
accountService.plus2Money();
return getSessionId();
}
测试传播机制对事务的隔离级别有什么影响
@Test
void test3() throws InterruptedException {
// 测试默认传播机制下,不同隔离级别会有什么影响
// 测试步骤:
// 使用新线程,打开事务0,修改id=1的余额-1,5s后提交
Thread thread = new Thread(() -> {
testTransactionService.delayTransaction();
});
thread.start();
Thread.sleep(1000);
// 使用声明式事务打开事务1(读已提交),看是否能读取到id=1的修改, 嵌套一个事务(读未提交),使用默认传播机制
List<Account> accounts = testTransactionService.test_REQUIRED_isolation(1L);
assert accounts.get(0).getBalance().intValue() == 4;
assert accounts.get(1).getBalance().intValue() == 4; // 隔离级别沿用已经存在的那个事务,读已提交,所以看不到修改
thread.join();
log.info("事务0结束, 再次查看数据");
accounts = testTransactionService.test_REQUIRED_isolation(1L);
assert accounts.get(0).getBalance().intValue() == 3;
assert accounts.get(1).getBalance().intValue() == 3;
}
@Test
void test4() throws InterruptedException {
// 测试REQUIRES_NEW传播机制下,不同隔离级别会有什么影响
// 测试步骤:
// 使用新线程,打开事务0,修改id=1的余额-1,5s后提交
Thread thread = new Thread(() -> {
testTransactionService.delayTransaction();
});
thread.start();
Thread.sleep(1000);//确保事务执行了
// 使用声明式事务打开事务1(读已提交),看是否能读取到id=1的修改, 嵌套一个事务(读未提交),使用REQUIRES_NEW传播机制
List<Account> accounts = testTransactionService.test_REQUIRES_NEW_isolation(1L);
assert accounts.get(0).getBalance().intValue() == 4;
assert accounts.get(1).getBalance().intValue() == 3; //隔离级别使用新创建的那个事务,使用的是读未提交,所以未提交的事务能被看到
thread.join();
log.info("事务0结束,再次查看数据");
accounts = testTransactionService.test_REQUIRES_NEW_isolation(1L);
assert accounts.get(0).getBalance().intValue() == 3;
assert accounts.get(1).getBalance().intValue() == 3;
}
结论
Spring中事务传播有7种机制,重点记住默认的REQUIRED和另一个叫REQUIRES_NEW的。 默认情况下,如果已经开启了事务就直接沿用原来的事务即可。 REQUIRES_NEW的话,就会挂起原先的事务,创建一个新的事务来执行,再回到原来的事务中。
隔离级别的影响随着是否是新事物而变化,也很好记住,如果沿用原来的事务,那么嵌套的那个事务他的隔离级别就沿用。如果创建了新的事务,那嵌套的这个事务就按新事物配置的隔离级别。
附件
如果想看可执行的源代码,请查看github仓库