美文网首页
spring 事务详解

spring 事务详解

作者: Q南南南Q | 来源:发表于2020-09-02 14:50 被阅读0次

    事务特性(ACID)

    • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用
    • 一致性: 执行事务前后,数据保持一致
    • 隔离性: 并发访问数据库时,一个用户的事物不被其他事务所干扰也就是说多个事务并发执行时,一个事务的执行不应影响其他事务的执行
    • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

    spring 事务管理接口介绍

    Spring 框架中,事务管理相关最重要的 3 个接口如下:

    • PlatformTransactionManager: (平台)事务管理器,Spring 事务策略的核心
    • TransactionDefinition: 事务属性(事务隔离级别、传播行为、超时、只读、回滚规则)
    • TransactionStatus: 事务运行状态

    我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinitionTransactionStatus 这两个接口可以看作是事务的描述。

    PlatformTransactionManager 会根据 TransactionDefinition 的定义(比如事务超时时间、隔离界别、传播行为等)来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态(比如是否新事务、是否可以回滚)

    一、PlatformTransactionManager(事务管理)

    spring 事务管理接口,通过这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了

    PlatformTransactionManager 接口中定义了三个方法:

    public interface PlatformTransactionManager {
        //获得事务
        TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
        //提交事务
        void commit(TransactionStatus var1) throws TransactionException;
        //回滚事务
        void rollback(TransactionStatus var1) throws TransactionException;
    }
    

    二、TransactionDefinition(事务属性)

    用于描述事务隔离级别、传播行为、超时、只读、回滚规则

    public interface TransactionDefinition {
        int PROPAGATION_REQUIRED = 0;
        int PROPAGATION_SUPPORTS = 1;
        int PROPAGATION_MANDATORY = 2;
        int PROPAGATION_REQUIRES_NEW = 3;
        int PROPAGATION_NOT_SUPPORTED = 4;
        int PROPAGATION_NEVER = 5;
        int PROPAGATION_NESTED = 6;
        int ISOLATION_DEFAULT = -1;
        int ISOLATION_READ_UNCOMMITTED = 1;
        int ISOLATION_READ_COMMITTED = 2;
        int ISOLATION_REPEATABLE_READ = 4;
        int ISOLATION_SERIALIZABLE = 8;
        int TIMEOUT_DEFAULT = -1;
        // 返回事务的传播行为,默认值为 REQUIRED。
        int getPropagationBehavior();
        //返回事务的隔离级别,默认值是 DEFAULT
        int getIsolationLevel();
        // 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
        int getTimeout();
        // 返回是否为只读事务,默认值为 false
        boolean isReadOnly();
    
        @Nullable
        String getName();
    }
    

    三、TransactionStatus(事务状态)

    TransactionStatus 接口用来记录事务的状态,该接口定义了一组方法用来获取或判断事务的相应状态信息

    PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象

    TransactionStatus 接口接口内容如下:

    public interface TransactionStatus{
        // 是否是新的事物
        boolean isNewTransaction(); 
        // 是否有恢复点
        boolean hasSavepoint(); 
        // 设置为只回滚
        void setRollbackOnly();  
        // 是否为只回滚
        boolean isRollbackOnly(); 
        // 是否已完成
        boolean isCompleted; 
    }
    

    spring 事务属性

    事务传播行为

    一、简介

    事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

    事务传播行为是为了解决业务层方法之间互相调用的事务问题

    spring 定义了 7 中事务传播行为,其含义如下:

    事务行为 说明
    PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务
    PROPAGATION_SUPPORTS 当前方法不需要事务上下文,但若存在当前事务,则该方法会加入当前事务
    PROPAGATION_MANDATORY 该方法必须在事务中运行,若当前事物不存在,则抛出一个异常:IllegalTransactionStateException("Transaction propagation ‘mandatory’ but no existing transaction found")
    PROPAGATION_REQUIRES_NEW 当前方法必须运行在它自己的事务中。一个新的事务将被启动。若存在当前事务,则该方法执行期间,当前事务挂起。若用 JTATransactionManager 的话,则需访问 TransactionManager。内层事务和外层事务相互独立,互不影响
    PROPAGATION_NOT_SUPPORTED 该方法以非事务方式运行。若存在当前事务,则在该方法运行期间,当前事务挂起。若用 JTATransactionManager 的话,则需访问 TransactionManager
    PROPAGATION_NEVER 该方法不应该运行在事务上下文,若当前有一个事务正在运行,则抛出异常
    PROPAGATION_NESTED 若当前已存在一个事务,则该方法将会在嵌套事务中运行。若没有活动事务, 则按 PROPAGATION_REQUIRED 执行。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。嵌套事务开始执行时, 它将取得一个 savepoint。若嵌套事务失败, 则回滚到此 savepoint。嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交

    二、示例设计

    示例设计中,我们主要分为两部分:同一类中事务方法的传播行为以及不同类中事务方法的传播行为。我们以表中的情况进行组合,模拟事务方法的传播行为

    function a(外层事务方法) function b(内层事务方法)
    外层事务正常执行,不捕获内层事务异常 内层事务正常执行
    外层事务正常执行,捕获内层事务异常 内层事务执行异常,捕获异常
    外层事务执行异常,抛出异常 内层事务执行异常,抛出异常
    外层事务执行异常,捕获异常 内层非事务

    2.1 同一类中事务方法的传播行

    2.2 不同类中事务方法的传播行为

    2.2.1 内层方法没有声明事务

    场景:外层 PROPAGATION_REQUIRED,内层没有事务

    结果:正常执行

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        serviceB.innerFunction();
    }
    
    public void innerFunction() {
        // todo database operate
    }
    

    场景:外层 PROPAGATION_REQUIRED,内层抛出异常,外层没有捕获异常

    结果:全部回滚

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        serviceB.innerFunction();
    }
    
    public void innerFunction() {
        // todo database operate
        throw new RuntimeException("innerFunction RuntimeException");
    }
    

    场景:外层 PROPAGATION_REQUIRED,内层抛出异常,外层捕获异常

    结果:正常执行,不回滚

    @Transactional(rollbackFor = Exception.class)
        public void function() {
            // todo database operate
            try {
                serviceB.innerFunction();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    public void innerFunction() {
        // todo database operate
        throw new RuntimeException("innerFunction RuntimeException");
    }
    
    2.2.2 PROPAGATION_REQUIRED

    使用的最多的一个事务传播行为,我们平时经常使用的 @Transactional 注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:

    1. 如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰
    2. 如果外部方法开启事务并且被 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚

    场景:内层 PROPAGATION_REQUIRED,并抛出异常,外层事务不捕获异常

    结果:全部回滚

    原因:外层事务方法没有捕获内层事务方法抛出的异常,因此进行回滚操作

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        serviceB.innerFunction();
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void innerFunction() {
        // todo database operate
        throw new RuntimeException("innerFunction RuntimeException");
    }
    

    场景:内层 PROPAGATION_REQUIRED,并抛出异常,外层事务捕获异常

    结果:全部回滚

    原因:当内层事务异常的情况下,如果是 PROPAGATION_REQUIRED,正常来讲是需要回滚的,但是 spring 只给内层事务做了一个 rollback 的标记,当内层事务抛出的异常被外层捕获时,外层事务正常执行,但在最后提交的时候发现,内层事务被标记为 rollbck,所以就会抛出 UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        try {
            serviceB.innerFunction();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void innerFunction() {
        // todo database operate
        throw new RuntimeException("innerFunction RuntimeException");
    }
    

    解决 UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only 的方案有两个:

    1. 内层事务方法捕获自己抛出的异常

      @Transactional(rollbackFor = Exception.class)
      public void innerFunction() {
          try {
              // todo database operate
              throw new RuntimeException("innerFunction RuntimeException");
          } catch (RuntimeException e) {
              e.printStackTrace();
          }
      }
      
    2. 将内层事务传播行为改为 PROPAGATION_REQUIRES_NEW,详情见 2.2.3 Propagation.REQUIRES_NEW

      @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
      public void innerFunction() {
          // todo database operate
          throw new RuntimeException("innerFunction RuntimeException");
      }
      
    2.2.3 PROPAGATION_REQUIRES_NEW

    创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰

    场景:内层 PROPAGATION_REQUIRES_NEW,并抛出异常,外层事务不捕获异常

    结果:外层事务不回滚,内层事务回滚

    原因:内外层事务互不影响,内层事务的回滚不影响外层事务的正常执行

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        serviceB.innerFunction();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void innerFunction() {
        // todo database operate
        throw new RuntimeException("innerFunction RuntimeException");
    }
    

    场景:内层 PROPAGATION_REQUIRES_NEW,并抛出异常,外层事务捕获异常

    结果:外层事务不回滚,内层事务回滚

    原因:内外层事务互不影响,内层事务的回滚不影响外层事务的正常执行

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        try {
            serviceB.innerFunction();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void innerFunction() {
        // todo database operate
        throw new RuntimeException("innerFunction RuntimeException");
    }
    

    场景:外层事务抛出异常,内层事务正常执行

    结果:外层事务回滚,内层事务不回滚

    原因:内外层事务互不影响,外层事务的回滚不影响内层事务的正常执行

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        serviceB.innerFunction();
        throw new RuntimeException("function RuntimeException");
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void innerFunction() {
        // todo database operate
    }
    

    场景:外层事务抛出异常并捕获,内层事务正常执行

    结果:内外层事务均不回滚

    原因:外层事务捕获了 RuntimeException,因此不回滚

    @Transactional(rollbackFor = Exception.class)
    public void function() {
        // todo database operate
        try {
            serviceB.innerFunction();
            throw new RuntimeException("function RuntimeException");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void innerFunction() {
        // todo database operate
    }
    
    2.2.4 PROPAGATION_NESTED

    如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。也就是说:

    1. 在外部方法未开启事务的情况下 Propagation.NESTED 和 Propagation.REQUIRED 作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
    2. 如果外部方法开启事务的话,Propagation.NESTED 修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务

    场景:外层事务方法 function 正常执行,内层事务方法 innerFunction1 执行正常,内存事务方法 innerFunction2 抛出异常

    结果:全部回滚

    原因:有人可能会问,不应该是内层事务的回滚不影响外层事务执行吗?为什么会全部回滚。原因在于 innerFunction2 抛出 RuntimeException 后,function 没有进行捕获处理,因此该 RuntimeException 出发了 function 的 rollbackFor = {Exception.class} 条件,导致所以操作均回滚。正确的方式为对 innerFunction2 包一层 try-catch 语句,这样就达到内层事务回滚不影响外层事务了

    // function 没有捕获 innerFunction2 抛出的异常,因此 function 也会回滚,这是错误的打开方式
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
    public void function() {
        // todo database operate
        serviceB.innerFunction1();
        serviceB.innerFunction2();
    }
    
    // 这才是正确的打开方式
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
    public void function11() {
        // todo database operate
        try {
            serviceB.innerFunction1();
            serviceB.innerFunction2();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
    public void innerFunction1() {
        // todo database operate
    }
    
    @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
    public void innerFunction2() {
        // todo database operate
        throw new RuntimeException("innerFunction7 RuntimeException");
    }
    

    场景:外层事务方法 function 抛出异常,内层事务方法 innerFunction1 和 innerFunction2 执行正常

    结果:全部回滚

    原因:外层事务影响内层事务

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
    public void function() {
        // todo database operate
        try {
            serviceB.innerFunction1();
            serviceB.innerFunction2();
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new RuntimeException("function RuntimeException");
    }
    
    @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
    public void innerFunction1() {
        // todo database operate
    }
    
    @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
    public void innerFunction2() {
        // todo database operate
    }
    
    2.2.5 PROPAGATION_MANDATORY

    如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

    事务隔离级别

    TransactionDefinition 接口中定义了五个表示隔离级别的常量:

    public interface TransactionDefinition {
        ......
        int ISOLATION_DEFAULT = -1;
        int ISOLATION_READ_UNCOMMITTED = 1;
        int ISOLATION_READ_COMMITTED = 2;
        int ISOLATION_REPEATABLE_READ = 4;
        int ISOLATION_SERIALIZABLE = 8;
        ......
    }
    
    事务隔离界别 说明
    ISOLATION_DEFAULT 使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别,Oracle 默认采用的 READ_COMMITTED 隔离级别
    ISOLATION_READ_UNCOMMITTED 最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
    ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
    ISOLATION_REPEATABLE_READ 同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
    ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别

    MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 SELECT @@tx_isolation; 命令来查看

    这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以保证事务的隔离性要求,即达到了 SQL 标准的 SERIALIZABLE(可串行化) 隔离级别

    事务超时属性

    所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1

    事务只读属性

    public interface TransactionDefinition {
        ......
        // 返回是否为只读事务,默认值为 false
        boolean isReadOnly();
    }
    

    对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中

    很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?

    拿 MySQL 的 innodb 举例子,根据官网 https://dev.mysql.com/doc/refman/5.7/en/innodb-autocommit-commit-rollback.html 描述:

    MySQL 默认对每一个新建立的连接都启用了autocommit模式。在该模式下,每一个发送到 MySQL 服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。

    但是,如果你给方法加上了 @Transactional 注解的话,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。

    如果不加 @Transactional,每条 sql 会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值

    分享一下关于事务只读属性,其他人的解答:

    1. 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性
    2. 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持

    事务回滚规则

    默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚

    事务失效场景

    一、数据库引擎是否支持事务

    Mysql 的 MyIsam 引擎不支持事务

    二、注解所在的类是否注入 spring 容器中

    三、注解所在方法不是 public 或者是 final

    这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

    在 spring 中动态代理分为 JDK 动态代理和 CGLIB 动态代理,JDK 动态代理要求必须实现接口(所以方法必须是public的),但是 CGLIB 动态代理底层则是通过字节码生成被代理类的子类来实现的,这里要求被代理类必须能被继承(public 和 protected),被 final 修饰的方法不能被子类继承,因此 @Transactional 注解无效。但为何 @Transactional 注解不支持 protected 方法呢?

    spring 官方文档中有如下说明:

    Spring AOP 对 privateprotect 是不支持的,无论是 JDK 还是 CGLIB,如果要对 protect 方法进行拦截,建议使用 AspectJ

    不清楚 Spring 为什么不推荐其 AOP 对 protect 不支持,猜测可能:

    1. 代理行为本身就是一种三方调用的思想,那么被代理的方法本身应该是公有的
    2. 为了跟让 CGLIB 和 JDK 保持一致,因为 JDK 基于接口的肯定都是 public 的,而 CGLIB 干嘛搞特殊?
    3. 待续猜想

    四、所用数据源是否加载了事务管理器

    五、事务自调用(同一个类中的 A 方法调用 B 方法)

    若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务被忽略,不会发生回滚

    使用 AOP 代理后的方法调用执行流程,如图所示,可以看到调用者首先调用的是 AOP 代理对象而不是目标对象,首先执行事务切面,事务切面内部通过 TransactionInterceptor 环绕增强进行事务的增强,即进入目标方法之前开启事务,退出目标方法时提交/回滚事务

    目标对象内部的自我调用将无法实施切面中的增强,如图所示,this 指向目标对象,因此调用 this.b() 将不会执行 b 事务切面,即不会执行事务增强

    六、当方法发生异常时,使用 try-catch 捕获了异常,并且 catch 中没有抛出异常或者手动回滚

    事务的回滚是方法发生异常,在 aop 的异常通知中进行拦截回滚。如果方法中捕获了异常,是不会被 aop 的异常通知拦截到的。如果使用 try-catch 捕获异常,需要在catch中抛出一个异常或者在 catch 中通过 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 设置手动回滚

    @Transactional 事务注解原理

    @Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现接口,会使用 CGLIB 动态代理

    createAopProxy 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下:

    public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
    
        @Override
        public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
            if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
                ...
                if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                    return new JdkDynamicAopProxy(config);
                }
                return new ObjenesisCglibAopProxy(config);
            } else {
                return new JdkDynamicAopProxy(config);
            }
        }
        .......
    }
    

    如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务

    相关文章

      网友评论

          本文标题:spring 事务详解

          本文链接:https://www.haomeiwen.com/subject/bkhwsktx.html