今天我们接着来聊聊 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.设置手动事务提交,默认是trueconn.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 事务控制就要容易的多,下面我们一起来看看相关的使用方式。
在 Spring 中事务有两种实现方式,分别是编程式事务管理和声明式事务管理。
编程式事务管理:利用的是TransactionTemplate类或者更底层的PlatformTransactionManager事务管理器来控制事务操作,用户可以手动提交或者回滚事务,编程上比较灵活
声明式事务管理:利用的是@Transactional注解对事务进行管理,本质是通过 AOP 对方法前后进行拦截,在目标方法开始之前创建或者加入一个事务,目标方法执行完成之后根据情况进行提交或者回滚事务,使用上比较简单,易上手
当我们使用 SpringBoot 框架来开发项目的时候,SpringBoot 会自动将 Spring 对数据库事务支持的依赖库加载到工程中,无需再次添加相关依赖包。
下面我们以之前介绍的 SpringBoot 整合 mybatis 的工程为例子,利用事务控制来执行多表数据插入操作,一起来看看这两种事务管理的应用方式。
编程式事务管理主要有两种实现方式,第一种是利用TransactionTemplate类来提交事务,编程简单灵活,也是常用的方式之一;另一种是采用PlatformTransactionManager事务管理器来控制事务的提交和回滚。
我们先看看更底层的PlatformTransactionManager接口应用方式。
利用PlatformTransactionManager事务管理器来实现事务的操作,示例如下:
@Servicepublicclass ApiService { privatestaticfinal Logger LOGGER=LoggerFactory.getLogger(ApiService.class);@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;@Autowiredprivate PlatformTransactionManager transactionManager;publicvoidinsert(Role role,Menu menu){//手动开启事务TransactionStatusstatus=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")@SpringBootApplicationpublicclass Application {@BeanpublicObject testBean(PlatformTransactionManager platformTransactionManager){ System.out.println("transactionManager:"+platformTransactionManager.getClass().getName());returnnew Object();}publicstatic void main(String[]args){ SpringApplication.run(Application.class,args);} }
启动服务,输出结果如下:
transactionManager:org.springframework.jdbc.datasource.DataSourceTransactionManager
除了采用事务管理器来实现事务手动控制,Spring 事务框架还为用户提供了TransactionTemplate事务模板类,通过它也可以实现事务的手动控制,并且操作更加简单,示例如下:
@Servicepublicclass ApiService {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;@Autowiredprivate TransactionTemplate transactionTemplate;/** * 方式一:带返回值的事务提交 * @param role * @param menu */publicvoid insert1(Role role,Menu menu){Integerresult=transactionTemplate.execute(status->{// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);return1;});}/** * 方式二:忽略返回值的事务提交 * @param role * @param menu */publicvoid insert2(Role role,Menu menu){ transactionTemplate.execute(status->{// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);returnnull;});}/** * 方式三:不带返回值的事务提交 * @param role * @param menu */publicvoid insert3(Role role,Menu menu){ transactionTemplate.execute(new TransactionCallbackWithoutResult(){@Overrideprotected void doInTransactionWithoutResult(TransactionStatus transactionStatus){// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);} });} }
以上三种方式,都可以实现实现事务的手动控制,效果等同于采用事务管理器来实现事务手动控制。
如果仔细翻查TransactionTemplate类的execute()方法,你会发现它底层的实现逻辑,与上文介绍的利用事务管理器来控制事务的提交和回滚操作类似。
execute()方法的部分核心源码如下!
图片
因此,在编程式事务管理方式下,推荐采用TransactionTemplate类来实现,编程上会更加灵活简单。
声明式事务管理就更加简单了,只需要在方法上增加注解@Transactional即可,无需任何配置。
@Servicepublicclass ApiService {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;@Transactionalpublicvoidinsert(Role role,Menu menu){// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);} }
声明式事务管理方式,本质采用的是 AOP 动态代理的方式,对标注@Transactional注解的方法进行前后拦截,然后通过事务管理器来实现事务控制。
尽管@Transactional注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 不推荐在接口或者接口方法上使用该注解,如果编程不当某些场景下可能会失效。当作用于类上,那么该类的所有public方法将都具有事务属性。在实际使用过程中,推荐在类的方法上使用该注解,以便实现精准的事务控制。
在使用@Transactional注解时,有以下几个场景,事务可能不会生效!
场景一:@Transactional注解如果应用在非public方法,事务不会生效,并且不会抛异常,该注解只会代理public修饰的方法
场景二:同一个类中的方法,调用标注@Transactional注解的方法,事务控制也不会生效
场景三:内部异常如果被catch吃了,事务不会回滚
场景四:@Transactional注解默认只对运行时异常或者 Error 才回滚事务,其它场景不会触发事务回滚,如果异常不在范围之内,事务不会回滚
场景五:@Transactional注解上的配置参数使用不当,可能导致事务失效
下面我们每个场景下,错误的用法。
@Transactional注解应该只被应用到public方法上,如果应用在非public方法,事务不会生效,并且不会抛异常,错误示例如下:
@Servicepublicclass ApiService {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;@Transactionalprotected voidinsert(Role role,Menu menu){// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);} }
此时,执行insert操作的时候会自动提交,Spring Boot 不会开启事务控制。假如menuService.insert()方法执行异常,此时roleService.insert()提交的数据不会回滚。
原因在于:@Transactional注解只会代理public修饰的方法,由 Spring AOP 代理决定的。
同一个类中的方法,如果调用标注@Transactional注解的方法,事务控制也不会生效,错误示例如下:
@Servicepublicclass ApiService {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;publicvoidsave(Role role,Menu menu){insert(role,menu);}@Transactionalpublicvoidinsert(Role role,Menu menu){// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);} }
当外部调用save()方法来保存数据的时候,此时 Spring Boot 不会开启事务控制,会自动提交数据,如果执行过程中发生异常,之前执行过的数据操作不会回滚。
原因在于:被@Transactional标注的方法,只有被当前类以外的代码调用时,才会由 Spring Aop 生成的代理对象来管理。
被@Transactional标注的方法,内部异常如果被手动catch吃了,事务不会回滚,错误示例如下:
@Servicepublicclass ApiService {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;@Transactionalpublicvoidinsert(Role role,Menu menu){ try {// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);} catch(Exception e){ e.printStackTrace();// todo..} } }
此时,被@Transactional标注的方法具备事务控制,如果执行过程中发生异常,数据不会回滚,因为异常被捕获了。当 Spring AOP 事务代理类没有感知到异常时,会自动提交事务。
@Transactional注解默认只对运行时异常或者 Error 才回滚事务,其它场景不会触发事务回滚,如果异常不在范围之内,事务不会回滚,错误示例如下:
@Servicepublicclass ApiService {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;@Transactionalpublicvoidinsert(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注解类上,其实隐含了很多的事务属性参数,如果参数配置不当,可能也会导致事务失效,错误示例如下:
@Servicepublicclass ApiService {@Autowiredprivate RoleService roleService;@Autowiredprivate MenuService menuService;@Transactional(readOnly=true)publicvoidinsert(Role role,Menu menu){// 新增角色信息roleService.insert(role);// 新增菜单信息menuService.insert(menu);} }
此时提交数据会报错,因为readOnly = true参数表示只读模式,不能对数据库的数据进行更改操作。
在上文中,我们介绍了@Transactional事务注解的基本用法,正如上文所说,在注解类上,其实隐含了很多的事务属性参数,Transactional注解类源码如下。
@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic@interfaceTransactional {@AliasFor("transactionManager")Stringvalue()default"";@AliasFor("value")String transactionManager()default"";Propagation propagation()defaultPropagation.REQUIRED;Isolationisolation()defaultIsolation.DEFAULT;inttimeout()defaultTransactionDefinition.TIMEOUT_DEFAULT;booleanreadOnly()defaultfalse;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这三个参数属性值配置,其它参数基本上见名之意,就不用介绍了。
默认情况下,不需要我们手动配置事务管理器实例。如果 Spring 容器中有多个事务管理器实例,比如多数据源的情况下,某些场景下,就需要我们手动指定事务管理器实例。
具体应用示例如下:
@Configurationpublicclass TransactionManagerConfigBean {@Autowiredprivate DataSource dataSource;/** * 自定义一个事务管理器1,同时作为默认事务管理器 * @return */@Bean(name="txManager1")@PrimarypublicPlatformTransactionManager txManager1(){ returnnew DataSourceTransactionManager(dataSource);}/** * 自定义一个事务管理器2 * @return */@Bean(name="txManager2")publicPlatformTransactionManager txManager2(){ returnnew DataSourceTransactionManager(dataSource);} }
如果需要使用指定的事务管理器,只需要在@Transactional注解中配置相应的参数即可。
@Servicepublicclass ApiService {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuMapper menuMapper;@Transactional(value="txManager2")publicvoidinsert(Role role,Menu menu)throws Exception {// 新增角色信息roleMapper.insert(role);// 新增菜单信息menuMapper.insert(menu);} }
事务传播属性,指的是当一个方法内同时存在多个事务的时候,Spring 如何处理这些事务的行为。
Spring 支持 7 种事务传播方式,Propagation枚举类支持的属性值如下:
REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED
如果想要指定事务传播行为,可以通过propagation属性设置,例如:
@Transactional(propagation=Propagation.REQUIRED)
Spring 默认采用的是REQUIRED属性值,也就是说,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
这样设计的好处在于:当一个方法存在多个事务开启的操作时,只会有一个有效的事务实例,可以实现数据的原子性操作。
比如如下示例:
@Servicepublicclass ApiService { privatestaticfinal Logger LOGGER=LoggerFactory.getLogger(ApiService.class);@Autowiredprivate RoleService roleService;@Autowiredprivate MenuService menuService;@Autowiredprivate PlatformTransactionManager transactionManager;publicvoidsave(Role role,Menu menu){//手动开启事务TransactionStatusstatus=transactionManager.getTransaction(new DefaultTransactionDefinition());try {// 新增角色信息roleService.insert(role);// 新增菜单信息menuService.insert(menu);// 提交事务transactionManager.commit(status);} catch(Exception e){// 回滚事务transactionManager.rollback(status);LOGGER.error("提交数据异常",e);} } }
@Servicepublicclass RoleService {@Autowiredprivate RoleMapper roleMapper;@Transactionalpublicvoidinsert(Role role){ roleMapper.insert(role);} }
@Servicepublicclass MenuService {@Autowiredprivate MenuMapper menuMapper;@Autowiredprivate TransactionTemplate transactionTemplate;publicvoidinsert(Menu menu){ transactionTemplate.execute(status->{ menuMapper.insert(menu);returnnull;});} }
当调用ApiService.save()方法时,如果出现异常,所有的操作都会回滚;反之,提交事务。
事务隔离级别,可以简单的理解为数据库的事务隔离级别。
从数据库角度,为了解决多个事务操作同一条数据产生的并发问题,提出了事务隔离级别概念,由低到高依次为 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