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


您可以使用本指南对Spring的事务管理(包括@Transactional批注)的工作方式进行深入的实际了解。
唯一的前提条件?您需要对ACID有一个大概的了解,即什么是数据库事务以及为什么要使用它们。此外,尽管仍然适用于Spring的一般原则,但这里也不介绍XATransactions或ReactiveTransactions。
Spring官方文档相反,本指南不会将您与诸如物理或逻辑事务之类的术语混淆。
相反,您将以非常规的方式学习Spring事务管理:从头开始,逐步进行。

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

Connection connection = dataSource.getConnection(); // (1)
try (connection) {
    connection.setAutoCommit(false);
// (2)
   
// execute some SQL statements...
    connection.commit();
// (3)
} catch (SQLException e) {
    connection.rollback();
// (4)
}

  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); // (1)

Savepoint savePoint = connection.setSavepoint();
// (2)
...
connection.rollback(savePoint);

  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="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name=
"get*" read-only="true"/>
            <!-- other methods use the default transaction settings (see below) -->
            <tx:method name=
"*"/>
        </tx:attributes>
    </tx:advice>

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

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

public class UserService {

    @Transactional
    public void registerUser(User user) {
     //...validate the user
     userDao.save(user);
    }
}

只要您在Spring配置上设置了@EnableTransactionManagement批注(以及配置了其他两个Bean-稍后将进行更多说明),就可以使用@Transactional批注对方法进行批注,并确保您的方法在数据库事务内执行。
“在数据库事务内部执行”的真正含义是什么?有了上一部分的知识,上面的代码直接转换(简化)为:

public class UserService {

    public void registerUser(User user) {
        Connection connection = dataSource.getConnection(); // (1)
        try (connection) {
            connection.setAutoCommit(false);
// (1)
           
//...validate the user
            userDao.save(user);
// (2)
            connection.commit();
// (1)
        } catch (SQLException e) {
            connection.rollback();
// (1)
        }
    }
}

  1. 这仅仅是JDBC连接的标准打开和关闭。请参阅上一节。这就是Spring的事务注释自动为您完成的,而无需您编写它。
  2. 这是您自己的代码,可以通过DAO保存用户。

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

代理
Spring不能像我上面那样真正重写您的Java类来插入连接代码。您的registerUser()方法实际上只是调用userDao.save(user),无法即时更改它。
但是Spring有一个优势。它的核心是一个IoC容器。它为您实例化一个UserService,并确保将该UserService自动连接到需要UserService的任何其他bean中。
现在,每当在bean上使用@Transactional时,Spring都会使用一个小技巧。它不仅实例化UserService,而且实例化该UserService的事务代理。让我们在图片中看到它。

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

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

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

@Configuration
@EnableTransactionManagement
public static class MyAppConfig {

    @Bean
    public UserService userService() {  // (1)
        return new UserService();
    }
}

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

PlatformTransactionManager
现在仅缺少一条关键信息。您的UserService会即时进行代理,并且代理会打开并为您关闭连接/事务。
但这意味着,代理需要一个数据源:要获得连接,提交,回滚,关闭连接等。在Spring中,有一个花哨的名称表示处理所有事务状态的接口,称为PlatformTransactionManager
有许多不同的实现,但是最简单的实现是DataSourceTransactionManager,并且凭借从简单的Java章节中学到的知识,您将确切知道它的作用。首先,让我们看一下Spring配置:

@Bean
public DataSource dataSource() {
    return null; // (1)
}

@Bean
public PlatformTransactionManager txManager() {
    return new DataSourceTransactionManager(dataSource());
// (2)
}

  1. 在此处返回null显然没有意义,但为简洁起见放在此处。您可创建MySQL,Postgres,Oracle或连接池数据源。
  2. 您在此处创建一个新的TxManager,它将获取您的DataSource。

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

总结一下:

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

@Transactional深度挖掘
现在,有两个有趣的用例,涉及到Spring的事务性注释。
让我们看一下“物理”与“逻辑”事务。

想象以下两个事务类。

@Service
public class UserService {

    @Autowired
    private InvoiceService invoiceService;

    @Transactional
    public void invoice() {
        invoiceService.createPdf();
        // send invoice as email, etc.
    }
}

@Service
public class InvoiceService {

    @Transactional
    public void createPdf() {
       
// ...
    }
}

UserService具有事务性invoice()方法。它将调用另一个事务方法InvoiceService上的createPdf()。
现在就数据库事务而言,这实际上应该仅仅是一个数据库事务。(请记住:getConnection().setAutocommit(false).commit())Spring调用了此物理事务,即使这听起来有些混乱。
从Spring的角度来看,事务有两个逻辑部分:第一个在UserService中,另一个在InvoiceService中。Spring必须足够聪明,才能知道两个@Transactional方法都应使用相同的基础数据库事务。
在InvoiceService进行以下更改之后,情况会有什么不同?

@Service
public class InvoiceService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
        // ...
    }
}

这意味着您的代码将打开与数据库的两个(物理)连接/事务。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
public class UserService {

    @Transactional
    public void invoice() {
        createPdf();
        // send invoice as email, etc.
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
       
// ...
    }
}

您有一个带有事务invoice()方法的UserService类。调用createPDF(),这也是事务性的。
一旦有人调用invoice(),您觉得会打开几各实际事务?
答案不是两个,而是一个。为什么?

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

让我们看一下图片:
有一些技巧(例如self-injection),您可以用来解决此限制。但是主要的收获是:始终牢记代理事务边界。

Spring + Hibernate事务管理如何工作
在某些时候,您将希望您的Spring应用程序与另一个数据库库集成,例如HibernateJooq等。让我们以Hibernate为例。
假设您有一个@Transactional Spring方法,并将其与DataSourcePlatformTransactionManager一起使用,就像上一节中讨论的那样。因此,Spring将为您打开和关闭该DataSource上的连接。
但是,如果您的代码调用了Hibernate,则Hibernate本身将最终调用其自己的SessionFactory来创建和关闭新会话(〜=连接)并在没有 Spring 的情况下管理它们的状态。因此,Hibernate不会知道任何现有的Spring事务。
有一个简单的解决方案(针对最终用户):您将在HibernateTransactionManagerJpaTransactionManager中使用,而不是在Spring配置中使用DataSourcePlatformTransactionManager

专门的HibernateTransactionManager将确保:

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

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

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