Spring事务

java / Spring / 2022-12-04
0 869

一、事务的概念

对一组操作而言,要么全部成功,要么全部失败,就叫做事务。

01 特性

  • 原子性(Atomicity):一个事务中的所有操作,要么全部成功,要么全部失败,如果中间发生异常,则全部回滚
  • 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏
  • 隔离性(Isolation):不同事务间相互隔离,互不影响
  • 持久性(Durability):事务结束后,数据便是永久的

02 事务隔离级别

  • 未提交读(Read uncommitted),最低的隔离级别,允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。如果另一个事务回滚,那么当前事务读到的数据就是脏数据。
  • 读已提交(read committed),一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
  • 可重复读(repeatable read),一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。
  • 串行化(Serializable),最严格的隔离级别,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。

二、spring中事务

spring中支持两种事务:编程式事务、声明式事务。声明式事务就是常用的@Transational注解。

01 声明式事务

@Transational就是常用的声明式事务使用方式,spring使用aop机制完成对事务的支持,本质就是对方法前后进行拦截,在目标方法执行前创建一个事务,在执行后选择回滚或是提交。

02 编程式事务

编程式事务就是自己写代码实现事务控制,可以使用TransationTemplate来控制事务或者使用TransationManager来控制事务。spring更推荐前者。


声明式事务和编程式事务的优缺点:

1、声明式事务只支持对方法的事务控制,事务的力度比编程式事务大

2、声明式事务不用自己实现事务控制,对代码的侵入性接近0

三、spring事务原理

01 spring事务整体架构

spring整体架构.png

02 事务管理器

Spring将事务管理的核心抽象成了一个事务管理器(TransactionManager),它只有一个接口定义,属于标记接口。
该接口有两个实现,分别是编程式事务接口ReactiveTransactionManager和声明式事务接口PlatformTransactionManager。
通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

    void commit(TransactionStatus var1) throws TransactionException;

    void rollback(TransactionStatus var1) throws TransactionException;
}

TransactionDefinition和@Transational注解相对应。

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;
}
@Target({ElementType.TYPE, ElementType.METHOD})
@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 {};
}

思考:通常在一个service的业务代码中会同时存在多个对dao层的操作,spring是如何保证多个对dao层的操作的事务的呢?

JDBC中对事务的控制是由同一个java.sql.Connection完成的,想要保证多个dao层操作处于同一个事务,只需要保证它们处于同一个java.sql.Connection中即可。
spring完成这一功能的原理:在开始事务前,先获取java.sql.Connection,并绑定到当前调用线程上,之后获取数据库连接直接拿过来用,spring使用 ThreadLocal 保存java.sql.Connection。具体可以参照:TransactionSynchronizationManager
DataSourceUtils

DataSourceUtils最主要的工作是对connection的管理,DataSourceUtils会从TransactionSynchronizationManager那里获取Connection资源。如果当前线程之前没有绑定任何connection,那么它就通过数据访问对象的DataSource引用获取新的connection,否则就使用绑定的那个connection。这就是为什么要强调,当我们要使用Spring提供的事务支持的时候,必须通过DataSourceUtils来获取连接。因为它提供了Spring事务管理框架在数据访问层需要提供的基础设施中不可或缺的一部分,而JdbcTemplate等类内部已经使用DataSourceUtils来管理连接了,所以,我们不用操心这些细节。从这里,我们也应可以看出,Spring的事务管理与它的数据访问框架是紧密结合的。

03 事务传播行为

TransactionDefinition 一共定义了 7 种事务传播行为,其中PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW 两种传播行为是比较常用的。

PROPAGATION_REQUIRED
这也是 @Transactional 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:
如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。

也就是说如果a方法和b方法都添加了注解,在默认传播模式下,a方法内部调用b方法,会把两个方法的事务合并为一个事务。

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

当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 ,总结就是a不影响b,b影响a

其余几种传播行为 PROPAGATION_NESTED 如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。 当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.NESTED模式,然后在 在a 方法里调用 b方法操作数据库,然而 b方法抛出异常后,a方法是不的回滚 ,总结就是b不影响a,a影响b。

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

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

PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

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

04 事务隔离级别

TransactionDefinition 中一共定义了 5 种事务隔离级别:

  • ISOLATION_DEFAULT,使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。
  • ISOLATION_READ_UNCOMMITTED,最低的隔离级别,可能会出现脏读、幻读或者不可重复读。
  • ISOLATION_READ_COMMITTED,允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。
  • ISOLATION_REPEATABLE_READ,对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • ISOLATION_SERIALIZABLE,最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能。

通常情况下,我们采用默认的隔离级别 ISOLATION_DEFAULT 就可以了,也就是交给数据库来决定。

05 事务超时时间

事务允许执行的最长时间,超过就回滚

06 事务只读属性

事务的只读属性readOnly, 如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。
为什么一个查询操作还要启用事务支持呢?
这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。
那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。
有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。

07 事务不/回滚策略

**回滚策略rollbackFor **,用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
**不回滚策略noRollbackFor **,用于指定不触发事务回滚的异常类型,可以指定多个异常类型。

08 事务失效场景

  • 事务访问修饰符为非public

解决方式:
方式一:将方法修饰符改为public
方式二:开启AspectJ代理模式

⚠️ 如果是static、final的方法 事务也无法生效

  • 方法内部调用
  • 多线程调用
  • 数据库不支持事务
  • 异常没有抛出或是spring不支持回滚的异常

09 DEBUG

1、创建代理对象
AbstractAutoProxyCreator.postProcessAfterInitialization

    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
        if (bean != null) {
            Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
            if (!this.earlyProxyReferences.contains(cacheKey)) {
                return this.wrapIfNecessary(bean, beanName, cacheKey); // 在这里创建代理对象
            }
        }

        return bean;
    }
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        } else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        } else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {
            Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);
            if (specificInterceptors != DO_NOT_PROXY) {
                this.advisedBeans.put(cacheKey, Boolean.TRUE);
                // 在这里创建代理对象
                Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
                this.proxyTypes.put(cacheKey, proxy.getClass());
                return proxy;
            } else {
                this.advisedBeans.put(cacheKey, Boolean.FALSE);
                return bean;
            }
        } else {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }
    }

调用堆栈:
调用堆栈.png

2、同一线程使用同一数据库连接
方法调用堆栈:
调用堆栈1.png

service是代理对象,会由动态代理调用目标方法,而目标方法加了事务,所以会经过spring事务拦截器为目标方法添加事务,在添加事务时会将dataSource绑定到TransactionSynchronizationManager.ThreadLocal中在同一线程中传递。
开启事务.png
绑定数据源.png
绑定1.png

public abstract class TransactionSynchronizationManager {
    private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
    private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
    private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
    private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");

    public TransactionSynchronizationManager() {
    }
}