SpringBoot(七) @Transactional 事务管理

声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多

此前,我们主要通过XML配置Spring来托管事务。在SpringBoot则非常简单,只需在业务层添加事务注解@Transactional 即可快速开启事务。虽然事务很简单,但对于数据方面是需要谨慎对待的,识别常见坑点对我们开发有帮助。

简介

事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。

声明式事务

Spring 支持声明式事务,使用 @Transactional 注解在方法上表明这个方法需要事务支持。此时,Spring 拦截器会在这个方法调用时,开启一个新的事务,当方法运行结束且无异常的情况下,提交这个事务。

Spring 提供一个 @EnableTransactionManagement 注解在配置类上来开启声明式事务的支持。使用了 @EnableTransactionManagement 后,Spring 会自动扫描注解 @Transactional 的方法和类。

Spring Boot默认集成事务

Spring Boot 默认集成事务,所以无须手动开启使用 @EnableTransactionManagement 注解,就可以用 @Transactional注解进行事务管理。我们如果使用到 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa,Spring Boot 会自动默认分别注入
DataSourceTransactionManagerJpaTransactionManager

项目构建

实体类

创建一个实体对象。为了便于测试,我们对外提供一个构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class City {

private Long id;
private String name;
private String state;

public City(String name, String state) {
this.name = name;
this.state = state;
}
}

@Data : Lombok注解

数据层

提供一个新增方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author Ray
* @date 2018/7/25 0025
* 提供一个新增方法
*/
@Repository
public class CityDao {

@Autowired
private JdbcTemplate jdbcTemplate;

public int add(City city){
return jdbcTemplate.update(
"insert into city(name,state) values (?,?)",
city.getName(), city.getState());
}
}

业务层

我们提供三个方法。

1
2
3
4
5
6
7
8
9
10
11
/**
* @author Ray
* @date 2018/7/25 0025
* 业务层
*/
public interface CityService {

public int add1(City city);
public int add2(City city);
public int add3(City city);
}

业务实现层

通过定义 City 的 name 字段长度必须小于等于 5,如果字段长度超过规定长度就会触发参数异常。

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
49
50
51
52
/**
* @author Ray
* @date 2018/7/25 0025
* 业务层实现类
*/
@Service
public class CityServiceImpl implements CityService {

private static final int LENGTH = 5;

@Autowired
private CityDao cityDao;


/**
* 没有使用声明式事务管理,发生异常不会回滚
*/
@Override
public int add1(City city) {
int result = this.cityDao.add(city);
if(city.getName().length() > LENGTH){
throw new IllegalArgumentException("name is too long");
}
return result;
}

/**
* 使用声明式事务管理,noRollbackFor 表示指定异常类型不回滚,发生异常不会回滚
*/
@Override
@Transactional(noRollbackFor = {IllegalArgumentException.class})
public int add2(City city) {
int result = this.cityDao.add(city);
if(city.getName().length() > LENGTH){
throw new IllegalArgumentException("name is too long");
}
return result;
}

/**
* 使用声明式事务管理,rollbackFor 表示指定异常类型回滚,发生异常会回滚
*/
@Override
@Transactional(rollbackFor = {IllegalArgumentException.class})
public int add3(City city) {
int result = this.cityDao.add(city);
if(city.getName().length() > LENGTH){
throw new IllegalArgumentException("name is too long");
}
return result;
}
}

值得注意的是,noRollbackFor 修饰表明不做事务回滚,rollbackFor 修饰的表明需要事务回滚。

测试类

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
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot207ApplicationTests {

@Autowired
private CityService cityService;

/**
* 发生异常, 不回滚
* 新增数据 "珠海","广东"
* 新增数据 "ZhuHai","GuangDong"
*/
@Test
public void add1(){
cityService.add1(new City("珠海","广东"));
cityService.add1(new City("ZhuHai","GuangDong"));
}

/**
* 发生异常, 不回滚
* 新增数据 "珠海","广东"
* 新增数据 "ZhuHai","GuangDong"
*/
@Test
public void add2(){
cityService.add2(new City("珠海","广东"));
cityService.add2(new City("ZhuHai","GuangDong"));
}

/**
* 发生异常, 回滚
* 新增数据 "珠海","广东"
*/
@Test
public void add3(){
cityService.add3(new City("珠海","广东"));
cityService.add3(new City("ZhuHai","GuangDong"));
}

}

分别对上面的三个方法进行测试,只有最后一个方法进行了事务回滚。

常见错误

使用事务注解@Transactional 之前,应该先了解它的相关属性,避免在实际项目中踩中各种各样的坑点。

错误一

遇到非检测异常时,事务不开启,也无法回滚
例如下面这段代码,账户余额依旧增加成功,并没有因为后面遇到检测异常而回滚

1
2
3
4
5
6
7
@Transactional
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//然后遇到故障
throw new SQLException("发生异常了..");
}

原因分析:因为Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。如果想针对非检测异常进行事务回滚,可以在@Transactional 注解里使用

rollbackFor 属性明确指定异常
例如下面这样,就可以正常回滚:

1
2
3
4
5
6
7
@Transactional(rollbackFor = Exception.class)
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//然后遇到故障
throw new SQLException("发生异常了..");
}

错误二

在业务层捕捉异常后,发现事务不生效
这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常”吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。例如:下面这段代码直接导致增加余额的事务回滚没有生效。

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//谨慎:尽量不要在业务层捕捉异常并处理
try {
throw new SQLException("发生异常了..");
} catch (Exception e) {
e.printStackTrace(); // 把异常"吃"掉了
}
}

不要小瞧了这些细节,往前暴露异常很大程度上很能够帮我们快速定位问题,而不是经常在项目上线后出现问题,却无法刨根知道哪里报错。

推荐做法

在业务层统一抛出异常,然后在控制层统一处理。

1
2
3
4
5
6
7
@Transactional
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//推荐:在业务层将异常抛出
throw new RuntimeException("发生异常了..");
}

扩展

@Transactional 注解属性

属性名 说明
name 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。
propagation 事务的传播行为,默认值为 REQUIRED。
isolation 事务的隔离度,默认值采用 DEFAULT。
timeout 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
read-only 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollback-for 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。
no-rollback- for 抛出 no-rollback-for 指定的异常类型,不回滚事务。

除此以外,@Transactional 注解也可以添加到类级别上。当把@Transactional 注解放在类级别时,表示所有该类的公共方法都配置相同的事务属性信息。

具体参考

透彻的掌握 Spring 中@transactional 的使用

-------------- 本文结束  感谢您的阅读 --------------