Spring 声明式事务应该怎么学?

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!
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-console/ ,连接信息如下:

点击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/zhengxl5566/springboot-demo/tree/master/transactional

2、Spring 官网事务文档:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation

3、Oracle官网JDBC文档:https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html

4、Spring Data Redis 源码:https://github.com/spring-projects/spring-data-redis

推荐好文
主流Java进阶技术(学习资料分享)

最后

腾讯T3大牛总结的500页MySQL实战笔记意外爆火,P8看了直呼内行

腾讯T3大牛总结的500页MySQL实战笔记意外爆火,P8看了直呼内行
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

以下资料证明我不是在胡扯


1、文中测试用例代码:https://github.com/zhengxl5566/springboot-demo/tree/master/transactional

2、Spring 官网事务文档:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation

3、Oracle官网JDBC文档:https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html

4、Spring Data Redis 源码:https://github.com/spring-projects/spring-data-redis

推荐好文
主流Java进阶技术(学习资料分享)

最后

[外链图片转存中…(img-WsmrbnzB-1714752674458)]

[外链图片转存中…(img-FYWPuBfn-1714752674459)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

  • 27
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值