在之前的博客已经介绍了在 Spring 环境中整合 mybatis 完成数据库的增删查改操作,在正常情况下,操作数据库是没有问题的,但是当一个业务需要并发式的操作数据库,并且需要涉及到修改,插入,删除操作就可能会有问题,如转账业务,其实是有两个步骤,第一步从 A 账户扣钱,第二步在 B 账户中加钱。
如果第一步顺利执行的,在执行完成第二步前,程序发生了异常, 此时第二个操作就不能够正常执行了,这就会是很严重的事故了,致使 A 的钱少了,但 B 的钱没有增多,无论是用户还是我们都是不能容忍这样的 bug 的。
为了解决这个问题,Spring 引入了事务管理的机制,事务的作用是保证执行一组数据库操作的时候,要么全部失败,要不全部成功,即同成功或同失败。
那也就是在程序发生异常的时候,回滚所有已经成功数据库操作,这样就算这一次转账失败了,也不会给客户和商家带来损失。
Spring 中事务的作用是保证在数据层或业务层执行的一系列数据库操作同成功或同失败。
Spring 中的事务分为两类:
SpringBoot 内置了两个对象,DataSourceTransactionManager用来获取事务(开启事务、提交事务、回滚事务);ransactionDefinition是事务的属性,在获取事务时,需要将TransactionDefinition传递进去获取一个TransactionStatus。
Controller,Service,Mapper 各层演示代码如下:
package com.example.demo.controller; import com.example.demo.model.UserInfo; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; // JDBC 事务管理器 @Autowired private DataSourceTransactionManager transactionManager; // 定义事务属性 @Autowired private TransactionDefinition transactionDefinition; @RequestMapping("/add") public int del(Integer id) { UserInfo userinfo = new UserInfo(); userinfo.setUsername("zhaoliu"); userinfo.setPassword("123"); if(userinfo != null) { //开启事务 TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition); //删除用户业务操作 int result = userService.add(userinfo); System.out.println("受影响行数: " + result); // 提交事务/回滚事务 // transactionManager.commit(transactionStatus); //提交事务 transactionManager.rollback(transactionStatus); //回滚事务 } return 0; } }
数据库 userinfo 表中现有数据如下:
启动程序,在浏览器地址栏中进行 url 的访问,结果如下:
控制台显示的结果是插入成功的,此时再看数据库数据是否有变化。
我们可以发现,数据库中数据是没有变化的,说明在插入数据后,事务成功的进行了回滚操作,但是这样的方式是比较繁琐的,下面介绍更简单的声明式事务。
声明式事务我们只需要在方法上加上@Transactional注解就可以实现,此时无需我们手动进行开启事务和提交事务,进入方法就会自动开启事务,执行完毕自动提交,发生异常后会自动回滚事务。
package com.example.demo.controller; import com.example.demo.model.UserInfo; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/user") @RestController public class UserController2 { @Autowired private UserService userService; @Transactional @RequestMapping("/add") public int add() { UserInfo userinfo = new UserInfo(); userinfo.setUsername("zhaoliu"); userinfo.setPassword("123"); // 非空判断 if (userinfo == null) { return 0; } // 调用 service 执行添加 int result = userService.add(userinfo); System.out.println("受影响行数: " + result); // 将结果返回给前端 return result; } }
目前数据库数据:
访问结果:
此时数据库中就成功插入了一条数据,这种情况是没有异常的情况下,代码执行成功后自动进行了commit。
🍂我们来在业务中加上一段异常代码:
@RestController @RequestMapping("/user2") public class UserController2 { @Autowired private UserService userService; @Transactional @RequestMapping("/del") public int del(Integer id) { if(id == null || id <= 0) { return 0; } int result = userService.del(id); int n = 1 / 0; //异常业务 return result; } }
重新访问进行插入操作,结果如下:
可以看到的控制台中信息显示成功的进行了插入操作,但同时报了一个算术异常,再来看数据库;
还是原来的那几条记录,此时的情况就是发生异常自动进行了回滚操作了。
@Transactional 的作用范围:它既可以用来修饰方法也可以用来修饰类。
我们可以通过设置 @Transactional 的一些参数来决定事务的一些具体的功能。
参数 | 作用 |
---|---|
value | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。 |
transactionManager | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。 |
propagation | 事务的传播行为,默认值为 Propagation.REQUIRED |
isolation | 事务的隔离级别,默认值为 lsolation.DEFAULT |
timeout | 事务的超时时间,默认值为 -1,如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only为true。 |
rollbackFor | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。 |
rollbackForClassName | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。 |
noRollbackFor | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。 |
noRollbackForClassName | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。 |
要注意:@Transactional 在异常被捕获的情况下,不会进行事务自动回滚。
我们将上面的代码手动创建的一个异常进行try catch处理。
再来进行访问,结果如下:
此时程序抛出了异常,但数据库数据插入后并没有进行回滚操作,出现这种情况的原因是事务AOP通知只有自己捕捉到了目标抛出的异常,才能进行后续的回滚操作,如果目标自己处理掉了异常,事务是无法知悉的,也就无法处理了。
🎯解决方案:
1️⃣方式一:在 catch 块中将异常继续抛出,此时代理对象就能感知到异常,也就能自动的回滚事务了。
再次访问,结果如下:
此时数据库中数据是没有变化的,即在成功插入后进行了回滚操作。
2️⃣方式二: 手动回滚事务,在方法中使用TransactionAspectSupport.currentTransactionStatus()可以得到当前的事务,然后设置回滚方法setRollbackOnly就可以实现回滚了。
访问结果:
此时数据插入后也是成功进行回滚,而程序也不会报错。
@Transactional 是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
@Transactional 在开始执行业务之前,通过代理先开启事务,在执行成功之后再提交事务。如果中途遇到异常,会进行回滚业务。
@Transactional 具体执行主要是以下逻辑:
我们可以使用以下 sql 查询 MySQL 中全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
Spring 的事务隔离级别是有以下 5 种的,
相比于 MySQL 中多了一种DEFAULT。
默认情况下,Spring 会使用底层数据库的默认隔离级别,通常是 READ_COMMITTED 级别。可以通过事务管理器的 setDefaultTransactionIsolation() 方法或在@Transactional注解中使用isolation属性来设置隔离级别:
Spring 事务传播机制定义了多个包含事务的方法相互调用时的行为。
事务隔离级别是保证多个并发事务执行是可控的,而事务传播机制是保证一个事务在多个调用方法间是可控的。
事务隔离级别是为了解决多个事务同时调用一个数据库的问题:
事务传播机制是解决一个事务在多个方法中传递的问题:
🍂Spring 事务传播机制包含以下 7 种:
通过设置@Transactional注解中的propagation属性就可以完成Spring事务传播机制的修改。
🍂事务传播机制可以分为以下三类:
上面的前 6 种事务机制是比较容易理解的,下面主要来演示一下NESTED这种嵌套事务的效果,为了便于观察,我们清一下数据库中的数据;
首先我们在UserService再声明一个insert方法,方法中再调用一次UserMapper中的add接口执行插入逻辑;在UserController2的add中再加入一个调用insert的逻辑。
把调用逻辑写好后构造嵌套事务,将UserController2中的add的事务机制声明为REQUIRED,UserService中的add和insert的事务机制都声明为NESTED,整体代码如下:
@RequestMapping("/user2") @RestController public class UserController2 { @Autowired private UserService userService; @Transactional(propagation = Propagation.REQUIRED) @RequestMapping("/add") public int add() { UserInfo userinfo = new UserInfo(); userinfo.setUsername("zhaoliu"); userinfo.setPassword("123"); // 非空判断 if (userinfo == null) { return 0; } int result = userService.add(userinfo); result +=userService.insert(userinfo); System.out.println("受影响行数: " + result); return result; } } @Service public class UserService { @Autowired private UserMapper userMapper; @Transactional(propagation = Propagation.NESTED) public int add(UserInfo userinfo) { int result = userMapper.add(userinfo); System.out.println("add result -> " + result); return result; } @Transactional(propagation = Propagation.NESTED) public int insert(UserInfo userInfo) { int result = userMapper.add(userInfo); System.out.println("insert result ->" + result); try { int num = 10 / 0; } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return result; } }
访问结果:
此时执行到Controller中的add会开启一个事务,其中调用的add和insert开始的是嵌套事务,嵌套事务如果发生了回滚,只会影响它自己的局部逻辑,而不影响全局。
控制台显示的是成功插入了两条记录的,
查看数据库只是成功插入了一条记录,这是因为insert逻辑中发生了回滚,但它不会影响其他部分的逻辑。
要注意在嵌套事务中可能出现异常的部分要手动处理进行回滚,不能让异常抛出,否则会被全局代理感知到造成全局事务的整体回滚。
嵌套事务之所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务进入之后相当于新建了⼀个保存点,而滚回时只回滚到当前保存点,因此之前的事务是不受影响的,这一点可以在 MySQL 的官方文档汇总找到相应的资料:网页链接 。
🍂嵌套事务(NESTED)和加入事务(REQUIRED )的区别: