Java和Spring中的事务简介 - Baeldung


在本教程中,我们将了解Java中事务的含义。因此,我们将了解如何执行资源本地事务和全局事务。这也将使我们能够探索在Java和Spring中管理事务的不同方法。
 
什么是事务?
通常,Java中的事务是指一系列必须全部成功完成的动作。因此,如果一个或多个操作失败,则所有其他操作都必须撤消,以保持应用程序状态不变。这对于确保永不损害应用程序状态的完整性是必要的。(banq注:应用程序的状态完整性实现有两种途径:通过技术上的事务概念;或者通过DDD等业务分析方法,如果状态需要完整一致地改变,将涉及这些状态的资源设计成一个聚合对象,绑定在一起,当然不能绑定太多资源,可通过原子性的长流程(如Saga)管理多个资源的状态完整性。 总之,如果追求有一个中间件或数据库大统一支持任何尺度规模的分布式事务/全局事务,是一种伪命题!)
本文主要谈论的是试图满足这样需求的中间件技术:一个或多个资源,例如数据库,消息队列,从而产生了在事务下执行动作的不同方法。这些包括使用单个资源执行资源本地事务。或者,多个资源可以参与全局事务。 
 
资源本地事务
我们将首先探讨如何在使用单个资源的同时使用Java事务。在这里,我们可能有多个单独的操作,这些操作是我们对数据库等资源执行的。但是,我们可能希望它们作为一个不可分割的整体在一个统一的整体中发生。换句话说,我们希望这些动作在单个事务下发生。
在Java中,我们有几种访问和操作数据库等资源的方法。因此,我们处理交易的方式也不相同。在本节中,我们将找到如何在Java中经常使用的某些库中使用事务的方法。

  • JDBC

Java数据库连接(JDBC)是Java中的API,用于定义如何访问Java中的数据库。不同的数据库供应商提供了JDBC驱动程序,用于以与供应商无关的方式连接到数据库。因此,我们从驱动程序检索一个Connection来对数据库执行不同的操作。
JDBC为我们提供了在事务下执行语句的选项。连接的默认行为是auto-commit。要澄清的是,这意味着将每条语句都视为事务,并在执行后立即自动提交。
但是,如果我们希望将多个语句捆绑在一个事务中,那么也可以实现:
Connection connection = DriverManager.getConnection(CONNECTION_URL, USER, PASSWORD);
try {
    connection.setAutoCommit(false);
    PreparedStatement firstStatement = connection .prepareStatement("firstQuery");
    firstStatement.executeUpdate();
    PreparedStatement secondStatement = connection .prepareStatement(
"secondQuery");
    secondStatement.executeUpdate();
    connection.commit();
} catch (Exception e) {
    connection.rollback();
}

在这里,我们禁用了Connection的自动提交模式。因此,我们可以手动定义事务边界并执行commit或rollback。JDBC还允许我们设置一个保存点,该保存点为我们提供了对回滚量的更多控制。

  • JPA

Java Persistence API(JPA)是Java中的规范,可用于弥合面向对象的领域模型和关系数据库系统之间的鸿沟。因此,可以从Hibernate,EclipseLink和iBatis等第三方获得JPA的几种实现。
在JPA中,我们可以将常规类定义为为其提供持久标识的Entity。该EntityManager的类提供了必要的接口与一个持久化上下文中的多个实体的工作。持久性上下文可以看作是管理实体的第一级缓存:


这里的持久性上下文可以是两种类型,事务作用域或扩展作用域。事务范围的持久性上下文绑定到单个事务。而扩展范围的持久性上下文可以跨越多个事务。持久性上下文的默认范围是transaction-scope。
让我们看看如何创建EntityManager并手动定义事务边界:

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-example");
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
    entityManager.getTransaction().begin();
    entityManager.persist(firstEntity);
    entityManager.persist(secondEntity);
    entityManager.getTransaction().commit();
} catch (Exceotion e) {
    entityManager.getTransaction().rollback();
}

在这里,我们要在事务范围的持久性上下文的上下文中从EntityManagerFactory创建EntityManager。然后,我们使用begin,commit和rollback方法定义事务边界。
  • JMS

Java Messaging Service(JMS)是Java中的规范,它允许应用程序使用消息进行异步通信。该API使我们能够从队列或主题创建,发送,接收和读取消息。有几种符合JMS规范的消息传递服务,包括OpenMQ和ActiveMQ。
JMS API支持在单个事务中捆绑多个发送或接收操作。但是,由于基于消息的集成体系结构的性质,消息的生产和使用不能成为同一事务的一部分。事务的范围保留在客户端和JMS提供者之间:

JMS允许我们根据从供应商特定的ConnectionFactory获得的Connection创建Session。我们可以选择创建一个是否进行交易的会话。对于非事务会话小号,我们可以进一步定义适当的确认模式也是如此。
让我们看看如何创建事务处理会话以在事务下发送多个消息:

ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CONNECTION_URL);
Connection connection = = connectionFactory.createConnection();
connection.start();
try {
    Session session = connection.createSession(true, 0);
    Destination = destination = session.createTopic("TEST.FOO");
    MessageProducer producer = session.createProducer(destination);
    producer.send(firstMessage);
    producer.send(secondMessage);
    session.commit();
} catch (Exception e) {
    session.rollback();
}

在这里,我们正在为主题类型的目标创建一个MessageProducer。我们从之前创建的会话中获取目标。我们进一步使用Session通过commit和rollback方法定义事务边界。

全局事务
正如我们看到的,资源本地事务使我们能够在一个资源中作为一个统一的整体执行多个操作。但是,我们经常会处理跨多个资源的操作。例如,在两个不同的数据库或一个数据库和一个消息队列中进行操作。在这里,资源内的本地交易支持对我们来说还不够。
在这些情况下,我们需要一种全局机制来划分跨越多个参与资源的事务。这通常被称为分布式事务,并且已经提出了一些规范来有效地处理它们。
XA规范是一种这样的规范,其限定一个事务管理器,以跨越多个资源控制事务。Java通过组件JTA和JTS对符合XA规范的分布式事务提供了相当成熟的支持。

  • JTA

Java Transaction API(JTA)  是在Java Community Process下开发的Java Enterprise Edition API。它  使Java应用程序和应用程序服务器可以跨XA资源执行分布式事务。JTA利用XA架构建模,利用了两阶段提交。
JTA指定了事务管理器与分布式事务中的其他各方之间的标准Java接口:

让我们了解上面突出显示的一些关键接口:
  1. TransactionManager :  一个接口,允许应用程序服务器划分和控制事务
  2. UserTransaction :  此接口允许应用程序明确划分和控制事务
  3. XAResource :  此接口的目的是允许事务管理器与资源管理器一起使用XA兼容资源

  • JTS

Java Transaction Service(JTS)  是  用于构建映射到OMG OTS规范的事务管理器的规范。JTS使用标准的CORBA ORB / TS接口和Internet间ORB协议(IIOP)在JTS事务管理器之间传播事务上下文。
在较高级别,它支持Java事务处理API(JTA)。JTS事务管理器向分布式事务中涉及的各方提供事务服务:

JTS向应用程序提供的服务在很大程度上是透明的,因此我们甚至可能不会在应用程序体系结构中注意到它们。JTS是围绕应用程序服务器构建的,该服务器从应用程序中提取所有事务语义。
 
JTA事务管理
现在是时候了解我们如何使用JTA管理分布式事务了。分布式交易不是简单的解决方案,因此也涉及成本问题。此外,我们可以选择多个选项以将JTA包含在我们的应用程序中。因此,我们的选择必须基于整体应用程序体系结构和期望。
  • 应用服务器中的JTA

如我们先前所见,JTA体系结构依赖于应用程序服务器来促进许多与事务相关的操作。它依靠服务器提供的关键服务之一就是通过JNDI进行的命名服务。这是XA资源(例如数据源)的绑定和检索来源。
除此之外,我们可以选择如何在应用程序中管理事务边界。这在Java应用程序服务器中产生了两种类型的事务:
  1. 容器管理的事务:顾名思义,此处事务边界由应用程序服务器设置。由于它不包括与事务划分有关的语句,并且仅依赖于容器,因此简化了Enterprise Java Bean(EJB)的开发。但是,这不能为应用程序提供足够的灵活性。
  2. Bean管理的事务:与容器管理的事务相反,在Bean管理的事务中,EJB包含用于定义事务划分的显式语句。这为应用程序提供了精确的控制,以标记事务的边界,尽管以增加复杂性为代价。

在应用程序服务器的上下文中执行事务的主要缺点之一是应用程序与服务器紧密耦合。这与应用程序的可测试性,可管理性和可移植性有关。这在微服务架构中更为深刻,在微服务架构中,重点更多地放在开发与服务器无关的应用程序上。
  • JTA独立中间件

我们在上一节中讨论的问题为创建不依赖于应用程序服务器的分布式事务解决方案提供了巨大的动力。在这方面,我们有几种选择,例如在Spring中使用事务支持或使用Atomikos等事务管理器。
让我们看看如何使用像Atomikos这样的事务管理器来促进具有数据库和消息队列的分布式事务。分布式事务的关键方面之一是使用事务监视器征募和退出参与资源。Atomikos为我们解决了这个问题。我们要做的就是使用Atomikos提供的抽象:
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
DataSource dataSource = atomikosDataSourceBean;

在这里,我们将创建AtomikosDataSourceBean的实例,并注册特定于供应商的XADataSource。从这里开始,我们可以像其他任何DataSource一样继续使用它,并获得分布式事务的好处。

同样,我们有一个消息队列的抽象,它负责自动在事务监视器中注册特定于供应商的XA资源:

AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
atomikosConnectionFactoryBean.setXaConnectionFactory(new ActiveMQXAConnectionFactory());
ConnectionFactory connectionFactory = atomikosConnectionFactoryBean;

在这里,我们正在创建AtomikosConnectionFactoryBean的实例,并从启用XA的JMS供应商注册XAConnectionFactory。此后,我们可以继续将其用作常规ConnectionFactory。
现在,Atomikos为我们提供了将所有内容整合在一起的最后一个难题,这是UserTransaction的一个实例:

UserTransaction userTransaction = new UserTransactionImp();

现在,我们准备创建一个跨数据库和消息队列的分布式事务应用程序:
try {
    userTransaction.begin();
 
    java.sql.Connection dbConnection = dataSource.getConnection();
    PreparedStatement preparedStatement = dbConnection.prepareStatement(SQL_INSERT);
    preparedStatement.executeUpdate();
 
    javax.jms.Connection mbConnection = connectionFactory.createConnection();
    Session session = mbConnection.createSession(true, 0);
    Destination destination = session.createTopic("TEST.FOO");
    MessageProducer producer = session.createProducer(destination);
    producer.send(MESSAGE);
 
    userTransaction.commit();
} catch (Exception e) {
    userTransaction.rollback();
}

在这里,我们使用类UserTransaction中的begin和commit方法来划分事务边界。这包括将记录保存在数据库中以及将消息发布到消息队列。
 
Spring的事务支持
我们已经看到,处理事务是一项相当复杂的任务,其中包括许多样板代码和配置。而且,每种资源都有其自己的本地事务处理方式。在Java中,JTA使我们从这些变化中抽象出来,但进一步带来了提供程序特定的细节和应用程序服务器的复杂性。
Spring平台为我们提供了一种更干净的方式来处理事务,包括 Java中的资源本地事务和全局事务。这与Spring的其他优点一起,为使用Spring处理事务创造了令人信服的案例。而且,使用Spring可以很容易地配置和切换事务管理器,Spring可以是服务器提供的,也可以是独立的。
Spring 通过使用事务代码为方法创建代理为我们提供了这种无缝的抽象。代理借助TransactionManager代表代码管理事务状态:这里的中心接口是PlatformTransactionManager,它具有许多可用的实现。它通过JDBC(数据源),JMS,JPA,JTA和许多其他资源提供抽象。
  • Configurations

让我们看看如何配置Spring以将Atomikos用作事务管理器并为JPA和JMS提供事务支持。我们将从定义JTA类型的PlatformTransactionManager开始:

@Bean
public PlatformTransactionManager platformTransactionManager() throws Throwable {
    return new JtaTransactionManager(
                userTransaction(), transactionManager());
}

在这里,我们向JTATransactionManager提供UserTransaction和TransactionManager的实例。这些实例由Atomikos之类的事务管理器库提供:

@Bean
public UserTransaction userTransaction() {
    return new UserTransactionImp();
}
 
@Bean(initMethod = "init", destroyMethod = "close")
public TransactionManager transactionManager() {
    return new UserTransactionManager();
}

Atomikos在此处提供了类UserTransactionImp和UserTransactionManager。
此外,我们需要定义JmsTemplete,该类是允许在Spring中进行同步JMS访问的核心类:
@Bean
public JmsTemplate jmsTemplate() throws Throwable {
    return new JmsTemplate(connectionFactory());
}

在这里,ConnectionFactory由Atomikos提供,在其中它为其提供的Connection启用分布式事务:
@Bean(initMethod = "init", destroyMethod = "close")
public ConnectionFactory connectionFactory() {
    ActiveMQXAConnectionFactory activeMQXAConnectionFactory = new 
ActiveMQXAConnectionFactory();
    activeMQXAConnectionFactory.setBrokerURL(
"tcp://localhost:61616");
    AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
    atomikosConnectionFactoryBean.setUniqueResourceName(
"xamq");
    atomikosConnectionFactoryBean.setLocalTransactionMode(false);
atomikosConnectionFactoryBean.setXaConnectionFactory(activeMQXAConnectionFactory);
    return atomikosConnectionFactoryBean;
}

因此,正如我们所看到的,这里我们使用AtomikosConnectionFactoryBean包装了特定于JMS提供程序的XAConnectionFactory。
接下来,我们需要定义一个AbstractEntityManagerFactoryBean,它负责在Spring中创建JPA EntityManagerFactory bean:
@Bean
public LocalContainerEntityManagerFactoryBean entityManager() throws SQLException {
    LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
    entityManager.setDataSource(dataSource());
    Properties properties = new Properties();
    properties.setProperty( "javax.persistence.transactionType", "jta");
    entityManager.setJpaProperties(properties);
    return entityManager;
}

和以前一样,数据源,我们在设置LocalContainerEntityManagerFactoryBean这里被Atomikos公司提供分布式事务启用:
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSource() throws SQLException {
    MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
    mysqlXaDataSource.setUrl(
"jdbc:mysql://127.0.0.1:3306/test");
    AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
    xaDataSource.setXaDataSource(mysqlXaDataSource);
    xaDataSource.setUniqueResourceName(
"xads");
    return xaDataSource;
}

在这里,我们将提供程序特定的XADataSource包装在AtomikosDataSourceBean中。
 
事务管理
经过上一节中的所有配置后,我们一定感到不知所措!毕竟,我们甚至可能会质疑使用Spring的好处。但是请记住,所有这些配置使我们能够从大多数提供程序特定的样板中抽象出来,而我们的实际应用程序代码根本不需要意识到这一点。
因此,现在我们准备探索如何在Spring中使用事务,在Spring中我们打算更新数据库并发布消息。Spring为我们提供了两种方法来实现这一目标,并有各自的优势可供选择。让我们了解如何使用它们:
  • 声明式支持

在Spring中使用事务的最简单方法是声明式支持。在这里,我们有一个方便注释,可用于方法甚至类。这只是为我们的代码启用了全局事务处理:
@PersistenceContext
EntityManager entityManager;
 
@Autowired
JmsTemplate jmsTemplate;
 
@Transactional(propagation = Propagation.REQUIRED)
public void process(ENTITY, MESSAGE) {
   entityManager.persist(ENTITY);
   jmsTemplate.convertAndSend(DESTINATION, MESSAGE);
}

上面的简单代码足以允许JTA事务中的数据库中的保存操作和消息队列中的发布操作。
  • 程序化支持

尽管声明性支持非常优雅和简单,但它没有为我们提供更精确地控制交易边界的好处。因此,如果确实有一定的需要实现,Spring将提供编程支持来划分事务边界:
@Autowired
private PlatformTransactionManager transactionManager;
 
public void process(ENTITY, MESSAGE) {
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
    transactionTemplate.executeWithoutResult(status -> {
        entityManager.persist(ENTITY);
        jmsTemplate.convertAndSend(DESTINATION, MESSAGE);
    });
}

因此,正如我们所看到的,我们必须使用可用的PlatformTransactionManager创建一个TransactionTemplate。然后,我们可以使用TransactionTemplete处理全局事务中的一堆语句。
 
总结
如我们所见,处理事务,尤其是跨多种资源的事务是复杂的。而且,事务本质上是阻塞的,这不利于应用的等待时间和吞吐量。此外,使用分布式事务测试和维护代码并不容易,尤其是在事务依赖于底层应用程序服务器的情况下。因此,总而言之,最好是尽可能避免交易!
但这远非现实。简而言之,在实际应用中,我们确实确实有交易的合法需求。尽管可以重新考虑无需事务处理的应用程序体系结构,但这并不总是可能的。因此,在使用Java进行事务处理时,我们必须采用某些最佳实践,以使我们的应用程序更好:

  • 我们应该采用的基本转变之一是使用独立的事务管理器,而不是由应用程序服务器提供的事务管理器。仅此一项就可以大大简化我们的应用程序。而且,它非常适合云原生微服务架构。
  • 此外,像Spring这样的抽象层可以帮助我们遏制 JPA或JTA提供程序等提供程序的直接影响。因此,这可以使我们在提供商之间进行切换,而又不会对我们的业务逻辑产生太大影响。而且,它消除了我们管理交易状态的底层责任。
  • 最后,我们应该谨慎选择代码中的事务边界。由于事务受阻,因此最好始终限制事务边界。如果有必要,我们应该优先选择程序化而非声明式控制。

参考:
分布式事务可能是个伪概念