Spring 中的事务

前言:

之前总结了事务以及数据库中事务相关的知识点,Spring 对于事务做了相应的封装,便于业务开发中使用事务。

项目中使用Spring中的事务首先时基于Mysql数据库中InnoDB 引擎的,如果数据库中就是使用MyISAM 引擎那么就不支持事务了。

1. Spring 中的事务

Spring 项目中如何对代码块使用事务?

  • 编程式事务

通过 TransactionTemplate 或者 TransactionManager 手动管理事务。实际中很少使用。手写代码编程的方式。

// 使用 TransactionTemplate
 @Autowired
    private TransactionTemplate transactionTemplate;

    public void tansactional1() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                try {
                    // 进行业务处理

                } catch (Exception e) {
                    // 回滚操作
                    transactionStatus.setRollbackOnly();
                }
            }
        });
    }
// 使用 TransactionManager

@Autowired
    private PlatformTransactionManager transactionManager;

    public void transactional2() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 事务处理的业务代码
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }
  • 声明式事务 使用注解

对代码侵入小,实际通过 AOP 实现

@Transactional

@Transactional
    public void transactional3(){
        // 需要事务处理的业务代码
    }

2. Spring 事务管理接口

Spring中事务相关的管理主要涉及以下三个重要接口

  • PlatformTransactionManager: 对于事务的管理器 ,Spring事务策略的核心
  • TransactionDefinition:定义事务执行相关的规则 (事务的隔离级别、传播行为、超时、只读、回滚规则)
  • TransactionStatus: 用于事务运行的状态

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

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

2.1 PlatformTransactionManager: 事务管理接口

Spring 并不直接管理事务,而是提供了多种事务管理器。Spring提供事务管理器的接口是PlatformTransactionManager。

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

// PlatformTransactionManager 源码
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager {
    // 获取事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

    // 提交事务
    void commit(TransactionStatus var1) throws TransactionException;

    // 回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;
}
2.2 TransactionDefinition: 事务定义

事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到一个事务,这个方法里面的参数是 TransactionDefinition 类 ,这个类就定义了一些基本的事务属性。

事务属性定义了一些事务配置,描述了事务如何应用到方法上。主要描述五个方面:隔离级别、传播行为、回滚规则、是否只读、事务超时.以下代码为 TransactionDefinition 类的内容

package org.springframework.transaction;

import org.springframework.lang.Nullable;

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();
}
2.3 TransactionStatus: 事务状态

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

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

以下为 TransactionStatus 的定义

package org.springframework.transaction;

import java.io.Flushable;

public interface TransactionStatus extends SavepointManager, Flushable {

    // 是否是新的事务
    boolean isNewTransaction();

    // 是否有恢复点
    boolean hasSavepoint();

    // 设置为只回滚
    void setRollbackOnly();

    // 是否为只回滚
    boolean isRollbackOnly();

    // 刷新事务
    void flush();
    
    // 事务是否完成
    boolean isCompleted();
}

3. 事务属性

通常我们使用 @Transactional 注解来开启事务,@Transactional 中的参数包含了上面说到 TransactionDefinition 中描述的事务的五个方面 。

@Transactional 注解的定义:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
3.1 事务的传播行为 propagation

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

当事务方法被另一个事务方法调用时,此时事务应该如何处理呢? 此时必须指定事务应该如何传播。

比如如下代码:a() 方法和 b() 方法之间调用的事务问题,如何配置在b() 发生异常回滚时,a() 也回滚呢? 默认是如何配置的? 传播行为都有那些?

@Transactional(propagation = Propagation.?)
    public void a() {
        // something to do
        b();
    }

    @Transactional(propagation = Propagation.?)
    public void b() {
        
    }

以下为几种事务传播行为的定义

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

正确的四种事务传播行为配置:

  • REQUIRED:
    • 使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • 如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  • 如果外部方法开启事务并且被 Propagation.REQUIRED 的话,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。

例如如果我们上面的a()和b()使用的都是Propagation.REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。

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

如果我们上面的b()使用Propagation.REQUIRES_NEW事务传播行为修饰,a()还是用REQUIRED修饰的话。如果a()发生异常回滚,b()不会跟着回滚,因为 b()开启了独立的事务。但是,如果 b()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,a()同样也会回滚,因为这个异常被 a()的事务管理机制检测到了

  • NESTED
    • 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 Propagation.REQUIRED
  • 在外部方法未开启事务的情况下Propagation.NESTED和Propagation.REQUIRED作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
  • 如果外部方法开启事务的话,Propagation.NESTED修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。

举个例子:
如果 aMethod() 回滚的话,bMethod()和bMethod2()都要回滚,而bMethod()回滚的话,并不会造成 aMethod() 和bMethod()回滚。

Class A {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
        b.bMethod2();
    }
}

Class B {
    @Transactional(propagation=propagation.PROPAGATION_NESTED)
    public void bMethod {
       //do something
    }
    @Transactional(propagation=propagation.PROPAGATION_NESTED)
    public void bMethod2 {
       //do something
    }
}
  • MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)这个使用的很少

错误的三种配置:事务将不会发生回滚,使用的很少。

  • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

  • NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

  • NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

3.2 事务的隔离级别 isolation

isolation 配置事务的隔离级别

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

DEFAULT :使用后端数据库中默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别.

其他的事务的隔离级别与之前一篇数据库中事务的隔离级别相对应的。

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

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) :,但是你要知道的是 InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读) 并不会什么任何性能上的损失。

3.3 事务的超时属性 timeout

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

3.4 事务的只读属性 isReadOnly

什么是只读事务

// 返回是否为只读事务,默认值为 false
boolean isReadOnly();

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

为什么一个数据查询操作还要启用事务支持呢

拿 MySQL 的 innodb 举例子:

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

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

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

其他解读

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

这些规则定义了哪些异常会导致事务回滚而哪些不会。

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

如果你想要回滚你定义的特定的异常类型的话,可以这样:

@Transactional(rollbackFor= MyException.class)

4. @Transactional 注解使用详解

4.1 @Transactional 的作用范围

方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。

类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。

接口 :不推荐在接口上使用。

4.2 @Transactional 的常用配置参数

propagation、isolation、timeout、readOnly、rollbackFor 之前都有说 不再赘述

4.3 @Transactional 事务注解原理

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

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

DefaultAopProxyFactory 中 createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,

4.4 Spring AOP 自调用问题

若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。
这是由于Spring AOP代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
MyService 类中的method1()调用method2()就会导致method2()的事务失效。

@Service
public class MyService {

private void method1() {
     method2();
     //......
}
@Transactional
 public void method2() {
     //......
  }
}

解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。

4.5 @Transactional 的使用注意事项总结
  • @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
  • 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
  • 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败

References

相关推荐