Spring事务和事务传播机制

事务回顾

在数据库阶段, 我们已经学习过事务了.

什么是事务?

事务是⼀组操作的集合, 是⼀个不可分割的操作.
事务会把所有的操作作为⼀个整体, ⼀起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败.

为什么需要事务?

我们在进⾏程序开发时, 也会有事务的需求.
⽐如转账操作:
第⼀步:A 账⼾ -100 元.
第⼆步:B 账⼾ +100 元.
如果没有事务,第⼀步执⾏成功了, 第⼆步执⾏失败了, 那么A 账⼾的100 元就平⽩⽆故消失了. 如果使⽤事务就可以解决这个问题, 让这⼀组操作要么⼀起成功, 要么⼀起失败.

事务的操作

事务的操作主要有三步:

  1. 开启事start transaction/ begin (⼀组操作前开启事务)
  2. 提交事务: commit (这组操作全部成功, 提交事务)
  3. 回滚事务: rollback (这组操作中间任何⼀个操作出现异常, 回滚事务)
    -- 开启事务
    start transaction;
    -- 提交事务
    commit ;
    -- 回滚事务
    rollback ;
Spring 中事务的实现

前⾯课程我们讲了MySQL的事务操作, Spring对事务也进⾏了实现.
Spring 中的事务操作分为两类:

  1. 编程式事务(⼿动写代码操作事务).
  2. 声明式事务(利⽤注解⾃动开启和提交事务)
Spring 编程式事务

Spring ⼿动操作事务和上⾯ MySQL 操作事务类似, 有 3 个重要操作步骤:
• 开启事务(获取事务)
• 提交事务
• 回滚事务
SpringBoot 内置了两个对象:

  1. DataSourceTransactionManager 事务管理器. ⽤来获取事务(开启事务), 提交或回滚事务的
  2. TransactionDefinition 是事务的属性, 在获取事务的时候需要将 TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus

    提交事务,以及事务回滚同时进行则代码会报错

    事务已经被完成,不能提交或者回滚

    这是开启事务的信息,committing证明事务提交成功
    如果只有事务回滚的代码程序结果又是怎么样的?接下来我们看一看


    与上述提交事务的代码相比少了一行committing的日志,所以并没有提交事务,而是进行了回滚操作,接下来我们来看一下数据库

    发现并没有添加新的信息
    以上代码虽然可以实现事务, 但操作也很繁琐, 有没有更简单的实现⽅法呢?
    接下来我们学习声明式事务
Spring 声明式事务 @Transactional

声明式事务的实现很简单
两步操作:

  1. 添加依赖
    < dependency >
    < groupId >org.springframework</ groupId >
    < artifactId >spring-tx</ artifactId >
    </ dependency >
  2. 在需要事务的⽅法上添加 @Transactional 注解就可以实现了. ⽆需⼿动开启事务和提交事
    务, 进⼊⽅法时⾃动开启事务, ⽅法执⾏完会⾃动提交事务, 如果中途发⽣了没有处理的异常会⾃动
    回滚事务.


    上述代码中存在错误,程序会自动进行回滚,观察日志虽然用户插入数据成功,但是事务进行了回滚
@Transactional 作⽤

@Transactional 可以⽤来修饰⽅法或类:
• 修饰⽅法时: 只有修饰public ⽅法时才⽣效(修饰其他⽅法时不会报错, 也不⽣效)
• 修饰类时: 对 @Transactional 修饰的类中所有的 public ⽅法都⽣效.
⽅法/类被 @Transactional 注解修饰时, 在⽬标⽅法执⾏开始之前, 会⾃动开启事务, ⽅法执⾏结束
之后, ⾃动提交事务.
如果在⽅法执⾏过程中, 出现异常, 且异常未被捕获, 就进⾏事务回滚操作.
如果异常被程序捕获, ⽅法就被认为是成功执⾏, 依然会提交事务

我们对上述代码进行异常捕获

我们发现,对异常进行捕获之后,事务就会提交成功,并且数据插入成功, 运⾏程序, 发现虽然程序出错了, 但是由于异常被捕获了, 所以事务依然得到了提交.
如果需要事务进⾏回滚, 有以下两种⽅式:

  1. 重新抛出异常
  2. ⼿动回滚事务
    使⽤ TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并使⽤ setRollbackOnly 设置 setRollbackOnly
@Transactional 详解

通过上⾯的代码, 我们学习了 @Transactional 的基本使⽤. 接下来我们学习 @Transactional
注解的使⽤细节.
我们主要学习 @Transactional 注解当中的三个常⻅属性:

  1. rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
  2. Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
  3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED
rollbackFor

@Transactional 默认只在遇到运⾏时异常和Error时才会回滚, ⾮运⾏时异常不回滚. 即 Exception的⼦类中, 除了RuntimeException及其⼦类.

虽然程序抛出了异常IOException,但是这个异常不是error或者RunTimeException,所以事务不会进行回滚,还是会提交成功
如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通
过 rollbackFor 这个属性指定出现何种异常类型时事务进⾏回滚

• 在Spring的事务管理中,默认只在遇到运⾏时异常RuntimeException和Error时才会回滚.
• 如果需要回滚指定类型的异常, 可以通过rollbackFor属性来指定

事务隔离级别
3.2.1 MySQL 事务隔离级别

SQL 标准定义了四种隔离级别, MySQL 全都⽀持. 这四种隔离级别分别是:

1. 读未提交(READ UNCOMMITTED):

读未提交, 也叫未提交读. 该隔离级别的事务可以看到其他事务中未提交的数据
因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数据称之为脏数据, 这个问题称之为脏读.

2. 读提交(READ COMMITTED):

读已提交, 也叫提交读. 该隔离级别的事务能读取到已经提交事务的数据
该隔离级别不会有脏读的问题.但由于在事务的执⾏中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读

3. 可重复读(REPEATABLE READ):

事务不会读到其他事务对已有数据的修改, 即使其他事务已提交. 也就可以确保同⼀事务多次查询的结果⼀致, 但是其他事务新插⼊的数据, 是可以感知到的. 这也就引发了幻读问题. 可重复读, 是 MySQL 的默认事务隔离级别.
⽐如此级别的事务正在执⾏时, 另⼀个事务成功的插⼊了某条数据, 但因为它每次查询的结果都是
⼀样的, 所以会导致查询不到这条数据, ⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因). 明明在事务
中查询不到这条信息,但⾃⼰就是插⼊不进去, 这个现象叫幻读.

4. 串⾏化(SERIALIZABLE):

序列化, 事务最⾼隔离级别. 它会强制事务排序, 使之不会发⽣冲突, 从⽽解 决了脏读, 不可重复读和幻读问题, 但因为执⾏效率低, 所以真正使⽤的场景并不多.

Spring 事务隔离级别

Spring 中事务隔离级别有5 种:

  1. Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.
  2. Isolation.READ_UNCOMMITTED : 读未提交, 对应SQL标准中 READ UNCOMMITTED
  3. Isolation.READ_COMMITTED : 读已提交,对应SQL标准中 READ COMMITTED
  4. Isolation.REPEATABLE_READ : 可重复读, 对应SQL标准中 REPEATABLE READ
  5. Isolation.SERIALIZABLE : 串⾏化, 对应SQL标准中 SERIALIZABLE
    Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置
Spring 事务传播机制
什么是事务传播机制

事务传播机制就是: 多个事务⽅法存在调⽤关系时, 事务是如何在这些⽅法间进⾏传播的

⽐如有两个⽅法A, B都被 @Transactional 修饰, A⽅法调⽤B⽅法

A⽅法运⾏时, 会开启⼀个事务. 当A调⽤B时, B⽅法本⾝也有事务, 此时B⽅法运⾏时, 是加⼊A的事务, 还是创建⼀个新的事务呢?
这个就涉及到了事务的传播机制
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题

⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题

事务的传播机制有哪些

@Transactional 注解⽀持事务传播机制的设置, 通过 propagation 属性来指定传播⾏为.
Spring 事务传播机制有以下 7 种:

  1. Propagation.REQUIRED : 默认的事务传播级别. 如果当前存在事务, 则加⼊该事务. 如果当前没
    有事务, 则创建⼀个新的事务.
  2. Propagation.SUPPORTS : 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的
    ⽅式继续运⾏.
  3. Propagation.MANDATORY :强制性. 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则
    抛出异常.
  4. Propagation.REQUIRES_NEW : 创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起. 也
    就是说不管外部⽅法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开
    启⾃⼰的事务, 且开启的事务相互独⽴, 互不⼲扰.
  5. Propagation.NOT_SUPPORTED : 以⾮事务⽅式运⾏, 如果当前存在事务, 则把当前事务挂起(不
    ⽤).
  6. Propagation.NEVER : 以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常.
  7. Propagation.NESTED : 如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏.
    如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED
    举个例子来理解一下事务的传播机制
    ⽐如⼀对新⼈要结婚了, 关于是否需要房⼦
  8. Propagation.REQUIRED : 需要有房⼦. 如果你有房, 我们就⼀起住, 如果你没房, 我们就⼀起
    买房. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务)
  9. Propagation.SUPPORTS : 可以有房⼦. 如果你有房, 那就⼀起住. 如果没房, 那就租房. (如果
    当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的⽅式继续运⾏)
  10. Propagation.MANDATORY : 必须有房⼦. 要求必须有房, 如果没房就不结婚. (如果当前存在事
    务, 则加⼊该事务. 如果当前没有事务, 则抛出异常)
  11. Propagation.REQUIRES_NEW : 必须买新房. 不管你有没有房, 必须要两个⼈⼀起买房. 即使
    有房也不住. (创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起)
  12. Propagation.NOT_SUPPORTED : 不需要房. 不管你有没有房, 我都不住, 必须租房.(以⾮事务
    ⽅式运⾏, 如果当前存在事务, 则把当前事务挂起)
  13. Propagation.NEVER : 不能有房⼦. (以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常)
  14. Propagation.NESTED : 如果你没房, 就⼀起买房. 如果你有房, 我们就以房⼦为根据地,做点下
    ⽣意. (如果如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏. 如果当前没有事
    务, 则该取值等价于 PROPAGATION_REQUIRED )
Spring 事务传播机制使⽤和各种场景演⽰

对于以上事务传播机制,我们重点关注以下两个就可以了:

  1. REQUIRED(默认值)
  2. REQUIRES_NEW
REQUIRED(加⼊事务)
  1. ⽤⼾注册, 插⼊⼀条数据

  2. 记录操作⽇志, 插⼊⼀条数据(出现异常)


运⾏程序, 发现数据库没有插⼊任何数据.
流程描述:

  1. r3 ⽅法开始事务
  2. ⽤⼾注册, 插⼊⼀条数据 (执⾏成功) (和r3 使⽤同⼀个事务)
  3. 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (和r3 使⽤同⼀个事务)
  4. 因为步骤3出现异常, 事务回滚. 步骤2和3使⽤同⼀个事务, 所以步骤2的数据也回滚了.
REQUIRES_NEW(新建事务)

将上述UserService 和LogService 中相关⽅法事务传播机制改为 Propagation.REQUIRES_NEW

运⾏程序, 发现⽤⼾数据插⼊成功了, ⽇志表数据插⼊失败. LogService ⽅法中的事务不影响 UserService 中的事务.

当我们不希望事务之间相互影响时, 可以使⽤该传播⾏为

NEVER (不⽀持当前事务, 抛异常)

修改UserService 中对应⽅法的事务传播机制为 Propagation. NEVER,


如果当前方法存在事务,则设置NEVER会抛出异常,如果当前方法不存在事务,调用这个不会有影响

NESTED(嵌套事务)

将上述UserService 和LogService 中相关⽅法事务传播机制改为 Propagation.NESTED

运⾏程序, 发现没有任何数据插⼊.
流程描述:

  1. Controller 中p1 ⽅法开始事务
  2. UserService ⽤⼾注册, 插⼊⼀条数据 (嵌套p1事务)
  3. LogService 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (嵌套p1事务, 回滚当前事务, 数
    据添加失败)
  4. 由于是嵌套事务, LogService 出现异常之后, 往上找调⽤它的⽅法和事务, 所以⽤⼾注册也失败
    了.
  5. 最终结果是两个数据都没有添加
    p1事务可以认为是⽗事务, 嵌套事务是⼦事务. ⽗事务出现异常, ⼦事务也会回滚, ⼦事务出现异常, 如果不进⾏处理, 也会导致⽗事务回滚.

    重新运⾏程序, 发现⽤⼾表数据添加成功, ⽇志表添加失败.
    LogService 中的事务已经回滚, 但是嵌套事务不会回滚嵌套之前的事务, 也就是说嵌套事务可以实
    现部分事务回滚
    对⽐REQUIRED
    把 NESTED 传播机制改为 REQUIRED,


    重新运⾏程序, 发现⽤⼾表和⽇志表的数据添加都失败了.
    REQUIRED 如果回滚就是回滚所有事务, 不能实现部分事务的回滚. (因为属于同⼀个事务)
NESTED和REQUIRED区别

• 整个事务如果全部执⾏成功, ⼆者的结果是⼀样的.
• 如果事务⼀部分执⾏成功, REQUIRED加⼊事务会导致整个事务全部回滚. NESTED嵌套事务可以实现局部回滚, 不会影响上⼀个⽅法中执⾏的结果.

总结
  1. Spring中使⽤事务, 有两种⽅式: 编程式事务(⼿动操作)和声明式事务. 其中声明式事务使⽤较多,在⽅法上添加 @Transactional 就可以实现了
  2. 通过 @Transactional(isolation = Isolation.SERIALIZABLE) 设置事务的隔离级别. Spring 中的事务隔离级别有 5 种
  3. 通过 @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播机制, Spring 中的事务传播级别有 7 种, 重点关注 REQUIRED (默认值) 和 REQUIRES_NEW
相关推荐
远歌已逝2 小时前
维护在线重做日志(二)
数据库·oracle
qq_433099403 小时前
Ubuntu20.04从零安装IsaacSim/IsaacLab
数据库
Dlwyz3 小时前
redis-击穿、穿透、雪崩
数据库·redis·缓存
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
工业甲酰苯胺5 小时前
Redis性能优化的18招
数据库·redis·性能优化
冰帝海岸5 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象5 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了6 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·6 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端