关于spring事务你需要知道的知识点

一、介绍

大家都知道,在SpringBoot中,使用事务只需要添加@Transactional就可以添加事务,很是方便。

那么它到底是怎么工作的呢?

这么说有点晕头晕脑的,那来简单看下

二、事务失效的场景

1)事务需要代理类启动

基本的配置我就贴出来了,就一个连接数据库的配置有啥好看的,数据库表也是一样

那么接下来,先来一个UserDao

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
package com.banmoon.test.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class UserDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional
public void insert(){
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月无霜')");
throw new NullPointerException();
}

@Transactional
public void insert2(){
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月')");
}

}

再来一个UserService并在14行打上断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.banmoon.test.service;

import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

@Autowired
private UserDao userDao;

public void insert(){
userDao.insert();
}

}

写段测试,调用一下这个插入方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.banmoon.test;

import com.banmoon.test.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ServiceTest {

@Autowired
private UserService userService;

@Test
void insertTest() {
userService.insert();
}

}

debug启动后,我们发现这个userDaoCGlib创建了一个代理类

image-20220306173738993

放行,可以发现报错,继续看看事务会不会回滚

image-20220306174137289

image-20220306174214964

代码中抛出的空指针异常就是我们写的那段,说明插入语句已经执行了。但看到数据库中,没有自己执行插入的数据,那证明了事务确实生效了。

那如果没有这个代理类,真的事务就不会生效吗?把测试代码改一下,不再依赖spring注入,我们自己来创建实例对象。

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
package com.banmoon.test.dao;

import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Setter// 加上set方法,jdbc从外部给予
@Repository
public class UserDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional
public void insert(){
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月无霜')");
throw new NullPointerException();
}

@Transactional
public void insert2(){
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月')");
}

}

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
package com.banmoon.test;

import com.banmoon.test.dao.UserDao;
import com.banmoon.test.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootTest
class ServiceTest {

@Autowired
private UserService userService;

@Autowired
private JdbcTemplate jdbcTemplate;

@Test
void insertTest() {
UserDao userDao = new UserDao();
userDao.setJdbcTemplate(jdbcTemplate);
userDao.insert();
}

}

查看结果,依然报错,这是肯定的。但数据库,表里却已经有了数据。事务失效了,这一次的userDao是自己创建的实例,而不是动态代理类。

动态代理类,在使用@Transactional的方法时,前置通知开启事务,后置通知决定是提交还是回滚。

image-20220306175629369

image-20220306175742286

2)修饰在非public方法上时

如果@Transactional修饰在非public修饰的方法上,事务将会失效。

这是因为CGlib在创建代理类时,它会查找目标方法是否是public

主要类在此:org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource

1
2
3
4
5
6
7
8
9
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// 判断方法是不是public
if (this.allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
} else {
// 其他逻辑
}
}

这是个抽象类,allowPublicMethodsOnly方法由它的子类来实现,本身默认的实现是返回false

在使用@Transactional时,使用到public的方法上。所以这也是我不建议将注解使用在类上的原因,你以为类中的方法都有事务了,但实际不然。

3)在同一个类中调用方法

还是简单的代码,讲的是同一个类中调用方法,这个方法有@Transactional

这里修改一下UserDao这个类,使得insert方法去调用insert2方法,查看事务是否生效

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
package com.banmoon.test.dao;

import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Setter
@Repository
public class UserDao {

@Autowired
private JdbcTemplate jdbcTemplate;

public void insert(){
insert2();
}

@Transactional
public void insert2(){
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月')");
throw new NullPointerException();
}

}

测试代码启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.banmoon.test;

import com.banmoon.test.dao.UserDao;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ServiceTest {

@Autowired
private UserDao userDao;

@Test
void insertTest() {
userDao.insert();
}

}

查看结果,报错是肯定的,那么数据呢?

image-20220306192905742

插入成功,怎么回事?

image-20220306192950529

原因估计还是出在代理类上,和第一条不同的是,代理类虽然有代理类,但insert方法直接调用了insert2方法,这个是目标类中自己调用的,所以事务没有生效。

image-20220306194333888

4)注解属性 propagation 设置错误

如使用了SUPPORTSNOT_SUPPORTED传播机制,这些传播机制讲的是不再创建事务。

详见第三章

5)注解属性rollbackFor设置错误

rollbackFor:这个注解属性的作用是可以指定能够触发事务回滚的异常类型。

而在默认情况下,spring只会对非检查的异常做回滚。

  • 检查异常:派生于Error或者RuntimeException的异常成为非检查异常

    • 例如:NullPointerExceptionArithmeticException
  • 非检查异常:剩下的就是非检查异常了;在写出后,编译器要我们进行及时处理的异常,要么自己捕获,要么抛出

    • 例如:IOExceptionTimeOutExpetion

所以在抛出其他类型的异常时,我们得指定rollbackFor属性。一旦有它和它的子类异常被抛出,事务也能够生效

6)异常被自己捕获

事务判断是否提交,还是回滚,就是根据是否捕获到非检查异常。

所以,如果在程序中自己try...catch...捕获掉,那么事务将会当做成功执行,将事务提交。

自己写了try...catch...的话,要么自己手动提交回滚,要么就是再抛出一个非检查异常。

三、事务的传播机制

事务的传播机制,简单的来说,就是一个方法,调用另一个方法。原本就有的事务,在遇到一个新的事务后会发生什么机制。这就是我们要讲得事务传播机制。

通过@Transactionalpropagation属性我们可以进行配置事务的传播机制,我们先看看这个枚举里面都有些什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.springframework.transaction.annotation;

public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);

private final int value;

private Propagation(int value) {
this.value = value;
}

public int value() {
return this.value;
}
}

简简单单,没有说明,一个一个来看吧。先写出两个Dao,一会测试用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.banmoon.test.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class UserDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional
public void insert(){
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月')");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.banmoon.test.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class PlatformDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional
public void insert(){
jdbcTemplate.execute("INSERT INTO `platform`(`name`) VALUES ('博客平台')");
}

}

1)REQUIRED(默认)

这是spring默认的事务传播机制。

  • 如果外部方法已经有事务了,那么本方法将加入这个事务

  • 如果外部方法没有事务,那么本方法就自己建事务

我们写一个Service去调用上面两个Dao

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
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

// 如果外部方法已经有事务了,那么本方法将加入这个事务
public void insert(){
// 此处将创建事务
userDao.insert();
// 此处将创建事务,如果发生异常,只会回滚自己的事务
platformDao.insert();
}

}
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
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

/**
* 外部方法有事务,里面的两个方法的事务,将直接加入到外部方法的事务中
* 如果发生异常,导致外部方法中整个事务的回滚
*/
@Transactional
public void insert(){
// 不创建事务了,加入到外部事务
userDao.insert();
// 不创建事务了,加入到外部事务
platformDao.insert();
}

}

2)SUPPORTS

  • 如果外部方法已经有事务了,那么本方法将加入这个事务

  • 如果外部方法没有事务,那么本方法就以非事务的方式执行,不再创建事务

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
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

/**
* 外部方法有事务,里面的两个方法的事务,将直接加入到外部方法的事务中
* 如果发生异常,导致外部方法中整个事务的回滚
*/
@Transactional
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.SUPPORTS)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.SUPPORTS)
platformDao.insert();
}

/**
* 外部方法没有事务,里面的两个方法不再会创建事务,直接以非事务的方式运行
*/
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.SUPPORTS)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.SUPPORTS)
platformDao.insert();
}

}

3)MANDATORY

  • 如果外部方法已经有事务了,那么本方法将加入这个事务

  • 如果外部方法没有事务,将抛出异常

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
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

/**
* 外部方法有事务,里面的两个方法的事务,将直接加入到外部方法的事务中
* 如果发生异常,导致外部方法中整个事务的回滚
*/
@Transactional
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.MANDATORY)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.MANDATORY)
platformDao.insert();
}

/**
* 外部方法没有事务,里面直接会抛出异常
*/
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.MANDATORY)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.MANDATORY)
platformDao.insert();
}

}

来看看异常吧

image-20220307210837620

4)REQUIRES_NEW

  • 如果外部方法已经有事务了,重新创建一个新的事务

  • 如果外部方法没有事务,重新创建一个新的事务

这也就是说,无论外部有无事务,内部方法都将会创建新的事务

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
42
43
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

/**
* 外部方法已经有事务了,内部方法还是会重新创建一个新的事务
* 如果发生异常,只会导致自己所在的方法事务回滚
* 这里创建了三个事务
*/
@Transactional
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.REQUIRES_NEW)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.REQUIRES_NEW)
platformDao.insert();
}

/**
* 外部方法没有事务,内部方法会创建一个新的事务
* 如果发生异常,只会导致自己所在的方法事务回滚
* 这里创建了两个事务
*/
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.REQUIRES_NEW)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.REQUIRES_NEW)
platformDao.insert();
}

}

5)NOT_SUPPORTED

  • 如果外部方法已经有事务了,以非事务的方式运行

  • 如果外部方法没有事务,以非事务的方式运行

也就是说,无论外部方法有无事务,里面的方法执行都是没有事务的。

但我有点疑惑,如果里面方法抛出异常了,外部方法会怎么样。简单测试一下吧

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
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

/**
* 无论外部方法有无事务,里面的方法执行都是没有事务的
*/
@Transactional
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.NOT_SUPPORTED)
userDao.insert();
// 外部方法的执行
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月1')");
// 这里的修饰是@Transactional(propagation = Propagation.NOT_SUPPORTED)
platformDao.insert();
}

}

我执行了,结果是只有26行的数据插入被回退。

24行和28行是非事务运行的,就算异常了也不会回退

6)NEVER

  • 如果外部方法已经有事务了,抛出异常

  • 如果外部方法没有事务,以非事务的方式运行

SUPPORTS相反的一个传播机制

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
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

/**
* 外部方法已经有事务了,将抛出异常
*/
@Transactional
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.NEVER)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.NEVER)
platformDao.insert();
}

/**
* 外部方法没有事务,内部方法也不再创建,以非事务的方式运行
*/
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.NEVER)
userDao.insert();
// 这里的修饰是@Transactional(propagation = Propagation.NEVER)
platformDao.insert();
}

}

7)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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.banmoon.test.service;

import com.banmoon.test.dao.PlatformDao;
import com.banmoon.test.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserDao userDao;

@Autowired
private PlatformDao platformDao;

@Autowired
private JdbcTemplate jdbcTemplate;


@Transactional
public void insert(){
// 这里的修饰是@Transactional(propagation = Propagation.NESTED)
userDao.insert();
// 外部方法的执行
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月1')");
try {
// 这里的修饰是@Transactional(propagation = Propagation.NESTED)
platformDao.insert();// 此处将抛出空指针
} catch (Exception e) {
e.printStackTrace();
}
}

@Transactional
public void insert1(){
// 这里的修饰是@Transactional(propagation = Propagation.NESTED)
userDao.insert();
// 外部方法的执行
jdbcTemplate.execute("INSERT INTO `user`(`name`) VALUES ('半月1')");
// 这里的修饰是@Transactional(propagation = Propagation.NESTED)
platformDao.insert();
throw new NullPointException();// 此处将抛出空指针
}

}

执行insert(),这是子事务中抛出的异常。结果是只有30行的插入数据回滚。注意的就是,内部方法的异常要自己捕获,别被父事务发现了。如果发现了,大家就一起回滚吧。

执行insert1(),这是父事务中抛出的异常。结果发现所有插入的数据都回滚了。

四、结语

我是半月,祝你幸福!!!