【问题标题】:Is it possible to have two MSSQL persistence units in a transaction without XA?在没有 XA 的事务中是否可以有两个 MSSQL 持久性单元?
【发布时间】:2017-06-11 15:27:29
【问题描述】:

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

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

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

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

我知道这些问题: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。

【问题讨论】:

  • 如果两个持久性单元使用相同的数据源,那么您应该不需要 XA
  • @SteveC 他们将从池中获得自己的连接,所以我看不出有任何方法可以强迫他们拥有相同的连接(以及事务)或 JPA 将如何实际管理这个。
  • 我希望他们都参与同一个 JTA 事务...
  • @SteveC 他们都将参与同一个 JTA 事务,作为单独的连接,被认为是分布式事务的一部分。这将需要 JTA 提供的 XA 源。如果不使用 XA,则两个持久性上下文都必须共享它们的连接,因为 JDBC 级别的事务绑定到连接。据我所知,这在容器管理的 JPA 中是不可能的。因此问题。
  • 嗯,我不确定。我会编造一些测试来调查这种行为。

标签: sql-server jpa jakarta-ee xa persistence-unit


【解决方案1】:

经过一些试验,我发现实际上可以在一个容器管理的事务中拥有两个使用非 XA 资源的持久性单元。但是,它可能取决于实现。 TL;DR 在底部。

如果多个资源参与事务,JTA 应该需要 XA 资源。它使用 X/Open XA 来实现分布式事务,例如跨多个数据库或数据库和 JMS 队列。显然有一些优化(它可能是 GlassFish 特定的,我不确定)允许最后一个参与者是非 XA。然而,在我的用例中,两个持久性单元都用于同一个数据库(但一组不同的表,可能有一些重叠),并且都是非 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 映射文件,但这有点超出了这里的重点。重要的是两者都指定了相同的&lt;jta-data-source&gt;

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

现在,如果将两个持久性上下文注入到 EJB 中,并调用一个被认为是在容器管理的事务中的方法,您会期望事情看起来像这样。

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

但是,当尝试使用指向具有非 XA 实现的连接池的数据源进行上述操作时(并进行一些实际的持久性工作),没有例外,一切正常! MSSQL 服务器中的 XA 支持甚至被禁用,并且尝试使用 XA 驱动程序会导致错误,直到它被启用,所以这并不是我在不知情的情况下意外使用 XA。

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

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

但这实际上是为什么它有效。分布式事务的提交点应该表现得好像所有部分形成一个整体(假设在投票阶段都投了“是”)。在这种情况下,两个持久性上下文都在同一个连接上运行,因此它们隐含地是一个工作单元。由于事务由容器管理,因此无论如何都无法立即访问它,这意味着您无法提交一个上下文而不是另一个上下文。并且只有一个连接来实际注册事务,它不一定是 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 资源,但都指向同一个连接池,那么它同样可以正常工作。这甚至在显式使用 com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource 类时也有效,确认它可能是 JPA 级别的优化,而不是意外地为同一个数据源获得两次相同的连接(这会破坏 GlassFish 池)。使用 XA 数据源时,它确实是一个启用 XA 的连接,但 JPA 提供者仍将相同的连接用于两个持久性上下文。只有在使用单独的池时,它实际上是两个完全独立的启用 XA 的连接,并且您将不再遇到上述异常。

那么,有什么问题呢?首先,我没有在 JPA 或 JTA 规范中找到任何描述(或强制)这种行为的内容。这意味着这可能是特定于实现的优化。移动到不同的 JPA 提供程序,甚至是不同的版本,它可能不再工作。

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

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


TL;DR

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

【讨论】:

  • 您确定两个实体管理器的持久性上下文(会话)都已正确关闭吗?如果没有,您可能会在一段时间后没有内存,或者如果在后续请求中重用持久性上下文实例,您可能会使用陈旧的数据。
  • @DraganBozanovic 好点。我假设(可能是错误的)Hibernate 开发人员必须努力明确检测两个上下文是否在同一个容器事务中并让它们共享连接,这一点已被考虑在内。会有什么好的方法来测试这个吗?一旦离开电话,我就在事务上下文之外,所以我真的没有什么要调试的。我想我可以获取 Hibernate 源代码
  • 一开始,您可以触发大量涉及两个实体管理器的请求(通过 jmeter 或类似方法)并通过分析器检查内存消耗是否在增长。
  • 同样的场景,使用同样的 JPA Provider (Hibernate),在 OpenLiberty 上不起作用。所以它与事务管理器的实现有关
  • @areus 这很有趣。它是哪个版本的 Hibernate?如果它比我在尝试此答案时使用的版本(版本 3.6.4.Final)更新,则不能保证行为仍然相同。但这肯定取决于 JTA 实现而不是 JPA 实现。或者也许是两者的结合决定了是否应用了优化。需要深入了解规范和实现才能找出确切的细节。
猜你喜欢
  • 2010-10-26
  • 2011-02-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-02
  • 1970-01-01
  • 2016-05-17
相关资源
最近更新 更多