Sam's Notes | Sam Blog

梦想还是要有的,万一实现了呢

0%

Spring transaction 事务传播机制

主要内容
Spring transaction 事务传播机制大家都听说过,事务传播机制就是当一个事务方法被另一个事务方法调用时,事务是如何传递的。实际使用中,很容易出错,大家可提前学习下“Spring 事务失效7大场景”,本文重点讨论同一类方法相互调用的情景。
本文以 spring boot 3.2.3、Mysql 8 版本为实验环境说明。

更新历史


预备知识

MySQL四个事务隔离级别

  • 读未提交(Read Uncommitted)
    特点:事务中的修改操作(INSERT、UPDATE、DELETE)立即生效,无需等待事务提交;事务读取数据时可以读取其他事务未提交的数据。
    应用场景:对于一些对数据一致性要求不高的场景,读取未提交的数据,也被称之为脏读(Dirty Read)
  • 读已提交(Read Committed)
    特点:事务中的修改操作需要等待事务提交后才生效;事务读取数据时只能读取其他事务已提交的数据。
    应用场景:适用于大部分常规业务场景,能够保证读取的数据具有较高的一致性。但不可重复读,因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。他
  • 可重复读(Repeatable Read)
    特点:事务中的修改操作需要等待事务提交后才生效;事务读取数据时只能读取事务开始时的快照数据,其他事务对数据的修改不可见。
    应用场景:适用于需要保证读取数据一致性的应用。 问题是幻读(快照数据,其他事务对数据的修改不可见,所以可能是虚幻的数据)。
  • 串行化(Serializable)
    特点:事务中的修改操作需要等待事务提交后才生效;事务读取数据时只能读取事务开始时的快照数据,并且其他事务对数据进行了读取和修改的过程中,该数据将被锁定,其他事务无法访问。
    应用场景:适用于对数据一致性要求极高的场景。

Spring 事务传播机制

名称 是否支持当前事务 说明
Propagation_Required 表示被修饰的方法必须运行在事务中,如果当前方法没有事务,则新建一个事务;如果已经存在一个事务中,就加入到这个事务中,此类型是最常见的默认选择。
Propagation_Supports 表示被修饰的方法不需要事务上下文,如果当前方法存在事务,则支持当前事务执行;如果当前没有事务,就以非事务方式执行。
Propagation_Mandatory 表示被修饰的方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。
Propagation_Requires_New 表示被修饰的方法必须运行在它自己的事务中,一个新的事务会被启动,如果调用者存在当前事务,则在该方法执行期间,当前事务会被挂起。
Propagation_Not_Supported 表示被修饰的方法不应该运行在事务中,如果调用者存在当前事务,则该方法运行期间,当前事务将被挂起。
Propagation_Never 表示被修饰的方法不应该运行在事务上下文中,如果调用者或者该方法中存在一个事务正在运行,则会抛出异常。
Propagation_Nested 嵌套 表示当前方法已经存在一个事务,那么该方法将会在嵌套事务中运行,嵌套的事务可以独立与当前事务进行单独地提交或者回滚,如果当前事务不存在,那么其行为与Propagation_Required一样。

实验场景

实验时分为this对象调用和Spring Context对象调用2种,Spring transaction 事务主要是通过AOP实现的,因此这2种调用方式存在差异。
mysql 事务隔离级别为可重复读(Repeatable Read), 数据库中有一User 表,现存36条数据(至于为什么是36,大概是天罡数),主要字段为id(主键)、type; 初始状态id=1的数据type=0。

this对象调用

Propagation.REQUIRES_NEW

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 测试主入口
*/
@Test
public void transcationTest() {

try {
userService.testTransaction();
} catch (Exception e){
e.printStackTrace();
}

userService.testTransaction4();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service
@Transactional
public class UrUserService {
/**
* 功能主入口
*/
public void testTransaction() {
testTransaction1();
// SpringContext.getBean(UrUserService.class).testTransaction2();
testTransaction2();
testTransaction3();
}
public void testTransaction1() {
UrUserEntity user = userRepository.getById(1l);
user.setType(1);
userRepository.save(user);
System.out.println("testTransaction111111111111 id=" + user.getId() + " user type=" + user.getType() + " count=" + userRepository.count() );
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
// @Transactional(propagation = Propagation.NESTED)
// @Transactional(propagation = Propagation.MANDATORY)
public void testTransaction2() {
UrUserEntity user = new UrUserEntity(null, "us", 1, "realname", null, "199123", null, "hah", null);
user.setType(2);
save(user);

UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction22222222222222 REQUIRES_NEW id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
// throw new RuntimeException("test2");
}
public void testTransaction3() {
UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction33333333333 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
throw new RuntimeException("test3");
}
public void testTransaction4() {

UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction4444444444444 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
}
}
Junit Test 执行结果如下
1
2
3
4
5
6
testTransaction111111111111  id=1    user type=1  count=36
testTransaction22222222222222 REQUIRES_NEW id=1 user type=1 count=37
testTransaction33333333333 id=1 user type=1 count=37
java.lang.RuntimeException: test3
......
testTransaction4444444444444 id=1 user type=0 count=36

最终结果 user type=0 count=36,有兴趣可以试试:
propagation = Propagation.MANDATORY或 Propagation.NESTED ,RuntimeException(“test2”) 结果如上一致。

结论

this对象调用场景下

  • spring事务传递的是当前事务;
  • @Transactional(propagation = Propagation.REQUIRES_NEW) 不起作用,新的事务没有启动;
  • 外层事务回滚,testTransaction2()的 事务也回滚。

Spring Context对象调用

Propagation.MANDATORY

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 测试主入口
*/
@Test
public void transcationTest() {

try {
userService.testTransaction();
} catch (Exception e){
e.printStackTrace();
}

userService.testTransaction4();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service
@Transactional
public class UrUserService {
/**
* 功能主入口
*/
public void testTransaction() {
testTransaction1();
SpringContext.getBean(UrUserService.class).testTransaction2();
// testTransaction2();
testTransaction3();
}
public void testTransaction1() {
UrUserEntity user = userRepository.getById(1l);
user.setType(1);
userRepository.save(user);
System.out.println("testTransaction111111111111 id=" + user.getId() + " user type=" + user.getType() + " count=" + userRepository.count() );
}
// @Transactional(propagation = Propagation.REQUIRES_NEW)
// @Transactional(propagation = Propagation.NESTED)
@Transactional(propagation = Propagation.MANDATORY)
public void testTransaction2() {
UrUserEntity user = new UrUserEntity(null, "us", 1, "realname", null, "199123", null, "hah", null);
user.setType(2);
save(user);

UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction22222222222222 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
// throw new RuntimeException("test2");
}
public void testTransaction3() {
UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction33333333333 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
throw new RuntimeException("test3");
}
public void testTransaction4() {

UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction4444444444444 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
}
}
Junit Test 执行结果如下
1
2
3
4
5
6
testTransaction111111111111  id=1    user type=1  count=36
testTransaction22222222222222 id=1 user type=1 count=37
testTransaction33333333333 id=1 user type=1 count=37
java.lang.RuntimeException: test3
...
testTransaction4444444444444 id=1 user type=0 count=36
结论

Spring Context对象调用时, Propagation.MANDATORY

  • spring事务传递的是当前事务;
  • 外层事务回滚,testTransaction2()的 事务也回滚;
  • testTransaction2()的 事务回滚, 外层事务也回滚。 (代码throw RuntimeException(“test2”))

Propagation.REQUIRES_NEW

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 测试主入口
*/
@Test
public void transcationTest() {

try {
userService.testTransaction();
} catch (Exception e){
e.printStackTrace();
}

userService.testTransaction4();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service
@Transactional
public class UrUserService {
/**
* 功能主入口
*/
public void testTransaction() {
testTransaction1();
SpringContext.getBean(UrUserService.class).testTransaction2();
// testTransaction2();
testTransaction3();
}
public void testTransaction1() {
UrUserEntity user = userRepository.getById(1l);
user.setType(1);
userRepository.save(user);
System.out.println("testTransaction111111111111 id=" + user.getId() + " user type=" + user.getType() + " count=" + userRepository.count() );
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
// @Transactional(propagation = Propagation.NESTED)
// @Transactional(propagation = Propagation.MANDATORY)
public void testTransaction2() {
UrUserEntity user = new UrUserEntity(null, "us", 1, "realname", null, "199123", null, "hah", null);
user.setType(2);
save(user);

UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction22222222222222 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
// throw new RuntimeException("test2");
}
public void testTransaction3() {
UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction33333333333 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
throw new RuntimeException("test3");
}
public void testTransaction4() {

UrUserEntity user1 = userRepository.getReferenceById(1l);
System.out.println("testTransaction4444444444444 id=" + user1.getId() + " user type=" + user1.getType() + " count=" + userRepository.count() );
}
}
Junit Test 执行结果如下
1
2
3
4
5
6
testTransaction111111111111  id=1    user type=1  count=36
testTransaction22222222222222 id=1 user type=0 count=37
testTransaction33333333333 id=1 user type=1 count=36
java.lang.RuntimeException: test3
...
testTransaction4444444444444 id=1 user type=0 count=37
结论

Spring Context对象调用时,

  • Propagation.REQUIRES_NEW 正常开启新事务;
  • 外层事务回滚,Propagation.REQUIRES_NEW 开启的内层事务回滚。
  • testTransaction2()的 事务回滚, 外层事务也回滚。 (代码throw RuntimeException(“test2”))
    1
    2
    3
    4
    5
    testTransaction111111111111  id=1    user type=1  count=36
    testTransaction22222222222222 id=1 user type=0 count=37
    java.lang.RuntimeException: test2
    ...
    testTransaction4444444444444 id=1 user type=0 count=36
    PS. 我的环境JPA的Hibernate实现不支持 Propagation.NESTED 策略。

总结

  • this对象调用场景下
    Propagation.REQUIRES_NEW 仍传递当前事务,其实就是一个事务,回滚时所有事务都回滚
  • Spring Context对象调用场景下
    Propagation.REQUIRES_NEW 正常开启新事务;外层事务回滚,Propagation.REQUIRES_NEW 开启的内层事务回滚;
    ropagation.REQUIRES_NEW 开启的内层事务回滚,外层事务也回滚。