SpringBoot中使用了aop方式,通过注解可以非常方便的实现数据库的事务,本文将简单介绍如何在SpringBoot中开启事务,以及使用SpringBoot事务时的一些特殊的用法和注意事项。
基本配置
本文中采用是数据库为Mysql 8.0,数据库连接池为druid,持久层采用mybatis plus
pom.xml
<!-- mysql支持 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- jdbc支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- druid数据库连接池,spring-boot版本 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
1. 开启事务
在SpringBoot中开启事务十分简单,只需要在方法或类上添加Transactional注解就可以了,需要注意这个方法所在的类需要被Spring的ioc容器初始化。
PersonServiceImp.java
@Override
@Transactional(rollbackFor = Exception.class)
public Person get(Long id) {
Person person = personMapper.selectById(id);
return person;
}
- 上面的代码中,在get方法上添加了Transactional注解,当其他模块调用这个方法时,Spring将会自动开启事务,并将该方法织入事务切面中,自动调用commit或者rollback。
- 注解中的rollbackFor属性指定了当程序遇到什么类型的错误时执行回滚,如果不指定默认为runtimeException
2. 隔离级别
上面介绍的只是最基本的事务使用方法,对于SpringBoot中的事务还有一些特殊的配置,这里介绍一种称为隔离级别的配置。
隔离级别实际上是数据库事务的一种设置,数据库事务拥有四个基本属性,分别为原子性,一致性,隔离性和持久性。其中隔离性反映的就是数据库事务的隔离级别。(对于其他几个属性,这里就不详细展开了)
隔离性主要是针对多个事务线程同时访问同一数据的情况,有可能会造成的数据更新丢失(更新没有生效)的情况,为了针对不同的使用场景,提出了4种隔离级别。
隔离级别 | 描述 |
---|---|
未提交读 | 最低的隔离级别,可以读取其他事务还未提交数据,会导致脏读 |
读写提交 | 第二级隔离级别,可以读取其他事务已经提交的数据,但是其他事务可能导致的数据变化无法获知 |
可重复读 | 第三级隔离级别,当某一线程事务读取某数据时,其他事务如果要读取这条数据需要等待,但是对于数据条数由于不属于数据本身,依然可能产生幻读(例如插入新数据) |
串行化 | 最高隔离级别,所有sql按照顺序串行执行,效率最低 |
在SpringBoot中启用隔离级别只需要在Transaction的注解中指定isolation属性即可。
PersonServiceImp.java
@Override
//这里指定了事务的隔离级别为读写提交
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
public int add(Person person) {
return personMapper.insert(person);
}
也可以全局设置默认隔离级别
application.properties
#数据源默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=3
#数据库连接池默认隔离级别
spring.datasource.druid.default-transaction-isolation=3
2.传播行为
当SpringBoot中一个方法调用另一个方法时,事务是如何执行的?传播行为就是用来配置方法调用过程中事务的策略。
一般我们认为一个方法调用了事务,则在方法中的子方法应该也是处于同一个事务之中,当子方法中有错误产生,则需要回滚整个事务。但是考虑一种情况,如果我们在子方法中执行了100条sql,但是只有一条失败了,我们需要让其他99条提交,而只回滚最后一条,这就需要我们对事务的传播行为进行设置。
Spring的事务机制中提供了7中传播行为,使用枚举类Propagation定义:
枚举值 | 描述 |
---|---|
REQUIRED | 需要事务,默认传播行为,如果当前存在事务则沿用当前事务,否则新建一个事务 |
SUPPORTS | 支持事务,如果存在事务则沿用,否则不采用事务 |
MANDATORY | 必须使用事务,没有事务抛出异常,否则沿用当前事务 |
REQUIRES_NEW | 无论是否存在当前事务,都会创建新事务 |
NOT_SUPPORTED | 不支持事务,当前存在事务则挂起事务 |
NEVER | 不支持事务,当前存在事务抛出异常,否则无事务运行 |
NESTED | 当前方法调用子方法时,如果子方法异常,只回滚子方法sql |
这里比较常用的就是上面表格中加粗的三类传播行为,之前提出的情况通过上面表格中的NESTED传播行为就可以解决。
使用传播行为也比较方便,只需要在子方法的Transaction注解中设置propagation的属性就可以了。
PersonServiceImp.java
@Service
public class PersonServiceImp implements PersonService, ApplicationContextAware {
@Autowired
PersonMapper personMapper;
//子方法
@Override
// @Transactional(
// rollbackFor = Exception.class,
// isolation = Isolation.READ_COMMITTED,
// propagation = Propagation.REQUIRES_NEW
// )//使用新事务
@Transactional(
rollbackFor = Exception.class,
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.NESTED
)//只回滚当前事务
public int add(Person person) {
return personMapper.insert(person);
}
PersonBatchServiceImp.java
@Service
public class PersonBatchServiceImp implements PersonBatchService {
@Autowired
PersonService personService;
//主方法
@Override
@Transactional(
rollbackFor = Exception.class,
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED
)//沿用当前事务
public int batchAdd(List<Person> personList) {
int count = 0;
for(Person person : personList) {
System.out.println("开始调用子方法...................................");
count += personService.add(person);
System.out.println("结束调用子方法...................................");
}
return count;
}
}
上面的例子中,PersonBatchServiceImp的batchAdd方法调用了PersonServiceImp中的add方法,其中add方法指定了事务的传播行为为NESTED,因此,当某一次add失败,只会回滚本次add的sql,不会回滚整个事务。
3.事务失效
在上面的例子中,我们看到当一个方法调用另一个子方法时,我们是通过不同的Service之间进行调用,即使用PersonBatchService.batchAdd调用了PersonService.add。那我们是否可以使用同一个Service调用子方法实现子方法上的事务配置?
我们可以考虑下面的代码是否可行:
PersonServiceImp.java
@Transactional(
rollbackFor = Exception.class,
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.NESTED
)//只回滚当前事务
public int add(Person person) {
return personMapper.insert(person);
}
@Transactional(rollbackFor = Exception.class)
public int addBatchSelf(List<Person> personList) {
int count = 0;
for(Person person : personList) {
//这里调用自身的接口子方法
count += add(person);
}
return count;
}
实际上,这里并不会调用子接口的事务,原因在于SpringBoot的事务是基于AOP的,因此需要使用代理对象来执行子方法,才能将方法织入到对应的事务切面,然而这里调用自身的子方法不会使用代理对象,而是自生调用,因此不会触发AOP的过程,因此子方法的事务就无法生效了。
那子调用的方法有没有办法让他的事务生效呢?答案是有的
->我们只需要手动获取当前Service的代理对象就可以了
观察下面的代码:
PersonServiceImp.java
@Service
//这里实现了ApplicationContextAware接口,可以获取ioc容器实例
public class PersonServiceImp implements PersonService, ApplicationContextAware {
@Autowired
PersonMapper personMapper;
//ico容器
ApplicationContext context;
@Override
// @Transactional(
// rollbackFor = Exception.class,
// isolation = Isolation.READ_COMMITTED,
// propagation = Propagation.REQUIRES_NEW
// )//使用新事务
@Transactional(
rollbackFor = Exception.class,
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.NESTED
)//只回滚当前事务
public int add(Person person) {
return personMapper.insert(person);
}
@Override
public int addBatchSelf(List<Person> personList) {
int count = 0;
//通过IOC容器获取personService的代理对象
PersonService personService = context.getBean(PersonService.class);
for(Person person : personList) {
//这里调用自身的接口子方法,不会触发代理,因此无法走子方法的事务切面
//count += add(person);
//手动调用personService的代理,使其触发子方法的事务切面
count += personService.add(person);
}
return count;
}
//获取ioc容器
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}
上面的代码里,我们通过手动获取ioc容器,并且从ioc容器中获取了personService的对象,这个对象是一个代理对象,因此,通过这个对象调用add子方法就可以触发子方法的事务。至于为什么ioc容器中获取的对象是一个代理对象,这是因为ioc容器在初始化bean时,会根据类中的方法是否有需要动态代理的方法来判断时直接通过反射实例化还是通过代理创建,如果一个类中的方法被Transactional注解,则这个类会被判断为需要动态代理的类,被实例化为一个代理对象。
总结
Spring-Boot中的事务使用非常方便,同时还可以通过设置隔离级别和传播行为实现各种业务上的需求,但同时也需要注意Spring-Boot的事务是基于Spring的AOP实现的,因此在某些情况下会导致事务失效,需要我们对Spring的AOP原理,以及Sping的Bean在启动时如何初始化有一定的了解。