主要内容
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();
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)
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() );
} 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();
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.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() );
} 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();
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)
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() );
} 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 开启的内层事务回滚,外层事务也回滚。