您可以使用本指南对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>
是的,只要您使用@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>
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>
这个示例可能看起来有些简化,但是让我们看一下Spring如何神奇地为您插入此连接/事务代码。
代理
Spring不能像我上面那样真正重写您的Java类来插入连接代码。您的registerUser()方法实际上只是调用userDao.save(user),无法即时更改它。
但是Spring有一个优势。它的核心是一个IoC容器。它为您实例化一个UserService,并确保将该UserService自动连接到需要UserService的任何其他bean中。
现在,每当在bean上使用@Transactional时,Spring都会使用一个小技巧。它不仅实例化UserService,而且实例化该UserService的事务代理。让我们在图片中看到它。
从该图中可以看到,代理只有一项工作。
快速考试 看看下面的源代码,并告诉我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>
因此,让我们从上面扩展图片:
总结一下:
@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方法中。
解释:
如您所见,大多数传播模式实际上与数据库或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类并调用其他内部方法,就不再涉及代理:没有新事务可供您使用。
让我们看一下图片:
有一些技巧(例如 self-injection ),您可以用来解决此限制。但是主要的收获是:始终牢记代理事务边界。
Spring + Hibernate事务管理如何工作
在某些时候,您将希望您的Spring应用程序与另一个数据库库集成,例如 Hibernate , Jooq 等。让我们以Hibernate为例。
假设您有一个@Transactional Spring方法,并将其与DataSourcePlatformTransactionManager一起使用,就像上一节中讨论的那样。因此,Spring将为您打开和关闭该DataSource上的连接。
但是,如果您的代码调用了Hibernate,则Hibernate本身将最终调用其自己的SessionFactory来创建和关闭新会话(〜=连接)并在没有 Spring 的情况下管理它们的状态。因此,Hibernate不会知道任何现有的Spring事务。
有一个简单的解决方案(针对最终用户):您将在 HibernateTransactionManager 或 JpaTransactionManager中 使用,而不是在Spring配置中使用 DataSourcePlatformTransactionManager 。
专门的HibernateTransactionManager将确保:
与往常一样,图片可能更容易理解(不过请注意,代理和真实服务之间的流在概念上只是正确的,而且过于简化)。
简而言之,就是如何集成Spring和Hibernate。对于其他集成或更深入的了解,有助于快速查看Spring提供的所有可能的 PlatformTransactionManager 实现。