一、背景介绍
今天我们接着来聊聊 SpringBoot 事务相关的具体使用方式。
何谓事务?熟悉数据库的同学可能比较熟悉,对于任意一组 SQL 语句,要么全部执行成功,要么全部执行失败;如果中途因为某个原因其中一条语句执行失败,那么先前执行过的语句将全部撤回。
事务控制的好处在于:可以保证数据操作的安全性。比如,当你给某个客户 A 转账 100 块,此时银行需要从你的个人账户中扣除 100 块,然后再给客户 A 账户增加 100 块,这其实是两个动作,假如银行在操作客户 A 的账户时出现故障,此时你个人账户的钱已经被扣除,但对方的账户并没有到账,这将会给客户产生重大损失。有了事务控制之后,当操作对方的账户发生异常时,可以将个人账户中扣除的钱进行撤回,从而保证用户资金账户的安全性。
Java 作为一个高级开发语言,同样支持数据库的事务控制。在上文中,我们了解到所有涉及到数据库的操作,都需要通过数据库连接对象来完成。当我们在操作数据库时,如果想要开启手动事务控制(默认是自动提交),其实通过连接对象的autoCommit参数就可以完成,例如如下示例:
// 1.加载数据库驱动包
Class.forName(DRIVER_CLASS);
// 2.创建一个数据库连接实例
Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASSWORD);
Statement statement = null;
try {
// 3.设置手动事务提交,默认是true
conn.setAutoCommit(false);
// 4.执行多条SQL语句
statement = conn.createStatement();
statement.executeUpdate("insert into tb_user(id, name) values(1, 'tom') ");
statement.executeUpdate("insert into tb_role(id, name) values(1, 'sys') ");
...
// 5.提交事务
conn.commit();
} catch (SQLException e) {
// 如果SQL执行异常,回滚事务
conn.rollback();
}
// 6.关闭连接
statement.close();
conn.close();
了解了 JDBC 的事务控制之后,再来学习 SpringBoot 事务控制就要容易的多,下面我们一起来看看相关的使用方式。
二、SpringBoot 事务
在 Spring 中事务有两种实现方式,分别是编程式事务管理和声明式事务管理。
- 编程式事务管理:利用的是TransactionTemplate类或者更底层的PlatformTransactionManager事务管理器来控制事务操作,用户可以手动提交或者回滚事务,编程上比较灵活
- 声明式事务管理:利用的是@Transactional注解对事务进行管理,本质是通过 AOP 对方法前后进行拦截,在目标方法开始之前创建或者加入一个事务,目标方法执行完成之后根据情况进行提交或者回滚事务,使用上比较简单,易上手
当我们使用 SpringBoot 框架来开发项目的时候,SpringBoot 会自动将 Spring 对数据库事务支持的依赖库加载到工程中,无需再次添加相关依赖包。
下面我们以之前介绍的 SpringBoot 整合 mybatis 的工程为例子,利用事务控制来执行多表数据插入操作,一起来看看这两种事务管理的应用方式。
2.1、编程式事务管理
编程式事务管理主要有两种实现方式,第一种是利用TransactionTemplate类来提交事务,编程简单灵活,也是常用的方式之一;另一种是采用PlatformTransactionManager事务管理器来控制事务的提交和回滚。
我们先看看更底层的PlatformTransactionManager接口应用方式。
2.1.1、PlatformTransactionManager 事务管理器
利用PlatformTransactionManager事务管理器来实现事务的操作,示例如下:
@Service
publicclass ApiService {
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(ApiService.class);
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private PlatformTransactionManager transactionManager;
public void insert(Role role, Menu menu){
//手动开启事务
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
LOGGER.error("提交数据异常",e);
}
}
}
在执行角色信息插入和菜单信息插入的时候,如果都成功,则提交事务;如果任意其中一个方法失败,整个操作进行回滚。
关于事务管理器,无论采用的是 JPA 还是 JDBC 等,底层的事务管理器都实现自PlatformTransactionManager接口。如果采用的是spring-boot-starter-jdbc或者Mybatis操作数据库,Spring Boot 框架会默认将DataSourceTransactionManager实例作为实现类;如果采用的是spring-boot-starter-data-jpa,框架会默认将JpaTransactionManager实例作为实现类。
关于这一点,我们可以写一个测试方法来查看PlatformTransactionManager接口的实现类,具体如下:
@MapperScan("com.example.mybatis.mapper")
@SpringBootApplication
publicclass Application {
@Bean
public Object testBean(PlatformTransactionManager platformTransactionManager){
System.out.println("transactionManager:" + platformTransactionManager.getClass().getName());
returnnew Object();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
启动服务,输出结果如下:
transactionManager:org.springframework.jdbc.datasource.DataSourceTransactionManager
2.1.2、TransactionTemplate 事务模板类
除了采用事务管理器来实现事务手动控制,Spring 事务框架还为用户提供了TransactionTemplate事务模板类,通过它也可以实现事务的手动控制,并且操作更加简单,示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private TransactionTemplate transactionTemplate;
/**
* 方式一:带返回值的事务提交
* @param role
* @param menu
*/
public void insert1(Role role, Menu menu){
Integer result =transactionTemplate.execute(status -> {
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
return1;
});
}
/**
* 方式二:忽略返回值的事务提交
* @param role
* @param menu
*/
public void insert2(Role role, Menu menu){
transactionTemplate.execute(status -> {
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
returnnull;
});
}
/**
* 方式三:不带返回值的事务提交
* @param role
* @param menu
*/
public void insert3(Role role, Menu menu){
transactionTemplate.execute(new TransactionCallbackWithoutResult(){
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
}
});
}
}
以上三种方式,都可以实现实现事务的手动控制,效果等同于采用事务管理器来实现事务手动控制。
如果仔细翻查TransactionTemplate类的execute()方法,你会发现它底层的实现逻辑,与上文介绍的利用事务管理器来控制事务的提交和回滚操作类似。
execute()方法的部分核心源码如下!
图片
因此,在编程式事务管理方式下,推荐采用TransactionTemplate类来实现,编程上会更加灵活简单。
2.2、声明式事务管理
声明式事务管理就更加简单了,只需要在方法上增加注解@Transactional即可,无需任何配置。
2.2.1、Transactional 注解应用示例
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
public void insert(Role role, Menu menu){
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
}
}
声明式事务管理方式,本质采用的是 AOP 动态代理的方式,对标注@Transactional注解的方法进行前后拦截,然后通过事务管理器来实现事务控制。
尽管@Transactional注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 不推荐在接口或者接口方法上使用该注解,如果编程不当某些场景下可能会失效。当作用于类上,那么该类的所有public方法将都具有事务属性。在实际使用过程中,推荐在类的方法上使用该注解,以便实现精准的事务控制。
2.2.2、Transactional 注解失效场景
在使用@Transactional注解时,有以下几个场景,事务可能不会生效!
- 场景一:@Transactional注解如果应用在非public方法,事务不会生效,并且不会抛异常,该注解只会代理public修饰的方法
- 场景二:同一个类中的方法,调用标注@Transactional注解的方法,事务控制也不会生效
- 场景三:内部异常如果被catch吃了,事务不会回滚
- 场景四:@Transactional注解默认只对运行时异常或者 Error 才回滚事务,其它场景不会触发事务回滚,如果异常不在范围之内,事务不会回滚
- 场景五:@Transactional注解上的配置参数使用不当,可能导致事务失效
下面我们每个场景下,错误的用法。
事务失效:场景一
@Transactional注解应该只被应用到public方法上,如果应用在非public方法,事务不会生效,并且不会抛异常,错误示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
protected void insert(Role role, Menu menu){
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
}
}
此时,执行insert操作的时候会自动提交,Spring Boot 不会开启事务控制。假如menuService.insert()方法执行异常,此时roleService.insert()提交的数据不会回滚。
原因在于:@Transactional注解只会代理public修饰的方法,由 Spring AOP 代理决定的。
事务失效:场景二
同一个类中的方法,如果调用标注@Transactional注解的方法,事务控制也不会生效,错误示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
public void save(Role role, Menu menu){
insert(role, menu);
}
@Transactional
public void insert(Role role, Menu menu){
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
}
}
当外部调用save()方法来保存数据的时候,此时 Spring Boot 不会开启事务控制,会自动提交数据,如果执行过程中发生异常,之前执行过的数据操作不会回滚。
原因在于:被@Transactional标注的方法,只有被当前类以外的代码调用时,才会由 Spring Aop 生成的代理对象来管理。
事务失效:场景三
被@Transactional标注的方法,内部异常如果被手动catch吃了,事务不会回滚,错误示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
public void insert(Role role, Menu menu){
try {
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
} catch (Exception e){
e.printStackTrace();
// todo..
}
}
}
此时,被@Transactional标注的方法具备事务控制,如果执行过程中发生异常,数据不会回滚,因为异常被捕获了。当 Spring AOP 事务代理类没有感知到异常时,会自动提交事务。
事务失效:场景四
@Transactional注解默认只对运行时异常或者 Error 才回滚事务,其它场景不会触发事务回滚,如果异常不在范围之内,事务不会回滚,错误示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional
public void insert(Role role, Menu menu) throws Exception {
try {
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
} catch (Exception e){
thrownew Exception("保存错误");
}
}
}
此时中途如果插入数据失败,会抛Exception异常,但是之前执行成功的数据不会回滚。
如果想要支持其它类型的异常,可以在@Transactional注解类上配置rollbackFor参数,比如如下示例:
@Transactional(rollbackFor = Exception.class)
这个参数配置仅限于 Throwable 异常类及其子类。
事务失效:场景五
在@Transactional注解类上,其实隐含了很多的事务属性参数,如果参数配置不当,可能也会导致事务失效,错误示例如下:
@Service
publicclass ApiService {
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
@Transactional(readOnly = true)
public void insert(Role role, Menu menu){
// 新增角色信息
roleService.insert(role);
// 新增菜单信息
menuService.insert(menu);
}
}
此时提交数据会报错,因为readOnly = true参数表示只读模式,不能对数据库的数据进行更改操作。
三、事务注解详解
在上文中,我们介绍了@Transactional事务注解的基本用法,正如上文所说,在注解类上,其实隐含了很多的事务属性参数,Transactional注解类源码如下。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public@interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
下面我们一起来看看每个属性的作用。
属性 | 类型 | 默认值 | 说明 |
transactionManager | String | DEFAULT | 事务管理器 |
propagation | Propagation枚举 | REQUIRED | 事务传播属性 |
isolation | Isolation枚举 | DEFAULT | 事务隔离级别 |
timeout | int | -1 | 超时(秒) |
readOnly | boolean | false | 是否只读 |
rollbackFor | Class[] | {} | 需要支持回滚的异常类 |
rollbackForClassName | String[] | {} | 需要支持回滚的异常类名 |
noRollbackFor | Class[] | {} | 不需要支持回滚的异常类 |
noRollbackForClassName | String[] | {} | 不需要支持回滚的异常类名 |
我们重点看看transactionManager、propagation和isolation这三个参数属性值配置,其它参数基本上见名之意,就不用介绍了。
3.1、事务管理器属性
默认情况下,不需要我们手动配置事务管理器实例。如果 Spring 容器中有多个事务管理器实例,比如多数据源的情况下,某些场景下,就需要我们手动指定事务管理器实例。
具体应用示例如下:
@Configuration
publicclass TransactionManagerConfigBean {
@Autowired
private DataSource dataSource;
/**
* 自定义一个事务管理器1,同时作为默认事务管理器
* @return
*/
@Bean(name = "txManager1")
@Primary
public PlatformTransactionManager txManager1() {
returnnew DataSourceTransactionManager(dataSource);
}
/**
* 自定义一个事务管理器2
* @return
*/
@Bean(name = "txManager2")
public PlatformTransactionManager txManager2() {
returnnew DataSourceTransactionManager(dataSource);
}
}
如果需要使用指定的事务管理器,只需要在@Transactional注解中配置相应的参数即可。
@Service
publicclass ApiService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Transactional(value = "txManager2")
public void insert(Role role, Menu menu) throws Exception {
// 新增角色信息
roleMapper.insert(role);
// 新增菜单信息
menuMapper.insert(menu);
}
}
3.2、事务传播属性
事务传播属性,指的是当一个方法内同时存在多个事务的时候,Spring 如何处理这些事务的行为。
Spring 支持 7 种事务传播方式,Propagation枚举类支持的属性值如下:
- REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
- MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
- NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
- NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
- NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED
如果想要指定事务传播行为,可以通过propagation属性设置,例如:
@Transactional(propagation = Propagation.REQUIRED)
Spring 默认采用的是REQUIRED属性值,也就是说,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
这样设计的好处在于:当一个方法存在多个事务开启的操作时,只会有一个有效的事务实例,可以实现数据的原子性操作。
比如如下示例:
@Service
publicclass ApiService {
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(ApiService.class);
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
@Autowired
private PlatformTransactionManager transactionManager;
public void save(Role role, Menu menu){
//手动开启事务
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 新增角色信息
roleService.insert(role);
// 新增菜单信息
menuService.insert(menu);
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
LOGGER.error("提交数据异常",e);
}
}
}
@Service
public class RoleService {
@Autowired
private RoleMapper roleMapper;
@Transactional
public void insert(Role role){
roleMapper.insert(role);
}
}
@Service
publicclass MenuService {
@Autowired
private MenuMapper menuMapper;
@Autowired
private TransactionTemplate transactionTemplate;
public void insert(Menu menu){
transactionTemplate.execute(status -> {
menuMapper.insert(menu);
returnnull;
});
}
}
当调用ApiService.save()方法时,如果出现异常,所有的操作都会回滚;反之,提交事务。
3.3、事务隔离级别属性
事务隔离级别,可以简单的理解为数据库的事务隔离级别。
从数据库角度,为了解决多个事务操作同一条数据产生的并发问题,提出了事务隔离级别概念,由低到高依次为 Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读等这几类问题,每个隔离级别作用如下:
- read uncommitted:俗称读未提交,指的是一个事务还没提交时,它做的变更就能被别的事务看到。
- Read committed:俗称读提交,指的是一个事务提交之后,它做的变更才会被其他事务看到。
- Repeatable read:俗称可重复读,指的是一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,同时当其他事务在未提交时,变更是不可见的。
- Serializable:俗称串行化,顾名思义就是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
在 Spring 中,事务隔离级别的设置可以通过Isolation枚举类来指定,其支持的属性值如下:
- DEFAULT:默认值,表示使用底层数据库的默认隔离级别。大部分数据库,默认隔离级别为可重复读;Mysql 有些例外,采用可重复读隔离级别
- READ_UNCOMMITTED:对应数据库中读未提交的隔离级别
- READ_COMMITTED :对应数据库中读提交的隔离级别
- REPEATABLE_READ :对应数据库中可重复读的隔离级别
- SERIALIZABLE:对应数据库中串行化的隔离级别
如果想要指定事务隔离级别,可以通过isolation属性设置,例如:
@Transactional(isolation = Isolation.DEFAULT)
四、小结
最后总结一下,编程式的事务管理比较灵活,如果当前操作非常耗时,可以采用编程式的事务管理来提交事务,避免长事务影响数据库性能;其次如果数据操作比较简单时间短,可以采用声明式事务管理,如果使用不当,可能会导致事务失效,因此在实际使用中要多加小心。
本文主要围绕 Spring Boot 事务管理的使用方式,做了一次知识内容的总结,如果有描述不对的地方,欢迎留言指出。
五、参考
1.https://www.cnblogs.com/sharpest/p/7995203.html
2.https://blog.csdn.net/MinggeQingchun/article/details/119579941