在没有 XA 的情况下,是否可以在一个事务中有两个 MSSQL 持久性单元?

Is it possible to have two MSSQL persistence units in a transaction without XA?

我们有一个应用程序,它有许多实体 class,并且必须有两个 table。 table 是相同的,唯一的区别是名称。 SO 上提供的常见解决方案是使用继承(映射的 superclass 和 table-per-class 策略)或两个具有不同映射的持久性单元。我们使用后一种解决方案,应用程序是建立在这种方法之上的,所以它现在被认为是给定的。

有一些 EJB 方法可以对两个持久性上下文进行更新,并且必须在一个事务中进行。两个持久性上下文具有相同的数据源,即与 Microsoft SQL Server 数据库(2012 版)的支持 XA 的连接。上下文之间的唯一区别是,有一个映射 XML 来更改某些实体 classes 的 table 名称,因此可以在那些 tables 上工作。

一位架构负责人希望看到 XA 事务被消除,因为它们会导致数据库的大量开销,并且显然还会使执行的查询的日志记录和分析变得更加困难,可能还会阻止一些准备好的语句缓存。我不知道所有细节,但对于很多应用程序,我们已经设法消除了 XA。然而,对于这一个,我们目前不能,因为有两个持久化上下文。

在这种情况下,是否有某种方法可以在没有 XA 的情况下以事务方式对两个上下文进行更新?如果是这样,如何?如果不是,是否有一些架构或配置更改可以使用一个持久性上下文而不必为两个 table 转向子 classes?

我知道这些问题:Is it possible to use more than one persistence unit in a transaction, without it being XA?XA transaction for two phase commit

在投票将其作为重复项关闭之前,请注意情况不同。我们不像第一个问题那样处于只读状态,两个上下文都在同一个数据库上运行,我们只使用 MSSQL 并且我们在 GlassFish 上,而不是 Weblogic。

经过一些试验,我发现实际上可以在一个 container-managed 事务中有两个使用 non-XA 资源的持久性单元。但是,它可能是implementation-dependent。 TL;DR 在底部。

如果多个资源参与事务,JTA 应该需要 XA 资源。它使用 X/Open XA 来允许分布式事务,例如在多个数据库或数据库和 JMS 队列上。显然有一些优化(可能是 GlassFish-specific,我不确定)允许最后一个参与者是 non-XA。然而,在我的 use-case 中,两个持久性单元都用于同一个数据库(但一组不同的表,可能有一些重叠)并且都是 non-XA。这意味着我们希望在第二个资源不支持 XA 时抛出异常。

假设这是我们的 persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="playground" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <jta-data-source>jdbc/playground</jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="be.dkv.hibernate.SQLServer2012Dialect" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
    <persistence-unit name="playground-copy" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <jta-data-source>jdbc/playground</jta-data-source>
        <mapping-file>META-INF/orm-playground-copy.xml</mapping-file>
        <properties>
            <property name="hibernate.dialect" value="be.dkv.hibernate.SQLServer2012Dialect" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

有两个持久化单元,一个名为 playground,另一个名为 playground-copy。后者有一个 ORM 映射文件,但这有点离题了。重要的是两者具有相同的 <jta-data-source> 指定。

在应用程序服务器(本例中为 GlassFish)中,我们将有一个 JDBC 连接池,其中一个名为 playground 的 JDBC 资源使用此池。

现在,如果将两个持久性上下文注入到一个 EJB 中,并且调用了一个被认为在 container-managed 事务中的方法,您会希望事情看起来像这样。

两个持久性上下文使用相同的数据源,但事务管理器和 JPA 层都不应该真正关心这一点。毕竟,他们可能有不同的数据源。由于数据源无论如何都由连接池支持,因此您希望两个单元都获得自己的连接。 XA 将允许工作以事务方式运行,因为 XA-enabled 资源将实现两阶段提交。

然而,当尝试将数据源指向具有 non-XA 实现的连接池(并进行一些实际的持久性工作)时,没有异常,一切正常! MSSQL 服务器中的 XA 支持甚至被禁用,尝试使用 XA 驱动程序会导致错误,直到它被启用,所以这不像是我在不知情的情况下不小心使用了 XA。

使用调试器进入代码后发现,作为不同的实体管理器(它们应该如此)的两个持久性上下文实际上使用了相同的连接。一些进一步的挖掘表明连接未设置为 XA 事务,并且在 JDBC 级别具有相同的事务标识符。于是情况变成了这样:

我只能假设如果为同一事务创建多个单元,则 JPA 提供程序具有利用同一连接的优化。那么,为什么这会好呢?在 JDBC 级别,事务在连接上提交。据我所知,JDBC 规范没有提供在单个连接上进行多个事务 运行 的方法。这意味着如果提交了一个持久性上下文的工作,那么另一个持久性上下文也会发生提交。

但这实际上是 有效的原因。分布式事务的提交点应该像所有部分组成一个整体一样(假设在投票阶段所有投票"yes")。在这种情况下,两个持久性上下文都在同一个连接上运行,因此它们隐含地是一个工作单元。由于事务由容器管理,因此无论如何都无法立即访问它,这意味着您无法提交一个上下文而不提交另一个上下文。并且只有一个连接来实际注册事务,它不必是 XA,因为从事务管理器的角度来看它不被认为是分布式的。

请注意,这并不违反持久性上下文的局部性。从数据库中获取实体会在两个上下文中产生一个单独的对象。它们仍然可以彼此独立运行,就像它们在单独连接时一样。在上图中,具有相同主键的相同类型的提取实体代表相同的数据库行,但是是由它们各自的实体管理器管理的独立对象。

为了验证这确实是 JPA 提供程序的一些优化,我创建了第二个连接池(到同一个数据库)和一个单独的 JDBC 资源,将其设置为第二个持久性单元并进行了测试。这会导致预期的异常:

Caused by: java.sql.SQLException: Error in allocating a connection. 
Cause: java.lang.IllegalStateException: Local transaction already has 1 non-XA Resource: cannot add more resources. 

如果您创建了两个 JDBC 资源,但都指向同一个连接池,那么它仍然可以正常工作。这甚至在显式使用 class com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource 时起作用,确认它可能是 JPA 级别的优化,而不是意外地为同一数据源获得两次相同的连接(这会破坏 GlassFish 池)。使用 XA 数据源时,确实是XA-enabled 连接,但 JPA 提供程序仍将对两个持久性上下文使用相同的连接。仅当使用单独的池时,它实际上是两个完全独立的 XA-enabled 连接,您将不再遇到上述异常。

那么,有什么收获呢?首先,我没有在 JPA 或 JTA 规范中找到任何描述(或强制)此行为的内容。这意味着这可能是一个 implementation-specific 优化。移至不同的 JPA 提供程序,甚至不同的版本,它可能不再有效。

其次,可能会出现死锁。如果您在两个上下文中获取上面示例中的实体,然后在一个上下文中更改它并刷新,就可以了。在一个上下文中获取它,调用 flush 方法,然后尝试在另一个上下文中获取它,您可能会遇到死锁。如果允许 read-uncommitted 事务隔离,则可以避免这种情况,但在一个上下文中看到的内容将取决于在另一个上下文中获取刷新时的情况。所以手动刷新调用可能很棘手。

作为参考,使用的 GlassFish 版本是 3.1.2.2。 JPA 提供程序是 Hibernate 版本 3.6.4.Final.


TL;DR

,您可以在 JavaEE container-managed 事务中使用具有相同 non-XA 资源的两个持久性上下文,并且保留 ACID 属性。然而,这要归功于当为具有相同数据源的相同事务创建多个 EntityManager 时,Hibernate 可能会进行优化。由于 JPA 或 JTA 规范似乎没有强制要求,因此您可能无法跨 JPA 实现、版本或应用程序服务器依赖此行为。所以测试一下,不要指望完全可移植。