文章目录
1. 事务简单介绍
事务定义:
将一组操作封装成一个执行单元,要么全部成功,要么全部失败。
事务的意义:
当执行某个操作,例如支付操作时(分为先将钱从个人账户扣除和将他人账户新增两个操作),如果这两个操作不能同时成功或者失败,那么就会出现财产问题。而使用事务就能够很好的解决这个问题。
2. Spring 中事务的实现
Spring 中的事务操作分为两类:
- 手动操作事务
- 声明式自动提交事务
2.1 Spring 手动操作事务
Spring 手动操作事务分为三个步骤:
- 开启事务(获取事务)
- 提交事务
- 回滚事务
SpringBoot 内置了两个对象,可以用来处理事务:
- JDBC 事务管理器 DataSourceTransactionManager 用来获取事务、提交事务和回滚事务。
- TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得一个事务 TransactionStatus。
具体代码实现如下:
@RestController
@RequestMapping("user")
public class UserController {
// JDBC 事务管理器
@Resource
private DataSourceTransactionManager dataSourceTransactionManager;
// 定义事务属性
@Resource
private TransactionDefinition transactionDefinition;
@RequestMapping("/test1")
public String test1(){
// 开启事务
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
// 执行数据库操作
// 提交事务
dataSourceTransactionManager.commit(transaction);
// 回滚事务
dataSourceTransactionManager.rollback(transaction);
return "测试完成!";
}
}
2.2 Spring 声明式事务
声明式事务的实现相较于手动实现要简单很多,只需要在需要添加事务的方法上加上 @Transactional
注解就可以,无需手动开启事务和提交事务,进入方法时会自动开启事务,方法执行完成会自动提交事务,如果中途发生了没有处理的异常就会自动回滚事务。
具体代码实现如下:
@RestController
@RequestMapping("user")
public class UserController {
@Transactional
@RequestMapping("/test2")
public String test2(){
// 执行数据库操作
return "测试完成!";
}
}
当我们在执行完数据库操作后,有一个语句出现异常,通过 @Transactional 注解就能进行回滚。
3. @Transactional 注解介绍
通过上文我们了解到,在需要的方法上添加 @Transactional 注解,就能自动开启事务。接下来将会具体了解下 @Transactional 的使用细节。
3.1 @Transactional 作用范围
@Fransactional 可以用来修饰方法或类:
- 修饰方法时:表示当前的方法开启事务,需要注意只能应用在 public 方法上。
- 修饰类时:表示当前该类下面的所有被 public 修饰的方法都开启事务。
3.2 @Transactional 参数说明
参数 | 作用 |
---|---|
value | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。 |
transactionManager | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。 |
propagation | 事务的传播行为,默认值为 Propagation.REQUIRED。 |
isolation | 事务的隔离级别,默认值为 Isolation.DEFAULT。 |
timeout | 事务的超时时间,默认值为-1(表示没有超时时间)。如果超过该超时时间显示但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值为 false。为了忽略那些不需要事务的方法,比如读取数据,可以设置为 true。 |
rollbackFor | 用于指定能够被触发事务回滚的异常类型,可以指定多个异常类型。 |
rollbackForClassName | 用于指定能够被触发事务回滚的异常类型,可以指定多个异常类型。 |
noRollbackFor | 抛出异常的类型,不回滚事务,也可以指定多个异常类型。 |
noRollbackForClassName | 抛出异常的类型,不回滚事务,也可以指定多个异常类型。 |
注意: rollbackFor 是将指定的非运行时异常进行回滚。
3.3 @Transactional 出现异常注意事项
@Transactional 在异常被捕获的情况下,不会进行事务的自动回滚。
注意: 默认情况下,Spring 中的事务如果遇到运行时异常,事务是会进行回滚的,但遇到非运行时异常,事务不会自动回滚。可以设置 rollbackFor 来解决非运行时异常不会被回滚的问题。
示例代码如下:
@RequestMapping("/test3")
@Transactional
public String test3(@RequestParam String username, @RequestParam String pwd) {
// 插⼊数据库
int result = userService.addUser(username, pwd);
try {
// 执⾏了异常代码
int i = 10 / 0;
} catch (Exception e) {
}
return "测试完成!";
}
以上代码虽然出现了算数异常,但是由于主动捕获了,因此不会进行事物的回滚,数据库中会插入该条数据。
如果要解决出现异常事务不能自动回滚的问题,以下提供两种解决方案:
-
方案一:对于捕获的异常,事务是不会自动回滚的,因此可以在捕获异常后主动将该异常重新抛出。示例代码如下:
@RequestMapping("/test3") @Transactional public String test3(@RequestParam String username, @RequestParam String pwd) { // 插⼊数据库 int result = userService.addUser(username, pwd); try { // 执⾏了异常代码 int i = 10 / 0; } catch (Exception e) { // 将异常重新抛出 throw e; } return "测试完成!"; }
-
方案二(推荐):手动去回滚事务,可以通过方法
TransactionAspectSupport.currentTransactionStatus()
得到当前的事务,然后设置回滚方法setRollbackOnly
就可以实现回滚。示例代码如下:@RequestMapping("/test3") @Transactional public String test3(@RequestParam String username, @RequestParam String pwd) { // 插⼊数据库 int result = userService.addUser(username, pwd); try { // 执⾏了异常代码 int i = 10 / 0; } catch (Exception e) { // 手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return "测试完成!"; }
3.4 @Transactional 工作原理
@Transactional 是基于 AOP 实现的,AOP 又是使用了动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理;如果目标对象没有实现接口,会使用 CGLIB 动态代理。@Transactional 在开始执行业务之前,通过代理先开启事务,在执行成功之后再提交事务。如果中途遇到异常,则回滚事务。
@Transactional 实现思路:
@Transactional 具体执行细节:
4. 事务隔离级别
4.1 事务特性
事务有4大特性,简称为 ACID,分别如下:
-
原子性(Atomicity):
一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束再在某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像事务从来没有执行一样。
-
一致性(Consistency):
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
-
持久性(Isolation):
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
-
隔离性(Durability):
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)、串行化(serializable)
在这四种特性中,只有隔离性可以设置,通过设置事务的隔离级别可以用来保障多个并发事务执行更加可控,更符合操作者的预期,是防止其它的事务影响当前事务执行的一种策略。
4.2 MySQL 事务隔离级别有4种
- READ UNCOMMITTED: 读未提交,也叫做未提交读,该隔离级别的事务可以看到其它事务中未提交的数据。该隔离级别因为可以读到其它事务中未提交的数据,而未提交的数据可能发生回滚,因此该隔离级别读取的数据称之为脏数据,把这个问题称之为脏读。
- READ COMMITTED: 读已提交,也叫提交读,该隔离级别的事务能读取到已提交事务的数据,因此它不会有脏读问题。但由于在事务的执行中可以读到其它事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。
- REPEATABLE READ: 可重复读,是 MySQL 的默认事务隔离级别,它能确保同一事务多次查询的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读。
- SERIALIZABLE: 串行化,事务最高隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读已提交 | × | √ | × |
可重复度 | × | × | √ |
串行化 | × | × | × |
- 脏读:一个事务读取到另一个事务修改的数据后,后一个事务又进行了回滚操作,从而导致第一个事务读取的数据是错误的。
- 不可重复读:一个事务两次查询到的结果不同,因为在两次查询中间,有另一个事务把数据修改了。
- 幻读:一个事务两次查询中得到的结果集不同,因为在两次查询中另一个事务新增了一部分数据。
在数据库中可以通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
4.3 Spring 事务隔离级别有5种
- Isolation.DEFAULT: 以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED: 读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITED: 读已提交,只能读到已提交的事务,解决了脏读,存在不可重复读。
- Isolation.REPEATABLE_READ: 可重复读,解决了不可重读读,但存在幻读。
- Isolation.SERIALIZABLE: 串行化,可以解决所有并发问题,但性能低。
Spring 中事务隔离级别可以通过 @Transactional 的 Isolation 属性进行设置。
5. Spring 事务传播机制
5.1 事务传播机制的定义
Spring 事务传播机制定义了多个包含了事务的方法在相互调用时,事务是如何在这些方法之间进行传递的。
5.2 事务传播机制的作用
- 事务隔离级别是保证多个并发事务执行的可控性,而事务传播机制是保证一个事务在多个调用方法之间的可控性。
- 事务隔离级别是解决多个事务同时调用数据库的问题,而事务传播机制是解决一个事务在多个节点中传递的问题。
5.3 Spring 事务传播机制有7种
- Propagation.REQUIRED: 默认的事务传播机制,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- Propagation.SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW: 表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,该传播机制修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 Propagation.REQUIRED。
嵌套事务和加入事务的区别:
- 嵌套事务:回事有问题的事务,但主事务不受影响。
- 加入事务:如果任意一个方法出现异常,那么整个事务会回滚。