关于java:从头到尾说一次-Spring-事务管理器还不会你打我

3次阅读

共计 10884 个字符,预计需要花费 28 分钟才能阅读完成。

先点赞再看,养成好习惯

事务管理:一个被说烂的也被看烂的话题,还是八股文中的根底股之一。但除了八股文中须要熟读并背诵的那些个流传行为之外,背地的“为什么”和外围原理更为重要。​

写这篇文章之前,我也翻过一些事务管理器原理介绍文章,但大多都是僵硬的翻译源码,一个劲的给源码加正文。这种源码翻译的文章虽说有帮忙,但对读者来说体验并不好,很容易陷入代码的一些细节里,并不能帮忙读者疾速的理解事务管理的全貌,以及设计思路。​

本文会从设计角度,一步步的分析 Spring 事务管理的设计思路(都会设计事务管理器了,还能不不会用么?)

为什么须要事务管理?

先看看如果没有事务管理器的话,如果 想让多个操作(办法 / 类)处在一个事务里 应该怎么做:

// MethodA:
public void methodA(){Connection connection = acquireConnection();
    try{int updated = connection.prepareStatement().executeUpdate();
        methodB(connection);
        connection.commit();}catch (Exception e){rollback(connection);
    }finally {releaseConnection(connection);
    }
}

// MethodB:
public void methodB(Connection connection){int updated = connection.prepareStatement().executeUpdate();}

或者用 ThreadLocal 存储 Connection?

static ThreadLocal<Connection> connHolder = new ThreadLocal<>();

// MethodA:
public void methodA(){Connection connection = acquireConnection();
    connHolder.set(connection);
    try{int updated = connection.prepareStatement().executeUpdate();
        methodB();
        connection.commit();}catch (Exception e){rollback(connection);
    }finally {releaseConnection(connection);
        connHolder.remove();}
}

// MethodB:
public void methodB(){Connection connection = connHolder.get();
    int updated = connection.prepareStatement().executeUpdate();
}

还是有点恶心,再形象一下?将绑定 Connection 的操作提取为公共办法:

static ThreadLocal<Connection> connHolder = new ThreadLocal<>();

private void bindConnection(){Connection connection = acquireConnection();
    connHolder.set(connection);
}

private void unbindConnection(){releaseConnection(connection);
    connHolder.remove();}

// MethodA:
public void methodA(){
    try{bindConnection();
        int updated = connection.prepareStatement().executeUpdate();
        methoB();
        connection.commit();}catch (Exception e){rollback(connection);
    }finally {unbindConnection();
    }
}

// MethodB:
public void methodB(){Connection connection = connHolder.get();
    int updated = connection.prepareStatement().executeUpdate();
}

当初看起来好点了,不过我有一个新的需要:想让 methodB 独立一个新事务,独自提交和回滚,不影响 methodA ​

这……可就有点难搞了,ThreadLocal 中曾经绑定了一个 Connection,再新事务的话就不好办了 ​

那如果再简单点呢,methodB 中须要调用 methodC,methodC 也须要一个独立事务…… ​

而且,每次 bind/unbind 的操作也有点太傻了,万一哪个办法忘了写 unbind,最初来一个连贯泄露那不是完蛋了!​

好在 Spring 提供了事务管理器,帮咱们解决了这一系列痛点。

Spring 事务管理解决了什么问题?

Spring 提供的事务管理能够帮咱们治理事务相干的资源,比方 JDBC 的 Connection、Hibernate 的 Session、Mybatis 的 SqlSession。如说下面的 Connection 绑定到 ThreadLocal 来解决共享一个事务的这种形式,Spring 事务管理就曾经帮咱们做好了。​

还能够帮咱们解决简单场景下的嵌套事务,比方后面说到的 methodB/methodC 独立事务。

什么是嵌套事务?

还是拿下面的例子来说,methodA 中调用了 methodB,两个办法都有对数据库的操作,而且都须要事务:

// MethodA:
public void methodA(){int updated = connection.prepareStatement().executeUpdate();
    methodB();
    // ...
}

// MethodB:
public void methodB(){// ...}

这种多个办法调用链中都有事务的场景,就是嵌套事务。不过要留神的是,并不是说多个办法应用一个事务才叫嵌套,哪怕是不同的事务,只有在这个办法的调用链中,都是嵌套事务。

什么是事务流传行为?

那调用链中的子办法,是用一个新事务,还是应用以后事务呢?这个子办法决定应用新事务还是以后事务(或不应用事务)的策略,就叫事务流传。​

在 Spring 的事务管理中,这个子办法的事务处理策略叫做事务流传行为(Propogation Behavior)

有哪些事务流传行为?

Spring 的事务管理反对多种流传行为,这里就不贴了,八股文里啥都有。​

但给这些流传行为分类之后,无非是以下三种:

  1. 优先应用以后事务
  2. 不应用以后事务,新建事务
  3. 不应用任何事务

比方下面的例子中,methodB/methodC 独立事务,就属于第 2 种流传行为 – 不应用以后事务,新建事务

看个例子

以 Spring JDBC + Spring 注解版 的事务举例。在默认的事务流传行为下,methodA 和 methodB 会应用同一个 Connection,在一个事务中

@Transactional
public void methodA(){jdbcTemplate.batchUpdate(updateSql, params);
    methodB();}

@Transactional
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}

如果我想让 methodB 不应用 methodA 的事务,本人新建一个连贯 / 事务呢?只须要简略的配置一下 @Transactional 注解:

@Transactional
public void methodA(){jdbcTemplate.batchUpdate(updateSql, params);
    methodB();}

// 流传行为配置为 - 形式 2,不应用以后事务,独立一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}

就是这么简略,获取 Connection/ 多办法共享 Connection/ 多办法共享 + 独享 Connection/ 提交 / 开释连贯之类的操作,齐全不须要咱们操心,Spring 都替咱们做好了。

怎么回滚?

在注解版的事务管理中,默认的的回滚策略是:抛出异样就回滚。这个默认策略挺好,连回滚都帮咱们解决了,再也不必手动回滚。​

然而如果在嵌套事务中,子办法独立新事务呢?这个时候哪怕抛出异样,也只能回滚子事务,不能间接影响前一个事务

可如果这个抛出的异样不是 sql 导致的,比方校验不通过或者其余的异样,此时应该将以后的事务回滚吗?​

这个还真不肯定,谁说抛异样就要回滚,异样也不回滚行不行?​

当然能够!抛异样和回滚事务原本就是两个问题,能够连在一起,也能够离开解决

// 流传行为配置为 - 形式 2,不应用以后事务,独立一个新事务

// 指定 Exception 也不会滚
@Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = Exception.class)
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}

每个事务 / 连贯应用不同配置

除了流传和回滚之外,还能够给每个事务 / 连贯应用不同的配置,比方不同的隔离级别:

@Transactional
public void methodA(){jdbcTemplate.batchUpdate(updateSql, params);
    methodB();}

// 流传行为配置为 - 形式 2,不应用以后事务,独立一个新事务
// 这个事务 / 连贯中应用 RC 隔离级别,而不是默认的 RR
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}

除了隔离级别之外,其余的 JDBC Connection 配置当然也是反对的,比方 readOnly。这样一来,尽管咱们不必显示的获取 connection/session,但还是能够给嵌套中的每一个事务配置不同的参数,非常灵活。

性能总结

好了,当初曾经理解了 Spring 事务管理的所有外围性能,来总结一下这些外围性能点:

  1. 连贯 / 资源管理 – 无需手动获取资源、共享资源、开释资源
  2. 嵌套事务的反对 – 反对嵌套事务中应用不同的资源策略、回滚策略
  3. 每个事务 / 连贯应用不同的配置

事务管理器(TransactionManager)模型

其实认真想想,事务管理的外围操作只有两个:提交和回滚。后面所谓的流传、嵌套、回滚之类的,都是基于这两个操作。​

所以 Spring 将事务管理的外围性能形象为一个 事务管理器(Transaction Manager),基于这个事务管理器外围,能够实现多种事务管理的形式。​

这个外围的事务管理器只有三个性能接口:

  1. 获取事务资源,资源能够是任意的,比方 jdbc connection/hibernate mybatis session 之类,而后绑定并存储
  2. 提交事务 – 提交指定的事务资源
  3. 回滚事务 – 回滚指定的事务资源
interface PlatformTransactionManager{
    // 获取事务资源,资源能够是任意的,比方 jdbc connection/hibernate mybatis session 之类
    TransactionStatus getTransaction(TransactionDefinition definition)
            throws TransactionException;

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

    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}

事务定义 – TransactionDefinition

还记得下面的 @Transactional 注解吗,外面定义了流传行为、隔离级别、回滚策略、只读之类的属性,这个就是一次事务操作的定义。​

在获取事务资源时,须要依据这个事务的定义来进行不同的配置:

  1. 比方配置了应用新事务,那么在获取事务资源时就须要创立一个新的,而不是已有的
  2. 比方配置了隔离级别,那么在首次创立资源(Connection)时,就须要给 Connection 设置 propagation
  3. 比方配置了只读属性,那么在首次创立资源(Connection)时,就须要给 Connection 设置 readOnly

为什么要独自用一个 TransactionDefinition 来存储事务定义,间接用注解的属性不行吗?​

当然能够,但注解的事务管理只是 Spring 提供的自动挡,还有适宜老司机的手动挡事务管理(前面会介绍);手动挡可用不了注解,所以独自建一个事务定义的模型,这样就能够实现通用。

事务状态 – TransactionStatus

那既然嵌套事务下,每个子办法的事务可能不同,所以还得有一个子办法事务的状态 – TransactionStatus,用来存储以后事务的一些数据和状态,比方事务资源(Connection)、回滚状态等。

获取事务资源

事务管理器的第一步,就是依据事务定义来获取 / 创立资源了,这一步最麻烦的是要辨别流传行为,不同流传行为下的逻辑不太一样。​

“默认的流传行为下,应用以后事务”,怎么算有以后事务呢?​

把事务资源存起来嘛,只有曾经存在那就是有以后事务,间接获取已存储的事务资源就行。文中结尾的例子也演示了,如果想让多个办法无感的应用同一个事务,能够用 ThreadLocal 存储起来,简略粗犷。​

Spring 也是这么做的,不过它实现的更简单一些,形象了一层 事务资源同步管理器 – TransactionSynchronizationManager(本文前面会简称 TxSyncMgr),在这个同步管理器里应用 ThreadLocal 存储了事务资源(本文为了不便了解,尽可能的不贴非关键源码)。​

剩下的就是依据不同流传行为,执行不同的策略了,分类之后只有 3 个条件分支:

  1. 以后有事务 – 依据不同流传行为解决不同
  2. 以后没事务,但须要开启新事务
  3. 彻底不必事务 – 这个很少用
public final TransactionStatus getTransaction(TransactionDefinition definition) {
    // 创立事务资源 - 比方 Connection
    Object transaction = doGetTransaction();

    if (isExistingTransaction(transaction)) {
        // 解决以后已有事务的场景
        return handleExistingTransaction(def, transaction, debugEnabled);
    }else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED){

        // 开启新事务
        return startTransaction(def, transaction, debugEnabled, suspendedResources);
    }else {// 彻底不必事务}

    // ...
}

先介绍一下 分支 2 – 以后没事务,但须要开启新事务,这个逻辑绝对简略一些。只须要新建事务资源,而后绑定到 ThreadLocal 即可:

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
            boolean debugEnabled, SuspendedResourcesHolder suspendedResources) {

        // 创立事务
        DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);

        // 开启事务(beginTx 或者 setAutoCommit 之类的操作)// 而后将事务资源绑定到事务资源管理器 TransactionSynchronizationManager
        doBegin(transaction, definition);

当初回到分支 1 – 以后有事务 – 依据不同流传行为解决不同,这个就略微有点麻烦了。因为有子办法独立事务的需要,可是 TransactionSynchronizationManager 却只能存一个事务资源。

挂起(Suspend)和复原(Resume)

Spring 采纳了一种 挂起(Suspend)– 复原(Resume)的设计来解决这个嵌套资源解决的问题。当子办法须要独立事务时,就将以后事务挂起,从 TxSyncMgr 中移除以后事务资源,创立新事务的状态时,将挂起的事务资源保留至新的事务状态 TransactionStatus 中;在子办法完结时,只须要再从子办法的事务状态中,再次拿出挂起的事务资源,从新绑定至 TxSyncMgr 即可实现复原的操作。​

整个挂起 – 复原的流程,如下图所示:​

留神:挂起操作是在获取事务资源这一步做的,而复原的操作是在子办法完结时(提交或者回滚)中进行的。

这样一来,每个 TransactionStatus 都会保留挂起的前置事务资源,如果办法调用链很长,每次都是新事务的话,那这个 TransactionStatus 看起来就会像一个链表:

提交事务

获取资源、操作结束起初到了提交事务这一步,这个提交操作比较简单,只有两步:

  1. 以后是新事务才提交
  2. 解决挂起资源

怎么晓得是新事务?​

每通过一次事务嵌套,都会创立一个新的 TransactionStatus,这个事务状态里会记录以后是否是新事务。如果多个子办法都应用一个事务资源,那么除了第一个创立事务资源的 TransactionStatus 之外,其余都不是新事务。​

如下图所示,A -> B -> C 时,因为 BC 都应用以后事务,那么尽管 ABC 所应用的事务资源是一样的,然而只有 A 的 TransactionStatus 是新事务,BC 并不是;那么在 BC 提交事务时,就不会真正的调用提交,只有回到 A 执行 commit 操作时,才会真正的调用提交操作。

这里再解释下,为什么新事务才须要提交,而曾经有事务却什么都不必做:

因为对于新事务来说,这里的提交操作曾经是事务实现了;而对于非新事务的场景,前置事务(即以后事务)还没有执行完,可能前面还有其余数据库操作,所以这个提交的操作得让以后事务创立方去做,这里并不能提交。

回滚事务

除了提交,还有回滚呢,回滚事务的逻辑和提交事务相似:

  1. 如果是新事务才回滚,起因下面曾经介绍过了
  2. 如果不是新事务则只设置回滚标记
  3. 解决挂起资源

留神:事务管理器是不蕴含回滚策略这个货色的,回滚策略是 AOP 版的事务管理加强的性能,但这个性能并不属于外围的事务管理器

自动挡与手动挡

Spring 的事务管理性能都是围绕着下面这个事务管理器运行的,提供了三种治理事务的形式,别离是:

  1. XML AOP 的事务管理 – 比拟古老当初用的不多
  2. 注解版本的事务管理 – @Transactional
  3. TransactionTemplate – 手动挡的事务管理,也称编程式事务管理

自动挡

XML/@Transactional 两种基于 AOP 的注解治理,其入口类是 TransactionInterceptor,是一个 AOP 的 Interceptor,负责调用事务管理器来实现事务管理。​

因为外围性能都在事务管理器里实现,所以这个 AOP Interceptor 很简略,只是调用一下事务管理器,外围(伪)代码如下:

public Object invoke(MethodInvocation invocation) throws Throwable {

    // 获取事务资源
    Object transaction = transactionManager.getTransaction(txAttr);    
    Object retVal;

    try {
        // 执行业务代码
        retVal = invocation.proceedWithInvocation();

        // 提交事务
        transactionManager.commit(txStatus);
    } catch (Throwable ex){
        // 先判断异样回滚策略,而后调用事务管理器的 rollback
        rollbackOn(ex, txStatus);
    } 
}

并且 AOP 这种自动挡的事务管理还减少了一个回滚策略的玩法,这个是手动挡 TransactionTemplate 所没有的,但这个性能并不在事务管理器中,只是 AOP 版事务的一个加强。

手动挡

TransactionTemplate 这个是手动挡的事务管理,尽管没有注解的不便,然而好在灵便,异样 / 回滚啥的都能够本人管制。​

所以这个实现更简略,连异样回滚策略都没有,非凡的回滚形式还要本人设置(默认是任何异样都会回滚),外围(伪)代码如下:

public <T> T execute(TransactionCallback<T> action) throws TransactionException {

    // 获取事务资源
    TransactionStatus status = this.transactionManager.getTransaction(this);
    T result;
    try {

        // 执行 callback 业务代码
        result = action.doInTransaction(status);
    }
    catch (Throwable ex) {

        // 调用事务管理器的 rollback
        rollbackOnException(status, ex);
    }

    提交事务
    this.transactionManager.commit(status);
    }
}

为什么有这么不便的自动挡,还要手动挡?

因为自动挡更灵便啊,想怎么玩就怎么玩,比方我能够在一个办法中,执行多个数据库操作,但应用不同的事务资源:

Integer rows = new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                       new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {
            // update 0
            int rows0 = jdbcTemplate.update(...);

            // update 1
            int rows1 = jdbcTemplate.update(...);
            return rows0 + rows1;
        }
    });

Integer rows2 = new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                        new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {

            // update 2
            int rows2 = jdbcTemplate.update(...);
            return rows2;
        }
    });

在下面这个例子里,通过 TransactionTemplate 咱们能够准确的管制 update0/update1 应用同一个事务资源和隔离级别,而 update2 独自应用一个事务资源,并且不须要新建类加注解的形式。

手自一体能够吗?

当然能够,只有咱们应用的是同一个事务管理器的实例,因为绑定资源到同步资源管理器这个操作是在事务管理器中进行的。​

AOP 版本的事务管理里,同样能够应用手动挡的事务管理持续操作,而且还能够应用同一个事务资源。​

比方上面这段代码,update1/update2 依然在一个事务内,并且 update2 的 callback 完结后并不会提交事务,事务最终会在 methodA 完结时,TransactionInterceptor 中才会提交

@Transactional
public void methodA(){

    // update 1
    jdbcTemplate.update(...);
    new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                        new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {

            // update 2
            int rows2 = jdbcTemplate.update(...);
            return rows2;
        }
    });

}

总结

Spring 的事务管理,其外围是一个形象的事务管理器,XML/@Transactional/TransactionTemplate 几种形式都是基于这个事务管理器的,三中形式的外围实现区别并不大,只是入口不同而已。

本文为了不便了解,省略了大量的非关键实现细节,可能会导致有局部形容不谨严的中央,如有问题欢送评论区留言。​

正文完
 0