经过一些试验,我发现实际上可以在一个容器管理的事务中拥有两个使用非 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 映射文件,但这有点超出了这里的重点。重要的是两者都指定了相同的<jta-data-source>。
在应用服务器(本例中为 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 实现、版本或应用程序服务器依赖此行为。所以测试一下,不要指望完全的可移植性。