Spring 事务详解
前段时间答应读者的 Spring 事务 分析总结终于来了。这部分内容比较重要,不论是对于工作还是面试,但是网上比较好的参考资料比较少。 [什么是事务?](什么是事务) 事务是逻辑上的一组操作,要
前段时间答应读者的 Spring 事务 分析总结终于来了。这部分内容比较重要,不论是对于工作还是面试,但是网上比较好的参考资料比较少。 什么是事务? 事务是逻辑上的一组操作,要么都执行,要么都不执行。 相信大家应该都能背上面这句话了,下面我结合我们日常的真实开发来谈一谈。 我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的 方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。 另外,需要格外注意的是: 事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的 引擎。但是,如果把数据库引擎变为 ,那么程序也就不再支持事务了! 事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是: 1. 将小明的余额减少 1000 元。 2. 将小红的余额增加 1000 元。 万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。 另外,数据库事务的 ACID 四大特性是事务的基础,下面简单来了解一下。 事务的特性(ACID)了解么? 1. 原子性 ( ):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; 2. 一致性 ( ):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; 3. 隔离性 ( ):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; 4. 持久性 ( ):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 🌈 这里要额外补充一点: 只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的! 想必大家也和我一样,被 ACID 这个概念被误导了很久! 我也是看周志明老师的公开课《周志明的软件架构课》才搞清楚的(多看好书!!!)。 另外,DDIA 也就是 《Designing Data Intensive Application(数据密集型应用系统设计)》 的作者在他的这本书中如是说: Atomicity, isolation, and durability are properties of the database, whereas consis‐ tency (in the ACID sense) is a property of the application. The application may rely on the database’s atomicity and isolation properties in order to achieve consistency, but it’s not up to the database alone. 翻译过来的意思是:原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。 《Designing Data Intensive Application(数据密集型应用系统设计)》这本书强推一波,值得读很多遍!豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外,中文翻译版本已经在 GitHub 开源,地址:<https://github.com/Vonng/ddia 。 详谈 Spring 对事务的支持 ⚠️ 再提醒一次:你的程序是否支持事务首先取决于数据库 ,比如使用 MySQL 的话,如果你选择的是 innodb 引擎,那么恭喜你,是可以支持事务的。但是,如果你的 MySQL 数据库使用的是 myisam 引擎的话,那不好意思,从根上就是不支持事务的。 这里再多提一下一个非常重要的知识点: MySQL 怎么保证原子性的? 我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行 回滚 ,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚之前未完成的事务。 Spring 支持两种方式的事务管理 编程式事务管理 通过 或者 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。 使用 进行编程式事务管理的示例代码如下: 使用 进行编程式事务管理的示例代码如下: 声明式事务管理 推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于 的全注解方式使用最多)。 使用 注解进行事务管理的示例代码如下: Spring 事务管理接口介绍 Spring 框架中,事务管理相关最重要的 3 个接口如下: :(平台)事务管理器,Spring 事务策略的核心。 :事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。 :事务运行状态。 我们可以把 接口可以被看作是事务上层的管理者,而 和 这两个接口可以看作是事务的描述。 会根据 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。 PlatformTransactionManager:事务管理接口 Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是: 。 通过这个接口,Spring 为各个平台如:JDBC( )、Hibernate( )、JPA( )等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。 接口的具体实现如下: 接口中定义了三个方法: 这里多插一嘴。为什么要定义或者说抽象出来 这个接口呢? 主要是因为要将事务管理行为抽象出来,然后不同的平台去实现它,这样我们可以保证提供给外部的行为不变,方便我们扩展。 我前段时间在我的知识星球分享过: “为什么我们要用接口?” 。 《设计模式》(GOF 那本)这本书在很多年前都提到过说要基于接口而非实现编程,你真的知道为什么要基于接口编程么? 纵观开源框架和项目的源码,接口是它们不可或缺的重要组成部分。要理解为什么要用接口,首先要搞懂接口提供了什么功能。我们可以把接口理解为提供了一系列功能列表的约定,接口本身不提供功能,它只定义行为。但是谁要用,就要先实现我,遵守我的约定,然后再自己去实现我定义的要实现的功能。 举个例子,我上个项目有发送短信的需求,为此,我们定了一个接口,接口只有两个方法: 1.发送短信 2.处理发送结果的方法。 刚开始我们用的是阿里云短信服务,然后我们实现这个接口完成了一个阿里云短信的服务。后来,我们突然又换到了别的短信服务平台,我们这个时候只需要再实现这个接口即可。这样保证了我们提供给外部的行为不变。几乎不需要改变什么代码,我们就轻松完成了需求的转变,提高了代码的灵活性和可扩展性。 什么时候用接口?当你要实现的功能模块设计抽象行为的时候,比如发送短信的服务,图床的存储服务等等。 TransactionDefinition:事务属性 事务管理器接口 通过 方法来得到一个事务,这个方法里面的参数是 类 ,这个类就定义了一些基本的事务属性。 什么是事务属性呢? 事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。 事务属性包含了 5 个方面: 隔离级别 传播行为 回滚规则 是否只读 事务超时 接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。 TransactionStatus:事务状态 接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。 方法返回一个 对象。 TransactionStatus 接口内容如下: 事务属性详解 实际业务开发中,大家一般都是使用 注解来开启事务,很多人并不清楚这个注解里面的参数是什么意思,有什么用。为了更好的在项目中使用事务管理,强烈推荐好好阅读一下下面的内容。 事务传播行为 事务传播行为是为了解决业务层方法之间互相调用的事务问题 。 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。 举个例子:我们在 A 类的 方法中调用了 B 类的 方法。这个时候就涉及到业务层方法之间互相调用的事务问题。如果我们的 如果发生异常需要回滚,如何配置事务传播行为才能让 也跟着回滚呢?这个时候就需要事务传播行为的知识了,如果你不知道的话一定要好好看一下。 在 定义中包括了如下几个表示传播行为的常量: 不过,为了方便使用,Spring 相应地定义了一个枚举类: 正确的事务传播行为可能的值如下 : 1. 使用的最多的一个事务传播行为,我们平时经常使用的 注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说: 如果外部方法没有开启事务的话, 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 如果外部方法开启事务并且被 的话,所有 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。 举个例子:如果我们上面的 和 使用的都是 传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。 创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务, 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 举个例子:如果我们上面的 使用 事务传播行为修饰, 还是用 修饰的话。如果 发生异常回滚, 不会跟着回滚,因为 开启了独立的事务。但是,如果 抛出了未被捕获的异常并且这个异常满足事务回滚规则的话, 同样也会回滚,因为这个异常被 的事务管理机制检测到了。 3. : 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务执行; 如果当前没有事务,就执行与 类似的操作。也就是说: 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。 如果外部方法无事务,则单独开启一个事务,与 类似。 代表的嵌套事务以父子关系呈现,其核心理念是子事务不会独立提交,依赖于父事务,在父事务中运行;当父事务提交时,子事务也会随着提交,理所当然的,当父事务回滚时,子事务也会回滚; 与 区别于: 是独立事务,不依赖于外部事务,以平级关系呈现,执行完就会立即提交,与外部事务无关; 子事务也有自己的特性,可以独立进行回滚,不会引发父事务的回滚,但是前提是需要处理子事务的异常,避免异常被父事务感知导致外部事务回滚; 举个例子: 如果 回滚的话,作为嵌套事务的 会回滚。 如果 回滚的话, 是否回滚,要看 的异常是否被处理: 的异常没有被处理,即 内部没有处理异常,且 也没有处理异常,那么 将感知异常致使整体回滚。 处理异常或 处理异常, 不会回滚。 4. 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) 这个使用的很少,就不举例子来说了。 若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚,这里不对照案例讲解了,使用的很少。 : 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 : 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 : 以非事务方式运行,如果当前存在事务,则抛出异常。 更多关于事务传播行为的内容请看这篇文章:《太难了 面试官让我结合案例讲讲自己对 Spring 事务传播行为的理解。》 事务隔离级别 接口中定义了五个表示隔离级别的常量: 和事务传播行为那块一样,为了方便使用,Spring 也相应地定义了一个枚举类: 下面我依次对每一种事务隔离级别进行介绍: :使用后端数据库默认的隔离级别,MySQL 默认采用的 隔离级别 Oracle 默认采用的 隔离级别. :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更, 可能会导致脏读、幻读或不可重复读 : 允许读取并发事务已经提交的数据, 可以阻止脏读,但是幻读或不可重复读仍有可能发生 : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改, 可以阻止脏读和不可重复读,但幻读仍有可能发生。 : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说, 该级别可以防止脏读、不可重复读以及幻读 。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 相关阅读:MySQL 事务隔离级别详解。 事务超时属性 所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 中以 int 的值来表示超时时间,其单位是秒,默认值为 1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。 事务只读属性 对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。 很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢? 拿 MySQL 的 innodb 举例子,根据官网 <https://dev.mysql.com/doc/refman/5.7/en/innodb autocommit commit rollback.html 描述: MySQL 默认对每一个新建立的连接都启用了 模式。在该模式下,每一个发送到 MySQL 服务器的 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。 但是,如果你给方法加上了 注解的话,这个方法执行的所有 会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。 如果不加 ,每条 会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。 分享一下关于事务只读属性,其他人的解答: 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性; 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持 事务回滚规则 这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常( 的子类)时才会回滚, 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。 如果你想要回滚你定义的特定的异常类型的话,可以这样: @Transactional 注解使用详解 的作用范围 1. 方法 :推荐将注解使用于方法上,不过需要注意的是: 该注解只能应用到 public 方法上,否则不生效。 2. 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。 3. 接口 :不推荐在接口上使用。 的常用配置参数 注解源码如下,里面包含了基本事务属性的配置: 的常用配置参数总结(只列出了 5 个我平时比较常用的): | 属性名 | 说明 | | | | | propagation | 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过 | | isolation | 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过 | | timeout | 事务的超时时间,默认值为 1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 | | readOnly | 指定事务是否为只读事务,默认值为 false。 | | rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 | 事务注解原理 面试中在问 AOP 的时候可能会被问到的一个问题。简单说下吧! 我们知道, 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。 🤐 多提一嘴: 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下: 如果一个类或者一个类中的 public 方法上被标注 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 注解的 public 方法的时候,实际调用的是, 类中的 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。 类中的 方法内部实际调用的是 类的 方法。由于新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。 Spring AOP 自调用问题 当一个方法被标记了 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。 这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。 类中的 调用 就会导致 的事务失效。 解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。 issue 2091补充了一个例子: 上面的代码确实可以在自调用的时候开启事务,但是这是因为使用了 方法来获取当前类的代理对象,然后通过代理对象调用 。这样就相当于从外部调用了 ,所以事务注解才会生效。我们一般也不会在代码中这么写,所以可以忽略这个特殊的例子。 的使用注意事项总结 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用; 避免同一个类中调用 注解的方法,这样会导致事务失效; 正确的设置 的 和 属性,否则事务可能会回滚失败; 被 注解的方法所在的类必须被 Spring 管理,否则不生效; 底层使用的数据库必须支持事务机制,否则不生效; …… 参考 [总结]Spring 事务管理中@Transactional 的参数:http://www.mobabel.net/spring 事务管理中 transactional 的参数/ Spring 官方文档:<https://docs.spring.io/spring/docs/4.2.x/spring framework reference/html/transaction.html 《Spring5 高级编程》 透彻的掌握 Spring 中@transactional 的使用: <https://www.ibm.com/developerworks/cn/java/j master spring transactional use/index.html Spring 事务的传播特性:https://github.com/love somnus/Spring/wiki/Spring 事务的传播特性 Spring 事务传播行为详解:<https://segmentfault.com/a/1190000013341344 全面分析 Spring 的编程式事务管理及声明式事务管理:<https://www.ibm.com/developerworks/cn/education/opensource/os cn spring trans/index.html 写在最后 如果内容对你有帮助的话,欢迎顺手给 JavaGuide 点一个免费的 Star 支持一下:GitHub | Gitee。 JavaGuide 已持续维护近七年,累计 6100+ 次提交,来自 620+ 位贡献者共同完善。你的 Star、反馈和 PR,都是这个项目继续更新的动力。 如果你正在准备后端/AI 应用开发面试,也可以了解一下我的知识星球,里面包括后端和 AI 实战项目、简历优化、一对一提问和高频考点资料,已经持续维护六年。