Spring事务传播机制与隔离级别详解
文章目录
- 1. Spring中事务的实现
- 1.1 声明式事务@Transactional
- 2. @Transactional详解
- 2.1 rollbackFor
- 2.2. 事务隔离级别
- 3. 事务传播机制
- 3.1 事务传播机制有哪些
- 3.2 Spring事务传播机制的使用
- 3.2.1 REQUIRED(加入事务)
- 3.2.2. REQUIRES_NEW(新建事务)
- 3.2.3 NESTED和REQUIRED 有什么区别?
1. Spring中事务的实现
事务是一组操作的集合, 是一个不可分割的操作
事务会把所有的操作作为一个整体, 一起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败.
事务的操作主要有三步:
- 开启事务start transaction (一组操作前开启事务)
- 提交事务: commit (这组操作全部成功, 提交事务)
- 回滚事务: rollback (这组操作中间任何一个操作出现异常, 回滚事务)
Spring中的事务操作分两类:
- 编程式事务(手动写代码操作事务)
- 声明式事务(利用注解自动开启和提交事务)
编程式事务感兴趣可自行了解, 本文主要介绍声明式事务
1.1 声明式事务@Transactional
@RequestMapping("/trans")@RestControllerpublicclassTransactionalController{@AutowiredprivateUserServiceuserService;@Transactional@RequestMapping("/registry")Stringregistry(Stringname,Stringpassword){//用户注册userService.registryUser(name,password);inta=10/0;// 强制抛出异常return"注册成功";}}此时数据库却没有新增数据, 事务进行了回滚
我们一般会在业务逻辑层当中来控制事务, 因为在业务逻辑层当中, 一个业务功能可能会包含多个数据访问的操作. 在业务逻辑层来控制事务, 我们就可以将多个数据访问操作控制在一个事务范围内, 这里写在Controller只是方便演示
@Transactional可以用来修饰方法, 也能修饰类
修饰方法时: 只有修饰public 方法时才生效(修饰其他方法时不会报错, 也不生效)[推荐]
修饰类时:对 @Transactional 修饰的类中所有的 public 方法都生效
方法/类被 @Transactional 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结束之后, 自动提交事务;
如果在方法执行过程中, 出现异常, 且异常未被捕获, 就进行事务回滚操作;
如果异常被程序捕获, 方法就被认为是成功执行, 依然会提交事务, 不会回滚;
我们也可以手动回滚事务, 使用TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并使用 setRollbackOnly, 可以写在try-catch中
try{//强制程序抛出异常inta=10/0;}catch(Exceptione){// 手动回滚事务TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}2. @Transactional详解
我们学习了 @Transactional 的基本使用. 接下来我们学习 @Transactional注解的使用细节.
我们主要学习 @Transactional 注解当中的三个常见属性:
- rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
- Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
- propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED
2.1 rollbackFor
@Transactional 默认只在遇到运行时异常和Error时才会回滚, 非运行时异常不回滚. 即Exception的子类中, 除了RuntimeException及其子类
我们修改下代码(IOException不属于RuntimeException)
@Transactional@RequestMapping("/r2")publicStringr2(Stringname,Stringpassword)throwsIOException{//用户注册userService.registryUser(name,password);log.info("用户数据插入成功");if(true){thrownewIOException();}return"r2";}此时虽然程序抛出了异常, 但是事务仍然进行了提交, 数据库中任然插入成功
如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚
@Transactional(rollbackFor=Exception.class)2.2. 事务隔离级别
先回顾下MySQL的事务隔离级别:
- 读未提交(READ UNCOMMITTED):也叫未提交读. 该隔离级别的事务可以看到其他事务中未提交的数据;
因为其他事务未提交的数据可能会发生回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数据称之为脏数据, 这个问题称为脏数;
- 读已提交(READ COMMITTED): 读已提交, 也叫提交读. 该隔离级别的事务能读取到已经提交事务的数据;
该隔离级别不会有脏读的问题.但由于在事务的执行中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读;
- 可重复读(REPEATABLE READ): 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交. 也就可以确保同一事务多次查询的结果一致, 但是其他事务新插入的数据, 是可以感知到的. 这也就引发了幻读问题. 可重复读 是 MySQL 的默认事务隔离级别;
此级别的事务正在执行时, 另一个事务成功的插入了某条数据, 但因为它每次查询的结果都是一样的, 所以会导致查询不到这条数据, 自己重复插入时又失败(因为唯一约束的原因). 明明在事务中查询不到这条信息,但自己就是插入不进去, 这个现象叫幻读
- 串行化(SERIALIZABLE): 序列化, 事务最高隔离级别. 它会强制事务排序, 使之不会发生冲突, 从而解决了脏读, 不可重复读和幻读问题, 但因为执行效率低, 所以真正使用的场景并不多
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | √ | √ | √ |
| 读已提交 | × | √ | √ |
| 可重复读 | × | × | √ |
| 串行化 | × | × | × |
Spring的事务隔离机制有五种:
- Isolation.DEFAULT: 以连接的数据库的事务隔离级别为主.
- Isolation.READ_UNCOMMITTED: 读未提交, 对应SQL标准中 READ UNCOMMITTED
- Isolation.READ_COMMITTED: 读已提交,对应SQL标准中 READ COMMITTED
- Isolation.REPEATABLE_READ: 可重复读, 对应SQL标准中 REPEATABLE READ
- Isolation.SERIALIZABLE: 串行化, 对应SQL标准中 SERIALIZABLE
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置
@Transactional(isolation=Isolation.READ_COMMITTED)3. 事务传播机制
事务传播机制: 多个事务方法存在调用关系时, 事务是如何在这些方法间进行传播的.
比如有两个方法A, B都被 @Transactional 修饰, A方法调用B方法
A方法运行时, 会开启一个事务. 当A调用B时, B方法本身也有事务, 此时B方法运行时, 是加入A的事务, 还是创建一个新的事务呢?
事务隔离级别解决的是多个事务同时调用一个数据库的问题;
事务传播机制解决的是一个事务在多个节点(方法)中传递的问题
3.1 事务传播机制有哪些
@Transactional 注解支持事务传播机制的设置, 通过 propagation 属性来指定传播行为
Spring 事务传播机制有以下 7 种:
- Propagation.REQUIRED: 默认的事务传播级别. 如果当前存在事务, 则加入该事务. 如果当前没有事务, 则创建一个新的事务;
- Propagation.SUPPORTS: 如果当前存在事务, 则加入该事务. 如果当前没有事务, 则以非事务的方式继续运行;
- Propagation.MANDATORY: 强制性. 如果当前存在事务, 则加入该事务. 如果当前没有事务, 则抛出异常;
- Propagation.REQUIRES_NEW: 创建一个新的事务. 如果当前存在事务, 则把当前事务挂起. 也就是说不管外部方法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部方法都会新开启自己的事务, 且开启的事务相互独立, 互不干扰.
- Propagation.NOT_SUPPORTED: 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起(不用).
- Propagation.NEVER:以非事务方式运行, 如果当前存在事务, 则抛出异常;
- Propagation.NESTED: 如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行.如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED
3.2 Spring事务传播机制的使用
对于以上事务传播机制,我们重点关注以下两个就可以了:
- REQUIRED(默认值)
- REQUIRES_NEW
3.2.1 REQUIRED(加入事务)
以下代码:
用户注册, 插入数据
记录操作日志, 插入数据(出现异常)
观察propagation = Propagation.REQUIRED 执行结果
对应的UserService和LogService操作都添加上了@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/propaga")@RestControllerpublicclassPropagationController{@AutowiredprivateUserServiceuserService;@AutowiredprivateLogServicelogService;@Transactional(propagation=Propagation.REQUIRED)@RequestMapping("/p1")publicStringp1(Stringname,Stringpassword){//用户注册userService.registryUser(name,password);//记录操作日志logService.insertLog(name,"用户注册");return"r3";}}@Slf4j@ServicepublicclassUserService{@AutowiredprivateUserInfoMapperuserInfoMapper;@Transactional(propagation=Propagation.REQUIRED)publicvoidregistryUser(Stringname,Stringpassword){//插入用户信息userInfoMapper.insert(name,password);}}@Service@Slf4jpublicclassLogService{@AutowiredprivateLogInfoMapperlogInfoMapper;@Transactional(propagation=Propagation.REQUIRED)publicvoidinsertLog(Stringname,Stringop){inta=10/0;//记录用户操作logInfoMapper.insertLog(name,"用户注册");}}结果:
- p1方法开始执行, 开启事务
- 用户注册, 插入数据, 执行成功(此操作的事务和p1使用同一个)
- 记录操作日志, 插入一条数据, 出现异常, 执行失败(同样和p1 使用同一个事务)
- 第三步出现错误, 事务回滚, 步骤2和3使用同一个事务, 所以步骤2的数据也回滚
3.2.2. REQUIRES_NEW(新建事务)
将上述UserService 和LogService 中相关方法事务传播机制改为
Propagation.REQUIRES_NEW
@RequestMapping("/propaga")@RestControllerpublicclassPropagationController{@AutowiredprivateUserServiceuserService;@AutowiredprivateLogServicelogService;@Transactional(propagation=Propagation.REQUIRES_NEW)@RequestMapping("/p1")publicStringp1(Stringname,Stringpassword){//用户注册userService.registryUser(name,password);//记录操作日志logService.insertLog(name,"用户注册");return"r3";}}@Slf4j@ServicepublicclassUserService{@AutowiredprivateUserInfoMapperuserInfoMapper;@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidregistryUser(Stringname,Stringpassword){//插入用户信息userInfoMapper.insert(name,password);}}@Service@Slf4jpublicclassLogService{@AutowiredprivateLogInfoMapperlogInfoMapper;@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidinsertLog(Stringname,Stringop){inta=10/0;//记录用户操作logInfoMapper.insertLog(name,"用户注册");}}运行程序, 发现用户数据插入成功了, 日志表数据插入失败
LogService 方法中的事务不影响 UserService 中的事务
当我们不希望事务之间相互影响时, 可以使用该传播行为
3.2.3 NESTED和REQUIRED 有什么区别?
整个事务如果全部执行成功, 二者的结果是一样的;
如果事务一部分执行成功, REQUIRED加入事务会导致整个事务全部回滚. NESTED嵌套事务可以实现局部回滚, 不会影响上一个方法中执行的结果.
嵌套事务之所以能够实现部分事务的回滚, 是因为事务中有一个保存点(savepoint)的概念, 嵌套事务进入之后相当于新建了一个保存点, 而滚回时只回滚到当前保存点.
REQUIRED 是加入到当前事务中, 并没有创建事务的保存点, 因此出现了回滚就是整个事务回滚, 这就是嵌套事务和加入事务的区别