首页天道酬勤非事务方法调用事务方法,spring 注解实现原理

非事务方法调用事务方法,spring 注解实现原理

张世龙 05-06 01:12 8次浏览

前言

对于Java后端开发人员,Spring事务注释几乎每天都会接触。 但你真的知道Spring事务评论的所有细节吗? 今天,我们将详细介绍回滚Spring事务注释、传播行为和只读三个属性的配置调整。

本文希望了解更多与数据库事务相关的框架和数据库引擎的内部原理,并为数据库优化工作提供有用的建议。

详细信息1 :为什么要配置rollbackFor=Exception.class

一.回滚的作用

最近,阿里发布了其编码规范,就像IDE插件一样。 插件的检查结果为“在Spring事务中,必须设置rollbackFor属性或显式调用rollback方法”。 为什么需要设置rollbackFor属性?

简单来说,如果不设置rollbackFor=Exception.class,则在方法抛出检查类型异常时不会回滚数据库操作。

例如,在以下代码的情况下:

@Transactional

公共语音演示() throws Exception { )。

this.user repository.save (新用户(username ) );

throw new exception (无回滚);

}

执行完成后,user记录将添加到数据库中。 如果希望在方法抛出检查类型异常后触发数据库回滚,则必须写如下:

@ transactional (roll back for=exception.class ) )

公共语音演示() throws Exception { )。

this.user repository.save (新用户(username ) );

throw new exception (无回滚);

}

或者,使用transaction status.setrollbackonly ()手动触发回滚。

@Transactional

公共语音演示() {

this.user repository.save (新用户(username ) );

transactionaspectsupport.currenttransactionstatus ().setRollbackOnly );

}

如果您的方法不声明异常,而只抛出运行时异常,则无需设置回滚for。 在运行时抛出异常将触发事务回滚。

@Transactional

公共语音演示() {

this.user repository.save (新用户(username ) );

thrownewruntimeexception (回滚);

}

如果代码如上所示,则没有新的用户记录。 因为数据库操作已回滚。

二.不需要配置回滚

因为如果方法抛出检查类型异常,则必须在方法中声明异常。 因此,通常如果方法没有声明异常,则不需要将Spring事务注释设置为rollbackFor。

另外,在蚂蚁的Java编码规范中也没有找到rollbackFor的设置。 因此,我个人的意见是,方法必须抛出检查型异常,才能设置回滚。

三、Spring为什么这么设计

我认为Java异常处理的原则与Spring为什么不默认回滚检查型异常的问题有关。 原则上,检测型异常是业务异常流程的一部分,需要开发者定制开发相应的异常处理功能。 因为需要开发人员自定义,所以Spring认为不需要画蛇添足,并自动回滚数据库操作。

四.一般情况

如上所述,方法通常不声明异常,因此不需要为Spring事务注释设置rollbackFor。 但是,有“普通”的情况吗? 有答案。

原因在于Java检查型异常的实现原理。 实际上,在Java中try.catch检查型的异常完全是编译器检查的结果。 实际上,一些黑色技术可以向代码抛出检查型异常。 也不需要try.catch。

当然,一般开发者不会积极使用这些黑色技术。 但是,这并不意味着不间接使用。 无法捕获检查类型异常的一个常见原因是使用Java以外的JVM语言。

例如,在Groovy中,可以写以下内容:

@Component

class GroovyDemoService {

void withException (

throw new exception (anexpectedexception )。

}

}

这样写完全没有问题。 因为,Groovy为了简化差异

常使用,已经抛弃了 Java 检查型异常的设计。通常,这没什么问题。但如果这个方法在使用 Spring 事务注解声明的方法时,那就不会导致事务回滚,除非定义 rollbackFor = Exception.class。

那怎么避免这样的问题呢?我的一个建议是尽量使用 RuntimeException。使用运行时异常可以简化代码,也可以避免上述事务相关的问题。而 Spring 中定义的各种异常,绝大多数也都是运行时异常。

细节二:Propagation.SUPPORTS 有什么效果

一、Propagation.SUPPORTS 是干什么的?

在 Spring 事务的传播级别配置中,有一个选择是 Propagation.SUPPORTS。这个选择是什么意思呢?简单说就是在单独执行时(外层没有事务),这个方法将以非事务的方式执行。进一步地说,当 Spring 事务的传播级别被设置为 Propagation.SUPPORTS 时,当前 JDBC Connection 就不会配置为 autocommit=0,从而也就没有开启事务。

Support a current transaction, execute non-transactionally if none exists. Analogous to EJB transaction attribute of the same name. Note: For transaction managers with transaction synchronization, PROPAGATION_SUPPORTS is slightly different from no transaction at all, as it defines a transaction scope that synchronization will apply for. As a consequence, the same resources (JDBC Connection, Hibernate Session, etc) will be shared for the entire specified scope. Note that this depends on the actual synchronization configuration of the transaction manager.

二、要不要配置 Propagation.SUPPORTS?

在知道了 Propagation.SUPPORTS 所产生的效果之后,我们来看一下是否需要配置 Propagration.SUPPORTS,以及什么时候配置。

有一些数据库相关的优化建议提到,可以将 Spring 的事务配置为 Propagration.SUPPORTS,这样可以提高方法执行速度。原因在于,将一个方法的事务配置为 Propagration.SUPPORTS 后,如果单独调用这个方法,那这个方法就是按非事务的方式执行了。没有事务岂不是更快!

对于上述优化建议,我要说,结论不完全正确,原因完全不正确。

1. 反对使用 Propagation.SUPPORTS 的意见

我们先来看看对这个优化意见的反面看法。在 Spring JIRA 上也有人提出 Spring Data JPA 应该按照这样(将事务默认传播行为配置为 Propagration.SUPPORTS)的方式修改默认行为,以提高性能:https://jira.spring.io/browse/DATAJPA-601。

但是,在这个帖子中,Spring Data 项目的主管 Oliver Gierke 回答说“没有必要使用这种手段优化”。原因有二:

没有证据表明这样的方式能提高性能

默认的行为有很多优化手段

文中提到,有开发人员认为数据库操作可以以非事务的方式进行,但这是不可能的。所谓的非事务数据库访问只是没有显式的事务边界而已,数据库操作只是 auto-commit 的方式,其实还是有事务的。

进一步,如果数据操作以“非事务”方式进行,这便意味着在一个业务功能中,多个小的数据库事务被开启并关闭。这反而不利于性能,不如用一个完整的事务替代。

在伸缩性方面,Hibernate 这样的 ORM 框架对于写锁有优化,持续时间已经很短了。而读锁相对的性能消耗可以忽略。所以,在伸缩性方面,小事务不存在什么优势。

上面这些用来支持反对意见的证据,都没什么错。确实,开启 auto-commit 之后,事务还是有,只是变为了单语句级别。所以,不要认为 Propagration.SUPPORTS 之后就没有事务了。至少,如果把数据库的范围限定为使用 InnoDB 引擎的 MySQL,那 Propagration.SUPPORTS 并不会使数据库操作都以非事务的方式执行。

2. InnoDB 只读事务优化

在这里,剧情再次反转一下。因为对于多数互联网应用来说,数据库通常是 MySQL,搭配 InnoDB 引擎。从 MySQL 5.6 版本开始,InnoDB 增加了对只读事务的优化。这里有比较详细的描述 https://dev.mysql.com/doc/refman/5.6/en/innodb-performance-ro-txn.html。

简单来说,InnoDB 引擎可以避免为只读事务创建事务 ID,进而避免多余的事务和锁操作。那 InnoDB 什么时候会将一个事务视为只读事务呢?有两个情况:

通过 START TRANSACTION READ ONLY 显式开启只读事务时;

如果 autocommit=1,当调用一个普通的 select 操作(没有 FOR UPDATE/LOCK 的 select 语句)时;

对于第一点,因为 JDBC 开启事务并不是通过 START TRANSACTION 语句,所以这种方式与多数 Java 应用无关。但 Java 可以从第二点优化。当我们执行查询操作时,如果将当前连接设置为 autocommit=1,那 InnoDB 便会对这次查询操作进行只读事务优化。我们如何做呢?如果使用了 Spring,那将事务设置为 Propagation.SUPPORTS 即可。

3. 小结

将 Spring 事务的传播行为配置为 Propagation.SUPPORTS 并不意味着数据库不会以事务的方式执行语句,只是将事务细化到每个语句级别。但是,当使用 MySQL + InnoDB (MySQL 版本大于等于 5.6)执行查询操作时,因为 InnoDB 只读事务优化机制,所以将 Spring 事务设置为 Propagation.SUPPORTS 会得到更好的性能。

三、延伸:使用大事务的注意事项

上一段提到用大事务比一个个小的事务要更好,这么说自然很笼统。大事务有大事务的问题。我们一般而言,大事务是指在业务层(例如 Service 层)的方法上定义事务。

但是在微服务架构流行的今天,业务层方法中越来越多地引入了各种远程调用。例如,HTTP、RPC、MQ、缓存等等。这种情况下出现了一个问题就是,这些远程调用的超时问题,往往会导致数据库事务时间变长,从而导致数据库相关的种种问题。

解决这一问题有两个方法:

方法一:事务下沉

将原本定义在 Service 层的事务下沉到 Repository 层(这里的 Repository 层是领域驱动设计中的一个概念,这里不做过多阐述)。在 Repository 层中只有数据持久化的工作,避免了远程调用对数据库事务的影响。当然,这会导致一致性的问题。同样,因为篇幅的问题,不在这里深入讨论。

方法二:熔断降级保护

方法一可能会导致系统层次增加、方法粒度过细等一些问题。如果这些问题导致了代码复杂,可读性下降等问题,可以考虑使用熔断降级的方法,避免远程调用超时时间过长,从而导致影响波及数据库事务的问题。

两种都一定程度解决在微服务架构下大事务导致的问题。一般而言,并发高的业务偏向采用第一种方法,第二种方法可以用在偏向内部系统的场景中使用。当然,这只是大致的建议,更具体的场景还需具体情况具体分析。

细节三:readOnly = true 有什么效果

除了事务的传播级别,Spring 事务中另一个常见的配置是只读属性。@Transactional(readOnly = true) 便意味着这个事务是只读的。那当我们这么配置的时候,框架和数据库底层究竟做了哪些事?

JDBC 的 Connection 接口有一个 setReadOnly(boolean readOnly) 方法。当我们将 Spring 事务配置为 readOnly = true 时,这个方法便会被调用。在 MySQL 的 JDBC 驱动中,这个方法会向 MySQL 数据库下发 set session transaction read only 调用。这个有什么效果吗?MySQL 文档中并没有明确的说明。所以,具体的效果只能通过测试说话。(有哪位同学找到了相应的文档,或者阅读过 MySQL 的源码,可以帮忙解释一下)

测试和总结

说了这么多,我们来总结一下。对于 Spring 事务的 rollbackFor 配置,我建议择情选择。重点说一下和性能相关的两个细节问题,传播级别和只读事务。

前面说了,当使用 MySQL 5.6 + InnoDB 时,正确开启只读事务的姿势是将 Spring 事务的传播行为配置为 Propagation.SUPPORTS,而不是设置 readOnly = true。

当然,上面一直是在进行理论层面的讨论,实际如何呢?

经过测试,当使用 Spring Boot 1.5.8、JPA、MySQL 5.6.26 + InnoDB,按照 500 QPS 进行1万次基于索引的查询。得到的每次查询耗时为

SUPPORTS + readOnly: 每次查询耗时约 3 ms

仅 SUPPORTS: 每次查询耗时大约 3 ms

默认事务配置(不加 SUPPORTS 和 readOnly): 每次查询耗时大约 10 ms

仅 readOnly: 每次查询耗时大约 17 ms

所以,从测试结果来看,当时用 MySQL 5.6 + InnoDB 时,为 Spring 事务配置 Propagation.SUPPORTS 对读操作的性能提升最大;readOnly = true 不但没有性能提升,反而会造成性能下降,原因可能和额外的 set session transaction read only 操作有关。

因为 Spring、Hibernate、JDBC 等技术面向的广泛的数据库技术,所以并不会针对 MySQL + InnoDB 做专门的优化配置。但是,作为一个企业或一个团队,可以自主选择优化方向。如果有必要将上述优化建议落地普及,可以考虑使用自定义 Spring 事务注解,修改其默认传播行为等属性,避免开发人员按照不合适的方式进行配置。关于如何自定义 Spring 注解,就不在这里过多阐述了。

spring的事务是如何配置的,什么是编程式事务管理