京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。

本文原文链接

45岁老架构 尼恩说在前面

在45岁老架构师 尼恩的读者交流群(100+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的相关面试题:

  • 什么是Spring事务?
  • Spring事务失效的10 种常见场景?
  • Spring加入型事务和嵌套型事务有什么区别?
  • spring事务隔离级别与数据库事务隔离级别的关系?

最近有小伙伴面试美团、JD,都问到了这个面试题。 小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取

基础知识:Spring 两种事务管理方式

Spring 支持两种事务管理方式:编程式事务和声明式事务。

image.png

事务分为 编程式事务 和声明式事务两种。

  • 编程式事务指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。
  • 声明式事务是通过配置来实现的,不需要在代码中显式地管理事务。

编程式事务是指在代码中显式地开启、提交或回滚事务。这种方式需要在代码中编写事务管理的相关逻辑,比较繁琐,但是灵活性较高,可以根据具体的业务需要进行定制。

关于 编程式事务 是如何实现,请参见尼恩架构团队的 顶奢好文:

顶奢好文:3W字,穿透Spring事务原理、源码,最少读10遍

声明式事务是通过配置来实现的,不需要在代码中显式地管理事务。这种方式需要在配置文件中声明事务的属性,比如事务的传播行为、隔离级别等。声明式事务的好处是可以将事务管理的逻辑与业务逻辑分离,使得代码更加简洁、清晰,同时也方便了事务管理的统一配置和维护。

在 Spring 中,声明式事务 是基于 AOP 面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,声明式事务也有两种实现方式。

关于 声明式事务 是如何基于 AOP 面向切面实现,请参见尼恩架构团队的 顶奢好文:

顶奢好文:3W字,穿透Spring事务原理、源码,最少读10遍

Spring 提供了两种声明式事务的方式:

  • 基于 XML 配置
  • 基于注解配置。

基于 XML 配置的方式需要在 Spring 配置文件中声明事务管理器和事务通知等相关信息,

而基于注解配置的方式则可以在代码中通过注解来声明事务的属性,比如 @Transactional。一种是基于 TX 和 AOP 的 xml 配置文件方式,二种就是基于 @Transactional 注解了,实际开发中 @Transactional 用的比较多。

声明式事务1:基于 XML 配置文件进行配置

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
    <!-- 开启扫描 -->
    <context:component-scan base-package="com.dpb.*"></context:component-scan>

    <!-- 配置数据源 -->
    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl"/>
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="username" value="pms"/>
        <property name="password" value="pms"/>
    </bean>

    <!-- 配置JdbcTemplate -->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" >
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 
    Spring中,使用XML配置事务三大步骤:  
        1. 创建事务管理器  
        2. 配置事务方法  
        3. 配置AOP
     -->
     <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
         <property name="dataSource" ref="dataSource"/>
     </bean>
     <tx:advice id="advice" transaction-manager="transactionManager">
         <tx:attributes>
             <tx:method name="fun*" propagation="REQUIRED"/>
         </tx:attributes>
     </tx:advice>
     <!-- aop配置 -->
     <aop:config>
         <aop:pointcut expression="execution(* *..service.*.*(..))" id="tx"/>
          <aop:advisor advice-ref="advice" pointcut-ref="tx"/>
     </aop:config>
</beans>

声明式事务2:基于注解的声明式配置

一般来说,更加推荐声明式事务比编程式事务,因为它可以使代码更加简洁、清晰,同时也方便了事务管理的统一配置和维护。

所以,这里使用 声明式事务 进行演示,并且是使用 基于注解配置的 声明式事务。

首先必须要添加 @EnableTransactionManagement 注解,保证事务注解生效

@EnableTransactionManagement
public class AnnotationMain {
   
    public static void main(String[] args) {
   
    }
}

其次,在方法上添加 @Transactional 代表注解生效

@Transactional
public int insertUser(User user) {
   
    userDao.insertUser();
    userDao.insertLog();
    return 1;
}

下面的案例,用到基于注解的声明式配置,具体的注解是 @Transactional。

@Transactional 注解的使用

@Transactional 可以作用在类上,当作用在类上的时候,表示所有该类的 public 方法都配置相同的事务属性信息。

@Transactional 也可以作用在方法上,当方法上也配置了 @Transactional,方法的事务会覆盖类的事务配置信息。

我们日常操作里,对于单个方法使用事物,经常是这样:

  @Transactional(rollbackFor = Exception.class)
    public Boolean add(UserInfo userInfo) {

        //... 业务处理
        //... 业务处理
        //... 业务处理

       //手动抛异常 触发回滚等
        retrun xxx;
  }

或者说配合手动回滚使用,是这样:

 @Transactional(rollbackFor = Exception.class)
    public Boolean add(UserInfo userInfo) {
   

       try {
   

          //....业务逻辑处理

          if(XXXX){
   
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                return false;
          }
          //....业务逻辑处理

          if(xxxxx){
   
                 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                 return false;
          }


        } catch (Exception e) {
   
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return false;
        }
    }

以上都是单个事物方法,理解起来很简单,相信大多数场景大家就这么用一下就没有过多去理会了。

首先,我们通过看 @Transactional 的源码来和大家重新认识一下 @Transactional 的用法。

@Transactional 注解 涉及到的 5大属性

总体来说,事务属性包含了5个方面,如图所示:

image.png

@Transactional 源码

image.png

transactionManager 和 value 是同一个配置项的两个别名:

大多数项目只需要一个事务管理器,但是在有些项目中为了提高效率、或者有多个完全不同又不相干的数据源,所以会有多个事务管理器,这里填的就是你想用的事务管理器的 Bean 的 id。

propagation属性: Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。

这个后面详细介绍。

isolation属性: 是事务的隔离级别,默认值为 Isolation.DEFAULT。

这里有四个隔离级别,具体这四个级别是什么意思 :

  • Isolation.DEFAULT:使用底层数据库默认的隔离级别。

  • Isolation.READ_UNCOMMITTED

  • Isolation.READ_COMMITTED

  • Isolation.REPEATABLE_READ

  • Isolation.SERIALIZABLE

在Innodb里面默认用的是 RR 级别,

timeout属性: 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

readOnly属性 : 指定事务是否为只读事务,默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

rollbackFor属性 : 用于指定能够触发事务回滚的 异常 类型,可以指定多个异常类型。

第一大属性:@Transactional 注解 的 传播机制

什么叫做事务的传播?

Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。

尼恩给大家举个 生动的例子.

比如,有三个 业务 方法,第一个业务 方法如下:

class testOne  
{
   
    @Transactional(rollbackFor = Exception.class)
    public Boolean addOne(UserInfo userInfo) {
   

        //... 业务处理
        //... 业务处理
        //... 业务处理
        retrun xxx;
    }

}

第二个业务 方法如下:

class testTwo  
{
   
 @Transactional(rollbackFor = Exception.class)
    public Boolean addTwo(UserInfo userInfo) {
   

        //... 业务处理
        //... 业务处理
        //... 业务处理
        retrun xxx;
    }

}

然后第三个 业务 方法如下:

class testThree  
{
   

    @Transactional(rollbackFor = Exception.class)
    public Boolean testThree(UserInfo userInfo) {
   

        addOne(xxxx);
        addTwo(xxxx);
        retrun xxx;
    }

}

那么, 三个 业务 方法 之间:

  • 是每一个 业务方法开启一个 新的独立的事务?
  • 还是 第一个 业务 方法、 第二个 业务 方法 加入到 第三个 业务 方法 开启的事务?
  • 还是 第一个 业务 方法、 第二个 业务 方法 各自开一个 NESTED 内嵌事务, 以局部事务的 加入到 第三个 业务 方法 开启的整体事务?

Spring定义了七种传播行为

使用spring声明式事务,自动在方法调用之前 (进入一个新的方法),spring会根据事务属性去决定是否开一个事务,并在方法执行之后,决定事务提交或回滚事务。这就是事务的传播。

Spring定义了七种传播行为:

传播行为 含义
PROPAGATION_REQUIRED 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务
PROPAGATION_SUPPORTS 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行
PROPAGATION_MANDATORY 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
PROPAGATION_REQUIRED_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATION_NESTED 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务

事务7种传播机制 对应的源码如下:

image.png

image.png

Spring 事务传播机制分为 3 大类,总共 7 种级别,如下图所示:

img

当我们不指定的时候, 默认使用的是 Propagation.REQUIRED。

1.1 支持当前事务 的三种传播方式

支持当前事务的传播机制有三种,分别是

  • 第一种传播: 加入当前事务 REQUIRED

所谓的加入当前事务,是指如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

所谓 当前事务 ,其实是用词 稍有有点错误, 其实 指的是 上一层方法的事务 。

含义:如果上一层方法 已经存在一个事务中,则加入到这个事务中; 如果上一层 方法没有事务,当前层方法 新建一个事务 。

REQUIRED 加入当前事务 , 这是 默认的 传播机制。

  • 第二种 传播: 支持当前事务 SUPPORTS

    支持一下当前 事务,是指如果当前存在事务,则加入该事务;如果当前没有事务, 就以非事务方式执行

所谓 当前事务 ,其实是用词 稍有有点错误, 其实 指的是 上一层方法的事务 。

含义:支持上一层 方法的 事务,如果上一层 方法没有事务, 那么,当前层方法 就以非事务方式执行

  • 第三种 传播: MANDATORY 强制当前事务

强制一下当前 事务,是指如果当前存在事务,则加入该事务;如果当前没有事务, 就抛出 异常 。

含义:如果 上一层 方法 没事务,那么,当前层方法 就抛出 异常 。

1.2 不支持当前事务的三种传播方式

  • 第4种 传播: REQUIRES_NEW

含义:新建事务,如果当前存在事务,把当前事务挂起。

  • 第5种 传播: NOT_SUPPORTED

含义:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

  • 第6种 传播: NEVER

含义: 以非事务方式执行,如果当前存在事务,则抛出异常。

1.3 NESTED 事务嵌套

  • 第7种 传播: NESTED 事务嵌套

含义: 如果当前存在事务,则在嵌套事务内执行。

如果当前没有事务,则执行与 REQUIRED类似的操作, 创建一个新的事务。

NESTED事务嵌套和 加入事务(REQUIRED)的主要区别在于 :

NESTED事务 的特点如下 :

  • 当存在外部事务时,NESTED会创建一个嵌套的子事务,这个子事务有自己的保存点(savepoint)。

  • 如果嵌套事务中发生异常,它只会回滚到自己的保存点(savepoint),而不影响外部事务。

  • 因此, NESTED事务可以实现部分事务的回滚,或者说 子事务部分回滚( 只有嵌套事务内的部分操作会被回滚),而外部事务的其他部分可以继续执行。

    加入事务(REQUIRED)的特点如下 :

  • 如果当前存在事务,则REQUIRED会加入到当前事务中,作为当前事务的一部分;

  • 如果当前没有事务,则创建一个新的事务。
  • 在REQUIRED传播级别下,如果遇到异常,整个事务(外部事务,包括嵌套之前的所有操作)将会回滚。

总结来说:

  • NESTED事务允许在当前事务中创建一个新的子事务,这个子事务可以独立于外部事务进行回滚
  • 而REQUIRED事务则会与外部事务一形成一个整体,同生共死,一起回滚。
  • NESTED事务通过保存点(savepoint)实现部分回滚,而REQUIRED事务则是整个事务的回滚。

默认的传播行为:加入当前事务 REQUIRED

除了Propagation.REQUIRED, 另外两个常用的是 Propagation.REQUIRES_NEW 和 Propagation.NESTED。

除了这个三个, 而另外四种我们基本是不会去使用的,所以小伙伴也没必要去了解。

看代码,默认啥都不指定的时候,我们使用的就是PROPAGATION_REQUIRED这种方式。

那么接下来就是关于 这种默认的事物传播机制 PROPAGATION_REQUIRED 我们需要关心的东西了。

前面介绍了 加入当前事务 REQUIRED 的传播行为:

  • 是指如果当前存在事务,则加入该事务
  • 如果当前没有事务,则创建一个新的事务。

假设,第一个业务类里面的方法 使用了 声明式事务 :

class testOne  
{
   
    @Transactional(rollbackFor = Exception.class)
    public Boolean addOne(UserInfo userInfo) {
   

        //... 业务处理
        //... 业务处理
        //... 业务处理
        retrun xxx;
    }

}

假设, 第二个业务类里面的方法,也使用了声明式事务:

class testTwo  
{
   
    @Transactional(rollbackFor = Exception.class)
    public Boolean addTwo(UserInfo userInfo) {
   

        //... 业务处理
        //... 业务处理
        //... 业务处理
        retrun xxx;
    }

}

然后第三个业务类里面的方法没有使用声明式事务,去调用第一个和第二个,如:

class testThree  
{
   
    @Transactional(rollbackFor = Exception.class)
    public Boolean testThree(UserInfo userInfo) {
   

        addOne(xxxx);
        addTwo(xxxx);
        retrun xxx;
    }

}

在testThree方法(对于addOne 和 addTwo 来说是个外部方法)上同样使用声明式事物,且也是默认指定传播机制PROPAGATION_REQUIRED。

默认指定传播机制PROPAGATION_REQUIRED , testThree 让testOne,testTwo 都加入到一个事务里面。

这样addOne事物开启时,发现外部存在指定传播机制PROPAGATION_REQUIRED的事物,那么就会加入该事物;

同样addTwo同理。

第二大属性:@Transactional 注解的 隔离属性

数据库有自己的隔离级别的定义,Spring也有自己的 隔离级别的定义

Spring中的隔离级别

Spring事务由 Transactional 注解实现,隔离级别由它的参数 isolation 控制,Isolation 的 Eum 类中定义了“五个”表示隔离级别的值,如下。

隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

Spring中的隔离级别 和数据一致性问题的 关系:

Isolation的值与隔离级别 隔离级别的值 脏读 不可重复读 幻读
Isolation.DEFAULT 0 - - -
Isolation.READ_UNCOMMITTED 1
Isolation.READ_COMMITTED 2 ×
Isolation.REPEATABLE_READ 4 × ×
Isolation.SERIALIZABLE 8 × × ×

数据库隔离级别

隔离级别 隔离级别的值 导致的问题
Read-Uncommitted 0 导致脏读
Read-Committed 1 避免脏读,允许不可重复读和幻读
Repeatable-Read 2 避免脏读,不可重复读,允许幻读
Serializable 3 串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重

MySQL 默认为 RR :PEATABLE_READ;

Oracle,sql server 默认为 RC:READ_COMMITTED;

READ_UNCOMMITTED 由于隔离级别较低,通常不会被使用。

数据库隔离级别 和数据一致性问题 的 关系:

隔离级别 隔离级别的值 脏读 不可重复读 幻读
Read uncommitted(未提交读) 0
Read committed(已提交读) 1 ×
Repeatable read(可重复读) 2 × ×
Serializable(可串行化) 3 × × ×

Spring事务的隔离级别与 数据库隔离级别的关系:

Spring默认的隔离级别, 是 Isolation.DEFAULT

它的含义是:使用数据库默认的事务隔离级别。

除此之外,另外Spring事务的隔离级别 四个与 JDBC 的隔离级别是相对应的,那个四个 Spring事务隔离级别,其实是在数据库隔离级别之上又进一步进行了封装。

如果 Spring事务的隔离级别与 数据库隔离级别的不一致会怎样?

以Spring事务为准的。

Spring 事务管理涉及到了与数据库的交互 。

JDBC 加载的流程 有四步:注册驱动,建立连接,发起请求,输出结果, 伪代码如下:

Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
    // 1.注册 JDBC 驱动
    Class.forName("com.mysql.jdbc.Driver");
    // 2.创建链接
    System.out.println("连接数据库...");
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/my_db","root","root");
    // 3.发起请求
    stmt = conn.createStatement();
    String sql = "SELECT id, name, url FROM websites";
    rs = stmt.executeQuery(sql);
    // 4.输出结果
    System.out.print("查询结果:" + rs);
    // 关闭资源(演示代码,不要纠结没有写在finally中)
    rs.close();
    stmt.close();
    conn.close();
} catch (SQLException se)
    se.printStackTrace();
}catch(Exception e){
    e.printStackTrace();
}

在创建连接阶段,JDBC 从数据库获取一个连接 Connection 对象

Connection 对象不仅有连接数据库的方法,还有设置当前连接的事物隔离级别的方法, 源码如下:

*/
public interface Connection  extends Wrapper, AutoCloseable {

  ... 

  /**

- 尝试将此连接对象的事务隔离级别更改为给定的级别
- 接口连接中定义的常量是可能的事务隔离级别
*/
  void setTransactionIsolation(int level) throws SQLException;

  ...
}

该方法的注释说明:尝试将此连接对象的事务隔离级别更改为给定的级别,如果在事务期间调用此方法,则结果由实现定义。

所以,如果spring与数据库事务隔离级别不一致时,spring 会调用类似的方法, 设置 一下 当前链接的 事务隔离级别。

第三大属性:@Transactional 注解的 readOnly属性

@Transactional注解的readOnly 属性用于指定事务是否为只读事务。

readOnly属性设置为true时,表示该事务只涉及读取数据, 而不进行任何写操作(如INSERT、UPDATE、DELETE等)。这有助于数据库引擎优化事务处理,因为它知道不需要考虑事务的并发写操作。

当使用 @Transaction 注解时,可以通过设置 readOnly=true 来指定这是一个只读事务,这样在事务执行期间就不会对数据进行修改,只会进行查询操作。

以下是一个使用 @Transaction 只读示例的代码片段:

@Service
public class UserService {
   

    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = true)
    public User getUserById(Long id) {
   
        return userRepository.findById(id).orElse(null);
    }

    // 其他方法...
}

在上面的示例中,getUserById 方法被标记为只读事务,因此在执行期间只会进行查询操作。如果在方法中尝试进行修改操作,将会抛出异常。

从数据库层面来讲,设置readOnly = true会向数据库发送一个信号,告诉数据库这个事务是只读的。

不同的数据库会根据这个提示进行优化。例如

  • 在一些数据库中,对于只读事务,数据库可以避免获取写锁,减少锁竞争,从而提高并发读取性能。
  • 同时,数据库也可能会跳过一些与写操作相关的日志记录和事务处理逻辑,提高事务执行的效率。

第四大属性:@Transactional 注解的 rollbackFor 回滚规则属性

事务五边形的rollbackFor 回滚规则属性 , 定义了哪些异常会导致事务回滚,而哪些异常不会。

下面是一个简单的 Java 代码示例,演示了 @Transactional 回滚规则属性。

首先是 不做配置,使用 rollbackFor 的默认值:

@Service
public class UserService {
   

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void createUser(User user) {
   
        userRepository.save(user);
        if (user.getId() == null) {
   
            throw new RuntimeException("Failed to create user");
        }
    }
}

在上面的示例中, 如果在方法执行过程中发生异常,事务会自动回滚,保证数据的一致性。

如果用户创建失败,createUser 方法会抛出一个 RuntimeException 异常,这会导致事务回滚,用户创建操作会被撤销。

尼恩提示,@Transactional 使用有很多的 约束:

  • 约束1 :@Transactional 注解只能应用于公共方法,因为只有公共方法才能被代理,从而实现事务管理。

  • 约束2 :默认情况下, @Transactional 注解 只对非受检 异常进行回滚,而对受检查异常不进行回滚。

非检查型异常 (Unchecked Exception/非受检查异常)的是程序在编译时不会提示需要处理该异常,而是在运行时才会出现异常, 如 RuntimeException。

检查型异常(Checked Exception)是指在 Java 中,编译器会强制要求对可能会抛出这些异常的代码进行异常处理,否则代码将无法通过编译。

一般来说,在编写代码时应该尽量避免抛出非检查型异常(如 RuntimeException),因为这些异常的发生通常意味着程序存在严重的逻辑问题。

如果是受检 异常(Checked Exception), 进行回滚,可以在 @Transactional 注解中指定 rollbackFor 属性,例如

@Transactional(rollbackFor = Exception.class)
public void createUser(User user) {
      userRepository.save(user);
       if (user.getId() == null) {
            throw new RuntimeException("Failed to create user");
      }
}

掌握了 @Transactional 的几个核心属性, 最后我们来说下 @Transactional 的失效场景。

Spring事务 的10种 失效场景

Spring事务管理 是Java应用中确保数据库操作一致性和完整性的关键机制之一。

然而,在实际开发中,有时候会遇到Spring事务失效的情况,导致期望的事务行为无法正常发生。

本文将深入探讨九种常见的导致Spring事务失效的场景,帮助开发者更好地理解事务管理的细节和注意事项。

场景1:非Spring容器管理的 事务方法

Spring事务是通过AOP(面向切面编程)来实现的,如果一个事务注解被应用到一个普通的Java类的方法上,并且该类不是通过Spring容器进行管理的,那么事务将不会生效。

因为Spring无法拦截并管理这个类的方法调用。

示例:

public class TransactionalService {
   
    @Transactional
    public void performTransaction() {
   
        // 事务操作
    }
}

在上述示例中,如果TransactionalService不是通过Spring容器进行管理,那么@Transactional注解将不会生效。

场景2: 在非公有方法上使用事务

Spring事务默认只对公有方法上的事务注解生效。@Transactional 应用在非 public 修饰的方法上,@Transactional 将会失效。

如果在一个非公有方法上使用事务注解,事务将不会生效。

示例:

@Transactional
private void performTransaction() {
      
    // 非公有方法上使用事务,事务失效
}

在上述示例中,performTransaction是一个私有方法,事务注解不会生效。

protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

场景3:异常被捕获 而不是 抛出

有时候,开发者可能选择捕获掉一个异常,而不重新抛出或处理。

这样的做法将导致事务失效,因为Spring事务管理依赖于异常来判断是否需要回滚事务。

示例:

@Transactional
public void handleException() {
   
    try {
   
        // 事务操作
        throw new RuntimeException("Simulate Exception");
    } catch (Exception e) {
   
        // 异常被忽略,事务失效
    }
}

在上述示例中,异常被捕获但未重新抛出或处理,导致事务失效。

场景4: 对 受检查异常进行 异常拦截

默认情况下, Spring事务只对RuntimeException(非受检查异常)及其子类进行回滚。

如果一个受事务管理的方法抛出了 受检查异常(如Exception), 默认情况下,事务将不会回滚。

示例:

@Transactional
public void performTransaction()    throws  Exception{
   

        // 事务操作
        throw new Exception("Unchecked Exception");
}

在上述示例中,抛出了一个 受检查异常, 导致事务失效。

如果是 对 受检查异常进行捕获, 需要使用 rollbackFor 定制回滚 规则:

@Transactional(rollbackFor = Exception.class)
public void createUser(User user)    throws  Exception{

        // 事务操作
        throw new Exception("Unchecked Exception");

}

场景5:方法内部调用导致的事务失效

Spring事务默认只对外部方法调用进行代理,对于同一个类的内部方法调用是无法触发事务的。

如果在一个事务方法内部调用另一个方法,而这个被调用的方法上标注了@Transactional注解,事务将不会生效。

示例:

@Transactional
public void outerTransaction() {
   
    innerTransaction(); // 内部调用,事务失效
}

@Transactional
public void innerTransaction() {
   
    // 内部事务操作
}

在上述示例中,outerTransaction方法内部调用了innerTransaction方法,但由于默认只对外部方法调用进行代理,导致innerTransaction方法上的事务失效。

场景6: 方法自调用导致的事务失效

类似于内部方法调用,如果一个事务方法内部自己调用自己,事务同样会失效。

这是因为Spring使用代理机制来管理事务,自调用会绕过代理对象,导致事务不生效。

示例:

@Transactional
public void selfInvokingTransaction() {
   
    // 自调用,事务失效
    selfInvokingTransaction();
    // 事务操作
}

在上述示例中,selfInvokingTransaction方法内部自己调用了自己,导致事务失效。

场景7: 在同一个类中,一个非事务方法调用另一个事务方法

当在同一个类中,一个非事务方法调用了另一个事务方法时,事务将不会生效。

这是因为Spring默认使用动态代理来管理事务,而动态代理只能拦截外部调用。

示例:

public void nonTransactionMethodA() {
   
    transactionMethodB(); // 在同一个类中调用另一个事务方法,事务失效
}

@Transactional
public void transactionMethodB() {
   
    // 事务操作
}

在上述示例中,nonTransactionMethodA调用了transactionMethodB,但事务不会生效。

场景8: 使用错误的事务传播行为

Spring事务提供了不同的传播行为,如REQUIREDREQUIRES_NEW等。

使用错误的传播行为可能导致事务失效,因为传播行为决定了事务如何在方法调用链中传播。

示例:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void performTransaction() {
   
    // 使用错误的传播行为,可能导致事务失效
}

在上述示例中,如果使用了错误的传播行为,可能会导致事务失效。

场景9: 数据库引擎不支持事务

数据库引擎不支持事务,Spring事务 失效。

这一点很简单,myisam 引擎是不支持事务的,innodb 引擎支持事务。

场景10:数据源没有配置事务管理器

数据源没有配置事务管理器,这个也很简单,要使用事务肯定要配事务管理器。

Hibernate 用的是HibernateTransactionManager,

JDBC 和 Mybatis 用的是 DataSourceTransactionManager。

如果数据源没有配置事务管理器 ,Spring事务 失效。

Spring事务 的10种 失效场景总结

开发者应当牢记这些场景,并在开发过程中注意避免出现事务失效的情况,以确保数据的一致性和完整性。

顶奢好文:3W字,穿透Spring事务原理、源码,最少读10遍

高端面试:必须来点 高大上的答案:

尼恩 提示: 要拿到 高薪offer, 或者 要进大厂,必须来点 非常见的、 高大上的答案, 整点技术狠活儿。

如果能讲 到尼恩答案 的 水平 , 面试官一定口水直流, 大厂 offer 就到手啦。

尼恩架构团队,持续为大家 梳理了一系列的 塔尖 面试题,帮助大家 进大厂,拿高薪:

  • Java基础

美团面试:String 为什么 不可变 ?(90%答错了,尼恩来一个绝世答案)

  • 索引

阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?

滴滴面试:单表可以存200亿数据吗?单表真的只能存2000W,为什么?

  • 索引下推 ?

贝壳面试:什么是回表?什么是 索引下推 ?

  • 索引失效

美团面试:mysql 索引失效?怎么解决?(重点知识,建议收藏,读10遍+)

  • MVCC

MVCC学习圣经:一文穿透MySQL MVCC,吊打面试官

  • binlog、redolog、undo log

美团面试:binlog、redolog、undo log底层原理是啥?分别实现ACID哪个特性?(尼恩图解,史上最全)

  • mysql 事务

阿里面试:事务ACID,底层是如何实现的?

京东面试:RR隔离mysql如何实现?什么情况RR不能解决幻读?

  • 分布式事务

分布式事务圣经:从入门到精通,架构师尼恩最新、最全详解 (50+图文4万字全面总结 )

阿里面试:秒杀的分布式事务, 是如何设计的?

说在最后:有问题找45岁老架构取经‍

只要按照上面的 尼恩团队梳理的 方案去作答, 你的答案不是 100分,而是 120分。 面试官一定是 心满意足, 五体投地。

按照尼恩的梳理,进行 深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。

很多小伙伴刷完后, 吊打面试官, 大厂横着走。

在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会, 可以找尼恩来改简历、做帮扶。前段时间,空窗2年 成为 架构师, 32岁小伙逆天改命, 同学都惊呆了

狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。

尼恩技术圣经系列PDF

……完整版尼恩技术圣经PDF集群,请找尼恩领取

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
7月前
|
Java 数据库 开发者
|
7月前
|
Java 数据库 Spring
Spring事务的传播机制(行为、特性)
Spring事务的传播机制(行为、特性)
128 0
|
3月前
|
Java 测试技术 数据库
Spring事务传播机制(最全示例)
在使用Spring框架进行开发时,`service`层的方法通常带有事务。本文详细探讨了Spring事务在多个方法间的传播机制,主要包括7种传播类型:`REQUIRED`、`SUPPORTS`、`MANDATORY`、`REQUIRES_NEW`、`NOT_SUPPORTED`、`NEVER` 和 `NESTED`。通过示例代码和数据库插入测试,逐一展示了每种类型的运作方式。例如,`REQUIRED`表示如果当前存在事务则加入该事务,否则创建新事务;`SUPPORTS`表示如果当前存在事务则加入,否则以非事务方式执行;`MANDATORY`表示必须在现有事务中运行,否则抛出异常;
154 4
Spring事务传播机制(最全示例)
|
7月前
|
Java 数据库连接 数据库
spring事务失效(疑难杂症)
spring事务失效(疑难杂症)
|
7月前
|
Java Spring
spring事务传播机制
spring事务传播机制
24 0
|
Java 数据库 开发者
探究Spring事务:了解失效场景及应对策略
在现代软件开发中,数据的一致性和完整性是至关重要的。为了保证这些特性,Spring框架提供了强大的事务管理机制,让开发者能够更加自信地处理数据库操作。然而,事务并非银弹,存在一些失效的情景,本文将带您深入探究Spring事务及其失效场景,并为您呈现应对策略。
199 0
探究Spring事务:了解失效场景及应对策略
|
Java Spring
这些Spring事务传播性类型,你有必要了解下
在Spring框架中,事务传播性是一个重要概念,决定了事务在方法间的传播和交互方式。了解不同的事务传播特性可以帮助我们设计可靠、灵活的系统。
这些Spring事务传播性类型,你有必要了解下
声明式事务-- 事务的传播机制-- 事务传播机制种类
声明式事务-- 事务的传播机制-- 事务传播机制种类
75 0
|
存储 Java 关系型数据库
Spring事务传播机制
通过举例Spring 父方法、子方法调用以及事务传播机制验证
Spring事务传播机制
|
XML 存储 前端开发
Spring事务传播行为实战
Spring框架提供了事务管理的标准实现,且可以通过注解或者XML文件的方式声明和配置事务。 通过异步事件的方式解耦服务调用,可以提高程序的响应速度,并且避免因为事务传播行为而导致的事务问题。 本文以一个电商平台包裹出库的业务为实际背景,通过异步事件与线程池的方式解耦嵌套事务,提高程序并发性能;为了便于问题的分析和方案的理解,同时还讲解了Spring的事务管理,并着重介绍了几种不同的事务传播行为。
189 0
Spring事务传播行为实战