[译]深入了解Spring事务管理:@Transactional

您可以使用本指南来简单实用地了解 Spring 使用 @Transactional 注释进行事务管理的工作原理。

唯一的先决条件?您需要对 ACID 有一个粗略的了解,即什么是数据库事务以及为什么使用它们。此外,这里不讨论分布式事务或反应式事务,但就 Spring 而言,一般原则仍然适用。

介绍

在本指南中,您将了解 Spring 核心事务抽象框架的主要支柱(这是一个令人困惑的术语,不是吗?) - 用大量代码示例进行描述:

  • @Transactional (声明式事务管理)与编程式事务管理。
  • 物理事务与逻辑事务。
  • Spring @Transactional 与 JPA/Hibernate 集成。
  • Spring @Transactional 与 Spring Boot 或 Spring MVC 集成。
  • 回滚、代理、常见陷阱等等。

与 Spring 官方文档相反,本指南不会直接深入探讨 Spring-first 主题,从而让您感到困惑。

相反,您将以非常规的方式学习 Spring 事务管理:从头开始,一步一步。这意味着,从普通的老式 JDBC 事务管理开始。

Why? 为什么?

因为 Spring 所做的一切都是基于这些 JDBC 基础知识。如果您掌握了这些基础知识,稍后您将可以使用 Spring 的 @Transactional 注释节省大量时间。

普通 JDBC 事务管理的工作原理

如果您在不完全了解 JDBC 事务的情况下考虑跳过本节:请不要这样做。

如何启动、提交或回滚 JDBC 事务

第一个重要的收获是:无论您使用 Spring 的 @Transactional 注释、普通 Hibernate、jOOQ 还是任何其他数据库库,都没有关系。

最后,它们都执行相同的操作来打开和关闭(我们称之为“管理”)数据库事务。普通的 JDBC 事务管理代码如下所示:

import java.sql.Connection;

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. 您需要连接到数据库才能启动事务。 DriverManager.getConnection(url,user,password)也可以工作,尽管在大多数企业应用程序中,您将配置一个数据源并从中获取连接。
  2. 这是在 Java 中“启动”数据库事务的唯一方法,尽管这个名字听起来有点不对劲。 setAutoCommit(true) 确保每个 SQL 语句自动包装在自己的事务中,而 setAutoCommit(false) 则相反:您是事务的主人,您需要开始调用 *commit* 标志在连接打开的整个时间内都有效,这意味着您只需调用该方法一次,而不是重复。
  3. 让我们提交我们的交易…​
  4. 或者,如果出现异常,则回滚我们的更改。

是的,这 4 行(过于简单化的)是当你使用 @Transactional 注释时 Spring 所做的一切。在下一章中,您将了解其工作原理。但在我们开始之前,您还需要学习一些知识。

(聪明人的一个快速说明:像 HikariCP 这样的连接池库可能会根据配置自动为您切换自动提交模式。但这是一个高级主题。)

如何使用 JDBC 隔离级别和保存点

如果您已经使用过 Spring 的 @Transactional 注释,您可能遇到过类似的情况:

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

稍后我们将更详细地介绍嵌套 Spring 事务和隔离级别,但再次了解这些参数都可以归结为以下基本 JDBC 代码会有所帮助:

import java.sql.Connection;

// isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED

connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // (1)

// propagation=TransactionDefinition.NESTED

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

Spring 或 Spring Boot 的事务管理如何工作

现在您已经对 JDBC 事务有了很好的了解,让我们看看简单的、核心的 Spring 是如何管理事务的。这里的所有内容都一一适用于 Spring Boot 和 Spring MVC,稍后会详细介绍。

Spring 的事务管理或其(名称相当令人困惑的)事务抽象框架实际上是什么?

请记住,事务管理简单地说就是:Spring 如何启动、提交或回滚 JDBC 事务?这听起来是不是很熟悉?

问题是:使用普通 JDBC,您只有一种方法 (setAutocommit(false)) 来管理事务,而 Spring 为您提供了许多不同的、更方便的方法来实现相同的目的。

如何使用 Spring 的程序化事务管理?

在 Spring 中定义事务的第一种但很少使用的方法是通过编程方式:通过 TransactionTemplate 或直接通过 PlatformTransactionManager。从代码角度来看,它看起来像这样:

@Service
public class UserService {

    @Autowired
    private TransactionTemplate template;

    public Long registerUser(User user) {
        Long id = template.execute(status ->  {
            // execute some SQL that e.g.
            // inserts the user into the db and returns the autogenerated id
            return id;
        });
    }
}

与普通 JDBC 示例相比:

  • 您不必自己搞乱打开或关闭数据库连接(try-finally)。相反,您使用事务回调。
  • 您也不必捕获 SQLException,因为 Spring 会为您将这些异常转换为运行时异常。
  • 并且您可以更好地融入 Spring 生态系统。 TransactionTemplate 将在内部使用 TransactionManager,它将使用数据源。所有这些都是您必须在 Spring 上下文配置中指定的 bean,但以后不必再担心。

虽然这算是一个小小的改进,但程序化事务管理并不是 Spring 事务框架的主要功能。相反,这都是关于声明式事务管理的。让我们看看那是什么。

如何使用 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>

您使用上面的 XML 块指定 AOP 建议(面向方面 ​​ 的编程),然后可以将其应用到 UserService bean,如下所示:

<aop:config>
    <aop:pointcut id="userServiceOperation" expression="execution(* x.y.service.UserService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="userServiceOperation"/>
</aop:config>

<bean id="userService" class="x.y.service.UserService"/>

您的 UserService bean 将如下所示:

public class UserService {

    public Long registerUser(User user) {
        // execute some SQL that e.g.
        // inserts the user into the db and retrieves the autogenerated id
        return id;
    }
}

从 Java 代码的角度来看,这种声明式事务方法看起来比编程方法简单得多。但它会导致大量复杂、冗长的 XML,以及切入点和顾问程序配置。

因此,这就引出了一个问题:是否有比 XML 更好的声明式事务管理方法?是的,有:@Transactional 注释。

如何使用 Spring 的@Transactional 注解(声明式事务管理)

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

public class UserService {

    @Transactional
    public Long registerUser(User user) {
       // execute some SQL that e.g.
        // inserts the user into the db and retrieves the autogenerated id
        // userDao.save(user);
        return id;
    }
}

这怎么可能?不再需要 XML 配置,也不需要其他代码。相反,您现在需要做两件事:

  • 确保您的 Spring 配置带有 @EnableTransactionManagement 注释(在 Spring Boot 中,这将自动为您完成)。
  • 确保您在 Spring 配置中指定了事务管理器(无论如何您都需要这样做)。
  • 然后 Spring 足够智能,可以透明地为您处理事务:您用 @Transactional 注释注释的任何 bean 的公共方法都将在数据库事务内执行(注意:存在一些陷阱)。

因此,要使 @Transactional 注释正常工作,您需要做的就是:

@Configuration
@EnableTransactionManagement
public class MySpringConfig {

    @Bean
    public PlatformTransactionManager txManager() {
        return yourTxManager; // more on that later
    }

}

现在,当我说 Spring 透明地为您处理事务时。那个的真实意义是什么?

借助 JDBC 事务示例中的知识,上面的 @Transactional UserService 代码可以直接转换(简化)为:

public class UserService {

    public Long registerUser(User user) {
        Connection connection = dataSource.getConnection(); // (1)
        try (connection) {
            connection.setAutoCommit(false); // (1)

            // execute some SQL that e.g.
            // inserts the user into the db and retrieves the autogenerated id
            // userDao.save(user); <(2)

            connection.commit(); // (1)
        } catch (SQLException e) {
            connection.rollback(); // (1)
        }
    }
}
  1. 这只是 JDBC 连接的标准打开和关闭。这就是 Spring 的事务注释自动为您做的事情,而无需您显式地编写它。
  2. 这是您自己的代码,通过 DAO 或类似的东西保存用户。

这个例子可能看起来有点神奇,但是让我们看看 Spring 如何为您插入这个连接代码。

CGlib 和 JDK 代理 - @Transactional 的幕后黑手

Spring 无法真正重写您的 Java 类,就像我上面所做的那样,以插入连接代码(除非您使用字节码编织等高级技术,但我们现在忽略它)。

你的 registerUser() 方法实际上只是调用 userDao.save(user) ,没有办法动态改变它。

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

现在,每当您在 bean 上使用 @Transactional 时,Spring 都会使用一个小技巧。它不仅实例化 UserService,而且还实例化该 UserService 的事务代理。

它在 Cglib 库的帮助下通过一种称为代理通过子类化的方法来实现这一点。还有其他构建代理的方法(例如动态 JDK 代理),但我们暂时就这样吧。

让我们看看这张图中代理的作用:

document1

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

  • 打开和关闭数据库连接/事务。
  • 然后委托给真正的 UserService,即您编写的那个。
  • 而其他 bean,例如 UserRestController,永远不会知道它们正在与代理通信,而不是与真实的事物通信。

快速考试

看一下下面的源代码,告诉我 Spring 自动构造什么类型的 UserService,假设它带有 @Transactional 标记或具有 @Transactional 方法。

@Configuration
@EnableTransactionManagement
public static class MyAppConfig {

    @Bean
    public UserService userService() {  // (1)
        return new UserService();
    }
}
  1. 正确的。 Spring 在这里为您的 UserService 类构造一个动态 CGLib 代理,它可以为您打开和关闭数据库事务。您或任何其他 bean 甚至不会注意到它不是您的 UserService,而是包装您的 UserService 的代理。

为什么需要事务管理器(如 PlatformTransactionManager)?

现在只缺少一条关键信息,尽管我们已经提到过几次了。

您的 UserService 会动态被代理,并且代理会为您管理事务。但处理所有事务状态(打开、提交、关闭)的不是代理本身,而是代理委托给事务管理器。

Spring 为您提供了 PlatformTransactionManager / TransactionManager 接口,默认情况下,它附带了一些方便的实现。其中之一是数据源事务管理器。

它所做的正是您迄今为止所做的管理事务的操作,但首先,让我们看看所需的 Spring 配置:

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

@Bean
public PlatformTransactionManager txManager() {
    return new DataSourceTransactionManager(dataSource()); // (2)
}
  1. 您可以在此处创建特定于数据库或特定于连接池的数据源。本示例使用 MySQL。
  2. 在这里,您创建事务管理器,它需要一个数据源才能管理事务。

简单如。然后,所有事务管理器都有像“doBegin”(用于启动事务)或“doCommit”这样的方法,它们看起来像这样——直接取自 Spring 的源代码并进行了一些简化:

public class DataSourceTransactionManager implements PlatformTransactionManager {

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        Connection newCon = obtainDataSource().getConnection();
        // ...
        con.setAutoCommit(false);
        // yes, that's it!
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        // ...
        Connection connection = status.getTransaction().getConnectionHolder().getConnection();
        try {
            con.commit();
        } catch (SQLException ex) {
            throw new TransactionSystemException("Could not commit JDBC transaction", ex);
        }
    }
}

因此,数据源事务管理器在管理事务时使用与您在 JDBC 部分中看到的完全相同的代码。

考虑到这一点,让我们从上面扩展我们的图片:

document2

总结一下:

  1. 如果 Spring 检测到 bean 上的 @Transactional 注释,它会创建该 bean 的动态代理。
  2. 代理可以访问事务管理器,并要求它打开和关闭事务/连接。
  3. 事务管理器本身将简单地执行您在普通 Java 部分中所做的操作:管理良好的旧 JDBC 连接。

物理事务和逻辑事务有什么区别?

想象一下以下两个事务类。

@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() {
        // ...
    }
}

将传播模式更改为 require_new 是告诉 Spring createPDF() 需要在自己的事务中执行,独立于任何其他已存在的事务。回想一下本指南的纯 Java 部分,您是否看到了将事务“拆分”为两半的方法?我也不。

这基本上意味着您的代码将打开两个到数据库的(物理)连接/事务。 (再次:getConnection() x2.setAutocommit(false) x2.commit() x2)Spring 现在必须足够智能,两个逻辑事务部分 (invoice()/createPdf()) 现在也映射到两个不同的物理数据库交易。

所以,总结一下:

  • 物理事务:是您实际的 JDBC 事务。
  • 逻辑事务:是(可能嵌套的)@Transactional 注解的(Spring)方法。

这使我们能够更详细地介绍传播模式。

@Transactional 传播级别的用途是什么?

查看 Spring 源代码时,您会发现可以插入 @Transactional 方法的各种传播级别或模式。

  @Transactional(propagation = Propagation.REQUIRED)

  // or

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  // etc

完整列表:

  • REQUIRED 必需的
  • SUPPORTS 支持
  • MANDATORY 强制的
  • REQUIRES_NEW REQUIRES_NEW
  • NOT_SUPPORTED 不支持
  • NEVER 绝不
  • NESTED 嵌套

练习:

在普通 Java 部分中,我向您展示了 JDBC 在事务方面可以执行的所有操作。花点时间思考一下每个 Spring 传播模式最终对您的数据源(或者更确切地说,您的 JDBC 连接)到底做了什么。

那就看看下面的回答吧。

答案:

  • Required (default):我的方法需要一个事务,要么为我打开一个事务,要么使用现有的事务 → getConnection()。设置自动提交(假)。犯罪()。
  • Supports:我并不关心事务是否打开,我可以以任何一种方式工作 → 与 JDBC 无关
  • Mandatory:我自己不会打开一个事务,但是如果没有人打开一个事务我会哭 → 与 JDBC 无关
  • Require_new:我想要完全自己的事务 → getConnection()。设置自动提交(假)。犯罪()。
  • Not_Supported:我真的不喜欢事务,我什至会尝试挂起当前正在运行的事务 → 与 JDBC 无关
  • **Never:**如果其他人启动事务我会哭 → 与 JDBC 无关
  • Nested: 听起来很复杂,但我们只是在谈论保存点! → 连接.setSavepoint()

正如您所看到的,大多数传播模式实际上与数据库或 JDBC 无关,而更多地与您如何使用 Spring 构建程序以及 Spring 期望事务出现的方式/时间/地点有关。

看这个例子:

public class UserService {

     @Transactional(propagation = Propagation.MANDATORY)
     public void myMethod() {
        // execute some sql
     }

}

在这种情况下,每当您调用 UserService 类的 myMethod() 时,Spring 都会期望打开一个事务。它本身不会打开一个方法,相反,如果您在没有预先存在的事务的情况下调用该方法,Spring 将抛出异常。请记住这一点,作为“逻辑事务处理”的附加点。

@Transactional 隔离级别有什么用?

此时这几乎是一个棘手的问题,但是当您像这样配置 @Transactional 注释时会发生什么?

@Transactional(isolation = Isolation.REPEATABLE_READ)

是的,它确实会导致这样的结果:

connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

然而,数据库隔离级别是一个复杂的主题,您应该花一些时间来完全掌握它们。一个好的开始是 Postgres 官方文档及其有关隔离级别的部分。

另请注意,在事务期间切换隔离级别时,您必须确保咨询 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() {
        // ...
    }
}

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

一旦有人调用 invoice(),您期望打开多少实际交易?

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

让我们回到本指南的代理部分。 Spring 为您创建事务性 UserService 代理,但是一旦您进入 UserService 类并调用其他内部方法,就不再涉及代理。这意味着,您没有新的交易。

我们用一张图来看看:

document3

您可以使用一些技巧(例如自注入)来绕过此限制。但主要的要点是:始终牢记代理事务边界。

如何在 Spring Boot 或 Spring MVC 中使用 @Transactional

到目前为止,我们只讨论了简单的、核心的 Spring。但是 Spring Boot 呢?还是 Spring Web MVC?他们处理交易的方式有什么不同吗?

最简洁的答案是不。

对于任一框架(或者更确切地说:Spring 生态系统中的所有框架),您将始终使用 *@Transactional* 注释,并结合事务管理器和 @EnableTransactionManagement 注释。没有其他办法。

然而,与 Spring Boot 的唯一区别是,它使用 JDBC 自动配置自动设置 *@EnableTransactionManagement* 注释并为您创建 *PlatformTransactionManager* 。在此处了解有关自动配置的更多信息。

Spring 如何处理回滚(以及默认回滚策略)

有关 Spring 回滚的部分将在本指南的下一版本中处理。

Spring 和 JPA/Hibernate 事务管理如何工作

目标:同步 Spring 的 @Transactional 和 Hibernate / JPA

在某些时候,您会希望 Spring 应用程序与另一个数据库库集成,例如 Hibernate(一种流行的 JPA 实现)或 Jooq 等。

让我们以普通 Hibernate 为例(注意:直接使用 Hibernate,或者通过 JPA 使用 Hibernate 并不重要)。

将之前的 UserService 重写为 Hibernate 如下所示:

public class UserService {

    @Autowired
    private SessionFactory sessionFactory; // (1)

    public void registerUser(User user) {

        Session session = sessionFactory.openSession(); // (2)

        // lets open up a transaction. remember setAutocommit(false)!
        session.beginTransaction();

        // save == insert our objects
        session.save(user);

        // and commit it
        session.getTransaction().commit();

        // close the session == our jdbc connection
        session.close();
    }
}
  1. 这是一个普通的、旧的 Hibernate SessionFactory,是所有 Hibernate 查询的入口点。
  2. 使用 Hibernate API 手动管理会话(即:数据库连接)和事务。

然而,上面的代码有一个很大的问题:

  • Hibernate 不会知道 Spring 的 @Transactional 注释。
  • Spring 的 @Transactional 对 Hibernate 的事务一无所知。

但我们实际上希望 Spring 和 Hibernate 能够无缝集成,这意味着它们了解彼此的事务。

用纯代码表示:

@Service
public class UserService {

    @Autowired
    private SessionFactory sessionFactory; // (1)

    @Transactional
    public void registerUser(User user) {
        sessionFactory.getCurrentSession().save(user); // (2)
    }

}
  1. 与之前相同的 SessionFactory
  2. 但不再需要手动状态管理。相反, getCurrentSession() 和 @Transactional 是同步的。

到那里怎么走?

使用 HibernateTransactionManager

对于这个集成问题有一个非常简单的解决方法:

您将使用 HibernateTransactionManager(如果使用普通 Hibernate)或 JpaTransactionManager(如果通过 JPA 使用 Hibernate),而不是在 Spring 配置中使用 DataSourcePlatformTransactionManager。

专门的 HibernateTransactionManager 将确保:

  1. 通过 Hibernate(即 SessionFactory)管理事务。
  2. 足够聪明,允许 Spring 在非 Hibernate 中使用相同的事务,即 @Transactional Spring 代码。

与往常一样,图片可能更容易理解(但请注意,代理和实际服务之间的流程仅在概念上正确且过于简单)。

document4

简而言之,就是如何集成 Spring 和 Hibernate。

对于其他集成或更深入的理解,快速查看 Spring 提供的所有可能的 PlatformTransactionManager 实现会有所帮助。

Fin

到目前为止,您应该对事务管理如何与 Spring 框架配合使用以及它如何应用于其他 Spring 库(如 Spring Boot 或 Spring WebMVC)有一个很好的概述。最大的收获应该是,最终使用哪个框架并不重要,这都与 JDBC 基础知识有关。

如果它们正确(记住:getConnection().setAutocommit(false).commit().),您将更容易理解稍后在复杂的企业应用程序中发生的情况。

谢谢阅读。

致谢

感谢 Andreas Eisele 对本指南早期版本的反馈。感谢 Ben Horsfield 提供了急需的 Javascript 片段来增强本指南。

原文链接:https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth

Share this post:

Related content