转载

Spring事务管理:非常规指南 - marcobehler

您可以使用本指南对Spring的事务管理(包括@Transactional批注)的工作方式进行深入的实际了解。

唯一的前提条件?您需要对ACID有一个大概的了解,即什么是数据库事务以及为什么要使用它们。此外,尽管仍然适用于Spring的一般原则,但这里也不介绍XATransactions或ReactiveTransactions。

与 Spring官方文档 相反,本指南不会将您与诸如物理或逻辑事务之类的术语混淆。

相反,您将以非常规的方式学习Spring事务管理:从头开始,逐步进行。

开始,提交和回滚事务

无论您使用的是Spring的@Transactional批注,纯Hibernate,jOOQ还是任何其他数据库库,都没有关系。最后,它们都在打开和关闭数据库事务中做同样的事情,就是:

Connection connection = dataSource.getConnection(); <font><i>// (1)</i></font><font>
<b>try</b> (connection) {
    connection.setAutoCommit(false); </font><font><i>// (2)</i></font><font>
    </font><font><i>// execute some SQL statements...</i></font><font>
    connection.commit(); </font><font><i>// (3)</i></font><font>
} <b>catch</b> (SQLException e) {
    connection.rollback(); </font><font><i>// (4)</i></font><font>
}
</font>
  1. 您显然需要与数据库的连接。尽管在大多数企业级应用程序中,您将配置一个DataSource并从中获取连接,但DriverManager.getConnection(...)也可以正常工作。
  2. 这是在Java中启动数据库事务的唯一方法,即使该名称听起来有些奇怪。 AutoCommit(true)在其自己的事务中包装每个SQL语句,而AutoCommit(false)相反:您是事务的主控者。
  3. 让我们进行交易...
  4. 或回滚我们的更改(如果有例外)。

是的,只要您使用@Transactional批注,这4行就简化了,Spring帮你做了这一切。在下一章中,您将了解其工作原理。 HikariCP之 类的连接池库可能会根据配置自动为您切换自动提交模式。但这是另一个主题。

保存点和隔离级别

如果您已经使用过Spring的@Transactional批注,那么您可能会遇到以下情况:

@Transactional(propagation=TransactionDefinition.NESTED,
               isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED)

稍后我们将介绍嵌套的Spring事务和隔离级别,但是再次帮助您了解这些参数到底归结为以下JDBC代码:

connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); <font><i>// (1)</i></font><font>

Savepoint savePoint = connection.setSavepoint(); </font><font><i>// (2)</i></font><font>
...
connection.rollback(savePoint);
</font>
  1. 这就是Spring如何在数据库连接上设置隔离级别。不完全是火箭科学,是吗?
  2. Spring中的嵌套事务实际上只是JDBC保存点。如果您不知道保存点是什么,请查看 本教程 。请注意,保存点支持取决于您的JDBC驱动程序/数据库。

Spring中的事务管理如何工作

现在您有了一个良好的JDBC事务基础,让我们看一下Spring。

旧版事务管理:XML

过去,当XML配置成为Spring项目的规范时,您还可以直接在XML中配置事务。除了几个遗留的企业项目,您将再也找不到这种方法了,因为它已经被更简单的@Transactional注释所取代。

因此,我们将在本指南中跳过XML配置,但是可以快速浏览一下它的外观(直接取自 Spring官方文档 ):

<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
    <tx:advice id=<font>"txAdvice"</font><font> transaction-manager=</font><font>"txManager"</font><font>>
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name=</font><font>"get*"</font><font> read-only=</font><font>"true"</font><font>/>
            <!-- other methods use the <b>default</b> transaction settings (see below) -->
            <tx:method name=</font><font>"*"</font><font>/>
        </tx:attributes>
    </tx:advice>
</font>

Spring使用AOP(面向方面​​的编程)来进行事务处理。您可以在 Spring官方文档中 了解有关AOP的更多信息。

Spring的@Transactional注释

现在让我们看一下现代Spring事务管理通常是什么样的:

<b>public</b> <b>class</b> UserService {

    @Transactional
    <b>public</b> <b>void</b> registerUser(User user) {
     <font><i>//...validate the user</i></font><font>
     userDao.save(user);
    }
}
</font>

只要您在Spring配置上设置了@EnableTransactionManagement批注(以及配置了其他两个Bean-稍后将进行更多说明),就可以使用@Transactional批注对方法进行批注,并确保您的方法在数据库事务内执行。

“在数据库事务内部执行”的真正含义是什么?有了上一部分的知识,上面的代码直接转换(简化)为:

<b>public</b> <b>class</b> UserService {

    <b>public</b> <b>void</b> registerUser(User user) {
        Connection connection = dataSource.getConnection(); <font><i>// (1)</i></font><font>
        <b>try</b> (connection) {
            connection.setAutoCommit(false); </font><font><i>// (1)</i></font><font>
            </font><font><i>//...validate the user</i></font><font>
            userDao.save(user); </font><font><i>// (2)</i></font><font>
            connection.commit(); </font><font><i>// (1)</i></font><font>
        } <b>catch</b> (SQLException e) {
            connection.rollback(); </font><font><i>// (1)</i></font><font>
        }
    }
}
</font>
  1. 这仅仅是JDBC连接的标准打开和关闭。请参阅上一节。这就是Spring的事务注释自动为您完成的,而无需您编写它。
  2. 这是您自己的代码,可以通过DAO保存用户。

这个示例可能看起来有些简化,但是让我们看一下Spring如何神奇地为您插入此连接/事务代码。

代理

Spring不能像我上面那样真正重写您的Java类来插入连接代码。您的registerUser()方法实际上只是调用userDao.save(user),无法即时更改它。

但是Spring有一个优势。它的核心是一个IoC容器。它为您实例化一个UserService,并确保将该UserService自动连接到需要UserService的任何其他bean中。

现在,每当在bean上使用@Transactional时,Spring都会使用一个小技巧。它不仅实例化UserService,而且实例化该UserService的事务代理。让我们在图片中看到它。

Spring事务管理:非常规指南 - marcobehler

从该图中可以看到,代理只有一项工作。

  • 打开和关闭数据库连接/事务。
  • 然后委托给真正的UserService。
  • 而其他的bean,例如UserRestController,将永远不会知道它们正在与代理对话,而不是真实的对话。

快速考试 看看下面的源代码,并告诉我Spring会自动构造什么样的UserService,假设它已标有@Transactional或具有@Transactional方法。

@Configuration
@EnableTransactionManagement
<b>public</b> <b>static</b> <b>class</b> MyAppConfig {

    @Bean
    <b>public</b> UserService userService() {  <font><i>// (1)</i></font><font>
        <b>return</b> <b>new</b> UserService();
    }
}
</font>

Spring在这里构造了UserService类的动态代理,可以为您打开和关闭数据库事务。在 Cglib库 的帮助下通过子类代理。还有其他构造代理的方法,但现在暂时不做介绍。

PlatformTransactionManager

现在仅缺少一条关键信息。您的UserService会即时进行代理,并且代理会打开并为您关闭连接/事务。

但这意味着,代理需要一个数据源:要获得连接,提交,回滚,关闭连接等。在Spring中,有一个花哨的名称表示处理所有事务状态的接口,称为 PlatformTransactionManager 。

有许多不同的实现,但是最简单的实现是DataSourceTransactionManager,并且凭借从简单的Java章节中学到的知识,您将确切知道它的作用。首先,让我们看一下Spring配置:

@Bean
<b>public</b> DataSource dataSource() {
    <b>return</b> <b>null</b>; <font><i>// (1)</i></font><font>
}

@Bean
<b>public</b> PlatformTransactionManager txManager() {
    <b>return</b> <b>new</b> DataSourceTransactionManager(dataSource()); </font><font><i>// (2)</i></font><font>
}
</font>
  1. 在此处返回null显然没有意义,但为简洁起见放在此处。您可创建MySQL,Postgres,Oracle或连接池数据源。
  2. 您在此处创建一个新的TxManager,它将获取您的DataSource。

因此,让我们从上面扩展图片:

Spring事务管理:非常规指南 - marcobehler

总结一下:

  1. 如果Spring在bean上检测到@Transactional,它将创建该bean的动态代理。
  2. 代理有权访问TransactionManager,并要求其打开和关闭事务/连接。
  3. TransactionManager本身将简单地执行您在普通Java部分中所做的事情:“操纵” JDBC连接。

@Transactional深度挖掘

现在,有两个有趣的用例,涉及到Spring的事务性注释。

让我们看一下“物理”与“逻辑”事务。

想象以下两个事务类。

@Service
<b>public</b> <b>class</b> UserService {

    @Autowired
    <b>private</b> InvoiceService invoiceService;

    @Transactional
    <b>public</b> <b>void</b> invoice() {
        invoiceService.createPdf();
        <font><i>// send invoice as email, etc.</i></font><font>
    }
}

@Service
<b>public</b> <b>class</b> InvoiceService {

    @Transactional
    <b>public</b> <b>void</b> createPdf() {
        </font><font><i>// ...</i></font><font>
    }
}
</font>

UserService具有事务性invoice()方法。它将调用另一个事务方法InvoiceService上的createPdf()。

现在就数据库事务而言,这实际上应该仅仅是一个数据库事务。(请记住:getConnection().setAutocommit(false).commit())Spring调用了此物理事务,即使这听起来有些混乱。

从Spring的角度来看,事务有两个逻辑部分:第一个在UserService中,另一个在InvoiceService中。Spring必须足够聪明,才能知道两个@Transactional方法都应使用相同的基础数据库事务。

在InvoiceService进行以下更改之后,情况会有什么不同?

@Service
<b>public</b> <b>class</b> InvoiceService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    <b>public</b> <b>void</b> createPdf() {
        <font><i>// ...</i></font><font>
    }
}
</font>

这意味着您的代码将打开与数据库的两个(物理)连接/事务。Spring现在足够聪明,两个逻辑事务块(invoice()/ createPdf())现在也映射到两个不同的数据库事务。

传播方式

查看Spring源代码时,您会发现各种传播模式,可以将它们插入@Transactional方法中。

  • REQUIRED(0)
  • SUPPORTS(1)
  • MANDATORY(2)
  • REQUIRES_NEW(3)
  • NOT_SUPPORTED(4)
  • NEVER(5)
  • NESTED(6)

解释:

  • REQUIRED(默认):我的方法需要一个事务,要么为我打开一个事务,要么使用现有的事务:getConnection().setAutocommit(false)commit()。
  • SUPPORTS:我并不真正在乎事务是否打开,我可以以任何一种方式工作:与JDBC无关
  • MANDATORY强制性的:我不会自己打开一个事务,但是如果没有人打开一个事务,我会哭泣,与JDBC无关
  • Require_new:我要我完全拥有的事务: getConnection().setAutocommit(false)commit()。
  • Not_Supported:我真的不喜欢事务,我会尝试挂起当前正在运行的事务,与JDBC无关
  • NEVER:如果其他人启动了事务,我会哭泣→与JDBC无关
  • NESTED:听起来很复杂,但实际上我们只是在谈论保存点!: connection.setSavepoint()

如您所见,大多数传播模式实际上与数据库或JDBC无关,而与您如何使用Spring构建程序的方式以及期望交易的方式/时间/位置有关。

隔离等级

当您像这样配置@Transactional批注时会发生什么:

@Transactional(isolation = Isolation.REPEATABLE_READ)

导致以下结果:

connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

您可以 在此处 阅读有关隔离级别的更多信息 , 但建议您在使用隔离级别甚至在事务中切换隔离级别时,必须确保咨询JDBC驱动程序/数据库,以了解受支持的内容和不支持的内容。

最常见的@Transactional陷阱

Spring初学者通常会遇到一个陷阱。看下面的代码:

@Service
<b>public</b> <b>class</b> UserService {

    @Transactional
    <b>public</b> <b>void</b> invoice() {
        createPdf();
        <font><i>// send invoice as email, etc.</i></font><font>
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    <b>public</b> <b>void</b> createPdf() {
        </font><font><i>// ...</i></font><font>
    }
}
</font>

您有一个带有事务invoice()方法的UserService类。调用createPDF(),这也是事务性的。

一旦有人调用invoice(),您觉得会打开几各实际事务?

答案不是两个,而是一个。为什么?

让我们回到本指南的代理部分。Spring为您注入了该事务代理,但是一旦您进入UserService类并调用其他内部方法,就不再涉及代理:没有新事务可供您使用。

让我们看一下图片:

Spring事务管理:非常规指南 - marcobehler 有一些技巧(例如 self-injection ),您可以用来解决此限制。但是主要的收获是:始终牢记代理事务边界。

Spring + Hibernate事务管理如何工作

在某些时候,您将希望您的Spring应用程序与另一个数据库库集成,例如 Hibernate , Jooq 等。让我们以Hibernate为例。

假设您有一个@Transactional Spring方法,并将其与DataSourcePlatformTransactionManager一起使用,就像上一节中讨论的那样。因此,Spring将为您打开和关闭该DataSource上的连接。

但是,如果您的代码调用了Hibernate,则Hibernate本身将最终调用其自己的SessionFactory来创建和关闭新会话(〜=连接)并在没有 Spring 的情况下管理它们的状态。因此,Hibernate不会知道任何现有的Spring事务。

有一个简单的解决方案(针对最终用户):您将在 HibernateTransactionManager 或 JpaTransactionManager中 使用,而不是在Spring配置中使用 DataSourcePlatformTransactionManager 。

专门的HibernateTransactionManager将确保:

  1. 通过Hibernate(即SessionFactory)打开/关闭连接/事务
  2. 足够聪明,可以让您在非休眠状态(即纯JDBC代码)中使用相同的连接/事务

与往常一样,图片可能更容易理解(不过请注意,代理和真实服务之间的流在概念上只是正确的,而且过于简化)。

Spring事务管理:非常规指南 - marcobehler

简而言之,就是如何集成Spring和Hibernate。对于其他集成或更深入的了解,有助于快速查看Spring提供的所有可能的 PlatformTransactionManager 实现。

原文  https://www.jdon.com/53359
正文到此结束
Loading...