关于spring:Spring-声明式事务应该怎么学

8次阅读

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

大家好呀,我是课代表。
关注我的公众号:Java 课代表,原创干货首发地儿,等你来呦。

1、引言

Spring 的申明式事务极大中央便了日常的事务相干代码编写,它的设计如此奇妙,以至于在应用中简直感觉不到它的存在,只须要优雅地加一个 @Transactional 注解,所有就都牵强附会地实现了!

毫不夸大地讲,Spring 的申明式事务切实是太好用了,以至于大多数人都遗记了编程式事务应该怎么写。

不过,越是你认为理所应当的事件,如果出了问题,就越难排查。不晓得你和身边的小伙伴有没有遇到过 @Transactional 生效的场景,这不然而日常开发中常踩的坑,也是面试中的高频问题。

其实这些生效场景不必死记硬背,如果搞明确了它的工作原理,再联合源码,须要用到的时候 Debug 一下就能本人剖析进去。毕竟,源码才是最好的说明书。

还是那句话,授人以鱼不如授人以渔,课代表就算总结 100 种生效场景,也不肯定能笼罩到你可能踩到的坑。所以本文中,课代表将联合几个常见生效状况,从源码层面解释其生效起因。认真读完本文,置信你会对申明式事务有更粗浅的意识。

文中所有代码已上传至课代表的 github,为了不便疾速部署并运行,示例代码采纳了内存数据库H2,不须要额定部署数据库环境。

2、回顾手写事务

数据库层面的事务,有 ACID 四个个性,他们独特保障了数据库中数据的准确性。事务的原理并不是本文的重点,咱们只须要晓得样例中用的 H2 数据库齐全实现了对事务的反对(read committed)。

编写 Java 代码时,咱们应用 JDBC 接口与数据库交互,实现事务的相干指令,伪代码如下:

// 获取用于和数据库交互的连贯
Connection conn = DriverManager.getConnection();
try {
    // 敞开主动提交:
    conn.setAutoCommit(false);
    // 执行多条 SQL 语句:
    insert(); 
    update(); 
    delete();
    // 提交事务:
    conn.commit();} catch (SQLException e) {
    // 如果出现异常,回滚事务:
    conn.rollback();} finally {
    // 开释资源
    conn.close();}

这是典型的编程式事务代码流程:开始前先敞开主动提交,因为默认状况下,主动提交是开启的,每条语句都会开启新事务,执行结束后主动提交。

敞开事务的主动提交,是为了让多个 SQL 语句在同一个事务中。代码失常运行,就提交事务,出现异常,就整体回滚,以此保障多条 SQL 语句的整体性。

除了事务提交,数据库还反对保留点的概念,在一个物理事务中,能够设置多个保留点,不便回滚到指定保留点(其相似玩单机游戏时的存档,你能够在角色挂掉后随时回到上次的存档)设置和回滚到保留点的代码如下:

// 设置保留点    
Savepoint savepoint = connection.setSavepoint();
// 回滚到指定的保留点
connection.rollback(savepoint);
// 回滚到保留点后按需提交 / 回滚后面的事务
conn.commit();//conn.rollback();

Spring 申明式事务所做的工作,就是围绕着 提交 / 回滚 事务,设置 / 回滚到保留点 这两对命令进行的。为了让咱们尽可能地少写代码,Spring 定义了几种流传属性将事务做了进一步的形象。留神哦,Spring 的事务流传(Propagation) 只是 Spring 定义的一层形象而已,和数据库没啥关系,不要和数据库的事务隔离级别混同。

3、Spring 的事务流传(Transaction Propagation)

察看传统事务代码:

    conn.setAutoCommit(false);
    // 执行多条 SQL 语句:
    insert(); 
    update(); 
    delete();
    // 提交事务:
    conn.commit();

这段代码表白的是三个 SQL 语句在同一个事务里。

他们可能是同一个类中的不同办法,也可能是不同类中的不同办法。如何来表白诸如事务办法退出别的事务、新建本人的事务、嵌套事务等等概念呢?这就要靠 Spring 的事务流传机制了。

事务流传(Transaction Propagation)就是字面意思:事务的流传 / 传递 形式。

在 Spring 源码的 TransactionDefinition 接口中,定义了 7 种流传属性,官网对其中的 3 个做了阐明,咱们只有搞懂了这 3 个,剩下的 4 个就是触类旁通的事了。

1)PROPAGATION_REQUIRED

字面意思:流传 - 必须

PROPAGATION_REQUIRED是其 默认 流传属性,强制开启事务,如果之前的办法曾经开启了事务,则退出前一个事务,二者在物理上属于同一个事务。

一图胜千言,下图示意它俩物理上是在同一个事务内:

上图翻译成伪代码是这样的:

try {conn.setAutoCommit(false);
    transactionalMethod1(); 
    transactionalMethod2();
    conn.commit();} catch (SQLException e) {conn.rollback();
} finally {conn.close();
}

既然在同一个物理事务中,那如果 transactionalMethod2() 产生了异样,导致须要回滚,那么请问 transactionalMethod1() 是否也要回滚呢?

得益于下面的图解和伪代码,咱们能够很容易地得出答案,transactionalMethod1()必定回滚了。

这里抛一个问题:

事务办法外面的异样被 try catch 吃了,事务还能回滚吗?

先别着急出论断,看上面两段代码示例。

示例一:不会回滚的状况(事务生效)

察看上面的代码,methodThrowsException()什么也没干,就抛了个异样,调用方将其抛出的异样try catch 住了,该场景下是不会触发回滚的

@Transactional(rollbackFor = Exception.class)
public void tryCatchRollBackFail(String name) {jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {methodThrowsException();
    } catch (RollBackException e) {//do nothing}
}

public void methodThrowsException() throws RollBackException {throw new RollBackException(ROLL_BACK_MESSAGE);
}

示例二:会回滚的状况(事务失效)

再看这个例子,同样是 try catch 了异样,后果却截然相同

@Transactional(rollbackFor = Throwable.class)
public void tryCatchRollBackSuccess(String name, String anotherName) {jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {
        // 带事务,抛异样回滚
        userService.insertWithTxThrowException(anotherName);
    } catch (RollBackException e) {// do nothing}
}

@Transactional(rollbackFor = Throwable.class)
public void insertWithTxThrowException(String name) throws RollBackException {jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    throw new RollBackException(ROLL_BACK_MESSAGE);
}

本例中,两个办法的事务都没有设置 propagation 属性,默认都是 PROPAGATION_REQUIRED。即前者开启事务,后者退出后面开启的事务,二者同属于一个物理事务。insertWithTxThrowException() 办法抛出异样,将事务标记为回滚。既然大家是在一条船上,那么后者打翻了船,前者必定也不能幸免。

所以 tryCatchRollBackSuccess() 所执行的 SQL 也必将回滚,执行此用例能够查看后果

拜访 http://localhost:8080/h2-cons…,连贯信息如下:

点击 Connect 进入控制台即可查看表中数据:

USER 表的确没有插入数据,证实了咱们的论断,并且能够看到日志报错:

Transaction rolled back because it has been marked as rollback-only事务曾经回滚,因为它被标记为必须回滚。

也就是前面办法触发的事务回滚,让后面办法的插入也回滚了。

看到这里,你应该能把默认的流传类型 PROPAGATION_REQUIRED 了解透彻了,本例中是因两个办法在同一个物理事务下,相互影响从而回滚。

你可能会问,那我如果想让前后两个开启了事务的办法互不影响该怎么办呢?

这就要用到上面要说的流传类型了。

2)、PROPAGATION_REQUIRES_NEW

字面意思:流传 - 必须 - 新的

PROPAGATION_REQUIRES_NEWPROPAGATION_REQUIRED 不同的是,其总是开启独立的事务,不会参加到已存在的事务中,这就保障了两个事务的状态互相独立,互不影响,不会因为一方的回滚而烦扰到另一方。

一图胜千言,下图示意他俩物理上不在同一个事务内:

上图翻译成伪代码是这样的:

//Transaction1
try {conn.setAutoCommit(false);
    transactionalMethod1(); 
    conn.commit();} catch (SQLException e) {conn.rollback();
} finally {conn.close();
}
//Transaction2
try {conn.setAutoCommit(false); 
    transactionalMethod2();
    conn.commit();} catch (SQLException e) {conn.rollback();
} finally {conn.close();
}

TransactionalMethod1 开启新事务,当他调用同样须要事务的 TransactionalMethod2 时,因为后者的流传属性设置了PROPAGATION_REQUIRES_NEW,所以挂起后面的事务(至于如何挂起,前面咱们会从源码中窥见),并开启一个物理上独立于前者的新事务,这样二者的事务回滚就不会互相烦扰了。

还是后面的例子,只须要把 insertWithTxThrowException() 办法的事务流传属性设置为 Propagation.REQUIRES_NEW 就能够互不影响了:

@Transactional(rollbackFor = Throwable.class)
public void tryCatchRollBackSuccess(String name, String anotherName) {jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {
        // 带事务,抛异样回滚
        userService.insertWithTxThrowException(anotherName);
    } catch (RollBackException e) {// do nothing}
}

@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRES_NEW)
public void insertWithTxThrowException(String name) throws RollBackException {jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    throw new RollBackException(ROLL_BACK_MESSAGE);
}

PROPAGATION_REQUIREDPropagation.REQUIRES_NEW 曾经足以应答大部分利用场景了,这也是开发中罕用的事务流传类型。前者要求基于同一个物理事务,要回滚一起回滚,后者是大家应用独立事务互不干涉。还有一个场景就是:内部办法和外部办法共享一个事务,然而内部事务的回滚不影响内部事务,内部事务的回滚能够影响内部事务。这就是嵌套这种流传类型的应用场景。

3)、PROPAGATION_NESTED

字面意思:流传 - 嵌套

PROPAGATION_NESTED能够在一个已存在的物理事务上设置多个供回滚应用的保留点。这种局部回滚能够让内部事务在其本人的作用域内回滚,与此同时,内部事务能够在某些操作回滚后继续执行。其底层实现就是数据库的savepoint

这种流传机制比后面两种都要灵便,看上面的代码:

@Transactional(rollbackFor = Throwable.class)
public void invokeNestedTx(String name,String otherName) {jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {userService.insertWithTxNested(otherName);
    } catch (RollBackException e) {// do nothing}
    // 如果这里抛出异样,将导致两个办法都回滚
    // throw new RollBackException(ROLL_BACK_MESSAGE);
}

@Transactional(rollbackFor = Throwable.class,propagation = Propagation.NESTED)
public void insertWithTxNested(String name) throws RollBackException {jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    throw new RollBackException(ROLL_BACK_MESSAGE);
}

内部事务办法 invokeNestedTx() 开启事务,内部事务办法 insertWithTxNested 标记为嵌套事务,内部事务的回滚通过保留点实现,不会影响内部事务。而内部办法的回滚,则会连带外部办法一块回滚。

小结:本大节介绍了 3 种常见的 Spring 申明式事务流传属性,联合样例代码,置信你也对其有所理解了,接下来咱们从源码层面看一看,Spring 是如何帮咱们简化事务样板代码,解放生产力的。

4、源码窥探

在浏览源码前,先剖析一个问题:我要给一个办法增加事务,须要做哪些工作呢?

就算咱们本人手写,也至多得须要这么四步:

  • 开启事务
  • 执行办法
  • 遇到异样就回滚事务
  • 失常执行后提交事务

这不就是典型的 AOP 嘛~

没错,Spring 就是通过 AOP,将咱们的事务办法加强,从而实现了事务的相干操作。上面给出几个要害类及其要害办法的源码走读。

既然是 AOP 那必定要给事务写一个切面来做这个事,这个类就是 TransactionAspectSupport ,从命名能够看出,这就是“事务切面反对类”,他的次要工作就是实现事务的执行流程,其次要实现办法为invokeWithinTransaction

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {
    
    // 省略代码...
    // Standard transaction demarcation with getTransaction and commit/rollback calls.
    // 1、开启事务
    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
       try {
        // This is an around advice: Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        //2、执行办法
        retVal = invocation.proceedWithInvocation();}
    catch (Throwable ex) {
        // target invocation exception
        // 3、捕捉异样时的解决
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    }
    finally {cleanupTransactionInfo(txInfo);
    }

    if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
        // Set rollback-only in case of Vavr failure matching our rollback rules...
        TransactionStatus status = txInfo.getTransactionStatus();
        if (status != null && txAttr != null) {retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
        }
    }
    //4、执行胜利,提交事务
    commitTransactionAfterReturning(txInfo);
    return retVal;
    // 省略代码...

联合课代表减少的这四步正文,置信你很容易就能看明确。

搞懂了事务的次要流程,它的流传机制又是怎么实现的呢?这就要看 AbstractPlatformTransactionManager 这个类了,从命名就能看出,它负责事务管理,其中的 handleExistingTransaction 办法实现了事务流传逻辑,这里挑 PROPAGATION_REQUIRES_NEW 的实现跟一下代码:

private TransactionStatus handleExistingTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled)
            throws TransactionException {
        // 省略代码...
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {if (debugEnabled) {
                logger.debug("Suspending current transaction, creating new transaction with name [" +
                        definition.getName() + "]");
            }
            // 事务挂起
            SuspendedResourcesHolder suspendedResources = suspend(transaction);
            try {return startTransaction(definition, transaction, debugEnabled, suspendedResources);
            }
            catch (RuntimeException | Error beginEx) {resumeAfterBeginException(transaction, suspendedResources, beginEx);
                throw beginEx;
            }
        }
        // 省略代码...
    }

前文咱们晓得 PROPAGATION_REQUIRES_NEW 会将前一个事务挂起,并开启独立的新事务,而数据库是不反对事务的挂起的,Spring 是如何实现这一个性的呢?

通过源码能够看到,这里调用了返回值为 SuspendedResourcesHoldersuspend(transaction)办法,它的理论逻辑由其外部的 doSuspend(transaction) 形象办法实现。这里咱们应用的是 JDBC 连贯数据库,天然要抉择 DataSourceTransactionManager 这个子类去查看其实现,代码如下:

protected Object doSuspend(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        txObject.setConnectionHolder(null);
        return TransactionSynchronizationManager.unbindResource(obtainDataSource());
    }

这里是把已有事务的 connection 解除,并返回该挂起资源。在接下来开启事务时,会将该挂起资源一并传入,这样当内层事务执行实现后,能够继续执行外层被挂起的事务。

那么,什么时候来继续执行被挂起的事务呢?

事务的流程,尽管是由 TransactionAspectSupport 实现的,然而真正的提交,回滚,是由 AbstractPlatformTransactionManager 来实现,在其 processCommit(DefaultTransactionStatus status) 办法最初的 finally 块中,执行了cleanupAfterCompletion(status):

private void cleanupAfterCompletion(DefaultTransactionStatus status) {status.setCompleted();
        if (status.isNewSynchronization()) {TransactionSynchronizationManager.clear();
        }
        if (status.isNewTransaction()) {doCleanupAfterCompletion(status.getTransaction());
        }
         // 有挂起事务则获取挂起的资源,继续执行
        if (status.getSuspendedResources() != null) {if (status.isDebug()) {logger.debug("Resuming suspended transaction after completion of inner transaction");
            }
            Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
           
            resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
        }
    }

这里判断有挂起的资源将会复原执行,至此实现挂起和复原事务的逻辑。

对于其余事务流传属性的实现,感兴趣的同学应用课代表的样例工程,打断点本人去跟一下源码。限于篇幅,这里只给出了大略解决流程,源码里有大量细节,须要同学们本人去体验,有了上文介绍的主逻辑框架根底,跟踪源码查看其余实现应该不怎么吃力了。

5、常见生效场景

很多人(包含课代表自己)一开始应用申明式事务,都会感觉这玩意儿真坑,应用起来那么多条条框框,一不小心就不失效了。为什么会有这种感觉呢?

爬了屡次坑之后,课代表总结了两条教训:

  1. 没看官网文档
  2. 不会读源码

上面简略列举几个生效场景:

1)非 public 办法不失效

官网有阐明:

Method visibility and @Transactional

When you use transactional proxies with Spring’s standard configuration, you should apply the @Transactional annotation only to methods with public visibility.

2)Spring 不反对 redis 集群中的事务

redis事务开启命令是multi,然而 Spring Data Redis 不反对 redis 集群中的 multi 命令,如果应用了申明式事务,将会报错:MULTI is currently not supported in cluster mode.

3)多数据源状况下须要为每个数据源配置 TransactionManager,并指定transactionManager 参数

第四局部源码窥探中曾经看到理论执行事务操作的是 AbstractPlatformTransactionManager,其为TransactionManager 的实现类,每个事务的 connection 连贯都受其治理,如果没有配置,无奈实现事务操作。单数据源的状况下失常运行,是因为 SpringBoot 的 DataSourceTransactionManagerAutoConfiguration 为咱们主动配置了。

4)rollbackFor 设置谬误

默认状况下只回滚非受检异样(也就是,java.lang.RuntimeException的子类)和java.lang.Error,如果明确晓得抛异样就要回滚,倡议设置为@Transactional(rollbackFor = Throwable.class)

5)AOP 不失效问题

其余诸如 MyISAM 不反对,es 不反对等等就不一一列举了。

如果感兴趣,以上这些在源码中都能找到解答。

6、结束语

对于 Spring 的申明式事务,如果想用好,还真得多 Debug 几遍源码,因为 Spring 的源码细节过于丰盛,切实不适宜全副贴到文章里,倡议本人去跟一下源码。相熟之后就不怕再遇到生效状况了。

以下材料证实我不是在胡扯

1、文中测试用例代码:https://github.com/zhengxl556…

2、Spring 官网事务文档:https://docs.spring.io/spring…

3、Oracle 官网 JDBC 文档:https://docs.oracle.com/javas…

4、Spring Data Redis 源码:https://github.com/spring-pro…

往期原创干货

应用 Spring Validation 优雅地校验参数
下载的附件名总乱码?你该去读一下 RFC 文档了!
单例模式,关键字级别详解


原创码字不易,欢送点赞关注和分享。
我在公众号:Java 课代表,等你呦。

正文完
 0