事务就是将一组操作封装成一个执行单元, 也就是封装到一起, 要么全部成功, 要么全部失败; 那么为什么要使用事务呢???
例如我们去超市购物消费了 500 元, 然后使用支付宝进行付款操作, 我付款成功后账户肯定时 - 500, 那理应超市的账户中 + 500; 但是如果没有事务这个概念, 有可能我付款成功后, 超市并没有 + 500, 也就意味着我这 500 元平白无故的消失了. 因此, 事务就是为了解决这样的问题而生的, 这这一组操作要么一起成功, 要么一起失败.
1 事务
1.1 手动操作事务
关于 Spring 中事务的实现主要分为两类: 手动操作事务 / 声明式自动提交事务; 其中手动操作事务和 MySQL 中操作事务类似, 主要有三个重要操作步骤:
开启事务 (start transaction) --> 提交事务 (commit) --> 回滚事务 (rollback).
SpringBoot 中内置了两个对象用来操作事务:
- DataSourceTransactionManager : 用来获取事务, 也就是开启事务, 提交事务及回滚事务;
- TransactionDefinition: 事务的属性, 在获取事务的时候需要将 TransactionDefinition 传递进去从而获得一个事务 TransactionStatus.
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/add")
public int add(UserInfo userInfo) {
// 非空校验
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
// 开启事务 (获取事务) (将 TransactionDefinition 传递进去从而获得一个事务 TransactionStatus)
TransactionStatus transactionStatus =
transactionManager.getTransaction(transactionDefinition);
int result = userService.add(userInfo);
System.out.println("add 受影响的行数: " + result);
// 提交事务 / 回滚事务
//transactionManager.rollback(transactionStatus); // 回滚事务
transactionManager.commit(transactionStatus);
return result;
}
}
通过上面的代码进行手动操作事务比较繁琐, 虽然也可以实现, 但是有更简单的方式, 所以以后得代码中还是建议使用声明式事务.
1.2 声明式事务
声明式事务的实现比较简单, 只需要在需要事务的方法上面添加注解 @Transactional 即可, 无需开启事务和提交事务, 进入方法时自动开启事务, 方法执行完会自动提交事务, 即使中途出现异常也会自动回滚事务.
@Transactional // 在进入方法之前, 自动开启事务;
// 在方法执行完之后, 自动提交事务; 如果出现异常, 则自动回滚事务
@RequestMapping("/add")
public int add(UserInfo userInfo) {
// 非空校验
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result = userService.add(userInfo);
System.out.println("add 受影响的行数: " + result);
// 下面这句代码就会出现异常 (0 不能做除数), 但是会回滚操作, 并不会污染数据
//int num = 10 / 0;
return result;
}
1.3 事务的作用范围及参数说明
@Transactional 作用范围:
- 修饰方法时: 需要注意只能应用到 public 方法上, 否则不生效;
- 修饰类: 表明该注解对该类中的所有 public 方法都生效; 这里其实更推荐在修饰方法时使用.
@Transactional 参数说明:
1.4 事务的工作原理及实现思路
@Transactional 工作原理
- @Transactional 是基于 AOP 实现的, AOP 又是使用动态代理实现的, 如果目标对象实现了接口, 默认情况下会采用 JDK 的动态代理, 如果目标对象没有实现了接口, 会使用 CGLIB 动态代理.
- @Transactional 在开始执行业务之前, 通过代理先开启事务, 在执行成功之后再提交事务, 如果中途出现异常, 则回滚事务.
@Transactional 实现思路:
@Transactional具体执行细节:
2 事务的隔离级别
事务的隔离级别: 设置隔离级别是用来保障多个并发事务执行更可控, 更符合操作者预期.
事务主要有四大特性: 原子性(要么一次性全部完成, 要么全部不完成) / 一致性(事务从开始到结束数据库的完整性未被破坏) / 持久性(事务处理完成后, 对数据的修改是永久的, 即使系统故障也不会被破坏) / 隔离性.
在事务的四大特性中, 只有隔离性是允许设置的, 而事务的隔离性指的是数据库允许多个并发事务同时对数据进行读写和修改的能力, 隔离性的作用就是防止多个事务并发执行时由于交叉执行而导致数据的不一致. 主要分为五种级别:
- DEFAULT: 使用连接的数据库的事务隔离级别;
- READ_UNCOMMITTED: 读未提交; 此隔离级别的事务可以看到其它事务中未提交的数据, 因为可以读到其它事务中未提交的数据, 而未提交的数据可能会发生回滚, 因此该级别读取到的数据称之为脏数据, 此问题就是脏读;
- READ_COMMITTED: 读已提交; 此隔离级别能读取到已经提交事务的数据, 当然也就不会有脏读情况; 但是由于事务的执行中可以读取到其它事务提交的结果, 所以在不同时间的相同 SQL 查询中可能会得到不同的结果, 这就是不可重复读;
- REPEATABLE_READ: 可重复读; MySQL 默认的事务隔离级别; 可以保证同一事务多次查询的结果一致; 但是此级别的事务正在执行时, 另一个事务插入了某条数据, 但因为每次查询的结果都一样, 所以会导致查询不到此数据, 也就是说明明已经插入了数据但就是读不到, 这就是幻读;
- SERIALIZABLE: 串行化; 事务隔离的最高级别, 会强制事务排序使之不会发生冲突, 从而解决了脏读 / 不可重复读和幻读问题; 但是执行效率比较低, 使用场景不多.
当 Spring 中设置了事务隔离级别和连接的数据库事务隔离级别发生冲突的时候, 就会以 Spring 为准; 毕竟 Spring 中的事务隔离级别机制的实现就是依靠连接数据库支持事务隔离级别为基础. 总之事务的隔离级别就是为了防止其它事务影响当前事务执行的一种策略.
3 事务的传播机制
事务隔离级别解决的事多个事务同时调用数据库的问题; 而事务传播机制解决的是一个事务在多个节点或者方法中传递的问题, 如下所示:
关于嵌套事务 NESTED 和加入事务 REQUIRED 的区别:
- 整个事务如果全部执行成功, 两者的结果是一样的;
- 如果事务执行到一半失败了, 那么 REQUIRED 整个事务会全部回滚; 而嵌套实物则会局部回滚, 不会影响上一个方法中执行的结果 (因为事务中有一个保存点 (savepoint) 的概念, 嵌套事务进入之后相当于新建了一个保存点, 而回滚时只回滚到当前的保存点, 因此之前的事务是不受影响的).