【spring】事务回滚

1. 主要的三种传播机制

https://www.cnblogs.com/xzwbl…

REQUIRED、REQUIRES_NEW、NESTED

注意:以下所有的testA()方法与testB()方法是在两个类之中且都将bean交给spring管理,若非如此会造成事务失效问题请看第二部分

1.1 REQUIRED(默认)

当前没有事务,则新建一个事务,如存在事务,则加入此事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 例子
// A.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void testA() {
insertDataToTableA(dataList);
testB();
}

// B.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void testB(){
insertDataToTableB(dataList);
int i = 1/0;
insertDataToTableC(dataList);
}

// 结果 testB()加入testA()的事务,testB()发生异常,两个方法均回滚
// 如果 testA()不加事务,只有testB()加,则testA()成功,testB()回滚

1.2 REQUIRES_NEW

创建一个新事务,如果存在当前事务,则挂起该事务。即无论是否已存在事务都新建事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 例子
// A.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void testA() {
insertDataToTableA(dataList);
testB();
int i = 1/0;
}

// B.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void testB(){
insertDataToTableB(dataList);
insertDataToTableC(dataList);
}


// 结果 testA() 回滚 testB() 成功,说明不是一个事务里

1.3 NESTED

如果当前事务存在,则在嵌套事务中执行(子事务),否则REQUIRED的操作一样(开启一个事务)

这里需要注意两点:

  • 和REQUIRES_NEW的区别

REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。
在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。

  • 和REQUIRED的区别

REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 例子1

// A.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void testA() {
insertDataToTableA(dataList);
testB();
int i = 1/0;
}

// B.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void testB(){
insertDataToTableB(dataList);
insertDataToTableC(dataList);
}

// 结果 二者皆回滚,因为是嵌套的父子级事务,所以有一个有问题就都回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 例子2

// A.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void testA() {
insertDataToTableA(dataList);
try{
testB();
}catch(Exception e){
log.error(e)
}

}

// B.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void testB(){
insertDataToTableB(dataList);
int i = 1/0;
insertDataToTableC(dataList);
}


// 结果 testA成功、testB回滚,说明相对 REQUIRED 异常可以被捕获处理

2. 事务失效

2.1 内部调用问题

1. A带事务调B不带事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 例0 无异常 -> 两个都能顺利插入表

// 例1 A或B方法异常
// 结果 事务生效(A,B公用一个事务),两个方法皆回滚
@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.REQUIRED)
public Integer insertUserInfo() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(5, 'Jack5')");

// 内部调用问题
insertUserInfo2();
return null;
}

//@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.NESTED)
public Integer insertUserInfo2() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(9, 'tom9')");
int i = 1 / 0;
return null;
}

// 例2 A方法调用完B后再异常 -> 结果相同事务生效

2. A带事务调B带事务(REQUIRES_NEW)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 例1 B异常 且 B的传播机制为 REQUIRES_NEW(新建事务)
// 结果 B事务失败(追加到A事务里),A,B皆回滚,说明A,B还是在一个事务里,A方法里异常时同样情况
@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.REQUIRED)
public Integer insertUserInfo() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(5, 'Jack5')");

// 内部调用问题
insertUserInfo2();
return null;
}

@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.REQUIRES_NEW)
public Integer insertUserInfo2() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(9, 'tom9')");
int i = 1 / 0;
return null;
}

3. A带事务调B带事务(NESTED)

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
// 例1 B异常 且 B的传播机制为 NESTED,在A中捕获异常
// 结果 A事务成功,B事务失败,异常后仍能插入数据
@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.REQUIRED)
public Integer insertUserInfo() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(5, 'Jack5')");

// 内部调用问题
try {
insertUserInfo2();
}catch (Exception e){
e.printStackTrace();
}
return null;
}

@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.NESTED)
public Integer insertUserInfo2() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(9, 'tom9')");
int i = 1 / 0;
return null;
}

// 例2 B异常 且 B的传播机制为 NESTED,在A中不捕获异常
// 结果 A,B皆回滚

// 例3 A异常 且 B的传播机制为 NESTED,在A中捕获异常与否都一样
// 结果 A,B皆回滚

4. A不带事务调B带事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 例0 无异常 -> 两个都能顺利插入表

// 例1 B方法异常 -> 事务失效,A,B都不会回滚,都会插入数据
// 第一部分中的三种传播机制都会是这种情况
//@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.REQUIRED)
public Integer insertUserInfo() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(5, 'Jack5')");

// 内部调用问题
insertUserInfo2();
return null;
}

@Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.REQUIRED)
public Integer insertUserInfo2() {
jdbcTemplate.execute("INSERT INTO user (id, username) VALUES\n" +
"(9, 'tom9')");
int i = 1 / 0;
return null;
}

// 例2 A方法调用完B后异常 -> 与例1相同,事务失效,A,B都不会回滚,都会插入数据

5. 综述

尽量避免内部调用,最多用A带事务调B不带事务,尤其是不能用A不带事务调B带

2.2 内部调用问题事务失败原因

  1. A带事务调B带事务:放在一个代理类里,追加到A的事务,只会代理A,然后this调用B
  2. A不带事务调B带事务:内部调用没走Spring的AOP增强代理机制,也就没做事务增强操作,本质是this实例调用

spring事务
@Transactional

事务失效十种情况

访问权限问题

只允许public修饰方法

方法用 final 修饰

final修饰导致事务失效

方法内部调用

在同一个类中的方法直接内部调用,会导致事务失效。

未被 spring 管理

没有加类似@Service注解,将类交给spring管理

多线程调用

@Transactional  
public void add(UserModel userModel) throws Exception {  
    userMapper.insertUser(userModel);  
    new Thread(() -> {  
        roleService.doOtherThing();  
    }).start();  
}

insertUser与doOtherThing两个方法,doOtherThing方法是在另外新建线程去执行的,就会导致两个方法不在同一个线程中执行,获取到的是不同的数据库连接,也就会导致事务失效

表不支持事务

数据库引擎是myisam,不支持事务

未开启事务

springboot会默认开启,传统spring需要配置事务管理器等

错误的传播特性

propagation参数,指定合适的传播特性

自己吞了异常

如果想要 spring 事务能够正常回滚,必须抛出它能够处理的异常(不能try,catch)。如果没有抛异常,则 spring 认为程序是正常的。

手动抛了别的异常

spring 事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的 Exception(非运行时异常),它不会回滚

自定义了回滚异常

rollbackFor参数,指定遇到该参数指定的异常才会回滚。如果roolbackFor设置不合适,会导致发生异常不回滚

嵌套事务回滚多了

@Transactional
public void add(){
    A();
    B();
}

@Transactional
public void A(){
    XXX
}

@Transactional
public void B(){
    XXX
}

如上,B方法内发生异常时,我们希望只回滚B方法内的事务操作,但实际连A的也一起回滚了。这是因为B方法的异常抛出后没有处理,又被add方法捕获导致继续回滚。
可以在add方法中try-catch住B方法,不继续往上抛异常即可。


其他问题

大事务问题

@Transactional加到方法上会导致整个方法都包含在事务中了,如果方法调用层级深,会导致事务非常耗时。

编程式事务

Transactional-声明式事务

@Autowired  
private TransactionTemplate transactionTemplate;  

...  

public void save(final User user) {  
    queryData1();  
    queryData2();  
    transactionTemplate.execute((status) => {  
        addData1();  
        updateData2();  
        return Boolean.TRUE;  
    })  
}

自己类中注入自己
spring ioc 内部的三级缓存保证了它不会出现循环依赖问题。