Hibernate.org社区说明文档

第 5 章 事务和并发性

5.1. 实体管理器和事务范围
5.1.1. 工作单元
5.1.2. 长工作单元
5.1.3. 考虑对象标识
5.1.4. 常见并发性控制问题
5.2. 数据库事务分界
5.2.1. 非托管环境
5.2.2. 使用 JTA
5.2.3. 异常处理
5.3. 扩展的持久化上下文
5.3.1. 容器托管实体管理器
5.3.2. 应用程序托管实体管理器
5.4. 乐观并发性控制
5.4.1. 应用程序版本检查
5.4.2. 扩展实体管理器和自动版本控制
5.4.3. 分离的对象和自动版本控制

关于 Hibernate 实体管理器和并发性控制最重要的一点是,它非常易于理解。Hibernate 实体管理器直接使用 JDBC 连接和 JTA 资源,而不会添加任何其他锁定行为。我们强烈建议您花时间了解 JDBC、ANSI 及数据库管理系统的交易隔离说明。Hibernate 实体管理器只会添加自动版本控制,但不会锁定内存中的对象或更改您数据库事务的隔离级别。基本上,使用 Hibernate 实体管理器,就像使用直接 JDBC(或 JTA/CMT)与数据库资源一样。

我们从EntityManagerFactoryEntityManager以及数据库事务和长工作单元的粒度开始讨论 Hibernate 中的并发性控制。

在本章中,除非明确表示,我们将混合匹配实体管理器和持久化上下文的概念。一个是 API 和编程对象,另一个是范围的定义。但是,请记住其本质区别。在 Java EE 中,持久化上下文通常绑定到 JTA 事务,并且持久化上下文从事务边界开始和结束(事务范围),除非您使用扩展实体管理器。有关更多信息,请参见第 1.2.3 节,“持久化上下文范围”

EntityManagerFactory是一个创建成本高的线程安全对象,旨在供所有应用程序线程共享。通常在应用程序启动时创建一次。

一个 EntityManager 是一个廉价的、非线程安全的对象,应该在单一的业务流程、一个工作单元中使用一次,然后丢弃。一个 EntityManager 将在需要的时候获取一个 JDBC Connection (或一个 Datasource),因此即使你不是很确定是否需要数据访问来响应一个特定的请求,你也可以安全地打开并关闭一个 EntityManager。(当你使用请求拦截器开始执行以下某些模式时,这一点变得非常重要。)

为了完整地理解这一概念,你还必须考虑数据库事务。一个数据库事务必须尽可能地短,以减少数据库中的锁竞争。长的数据库事务将阻止你的应用程序扩展到高度并发负载。

一个工作单元的范围是什么?一个 Hibernate EntityManager 能跨越多个数据库事务,还是这是一个一对一的范围关系?你什么时候应该打开并关闭一个 Session ,你如何划定数据库事务边界?

首先,不要使用 按操作使用实体管理器 反模式,也就是说,不要在单个线程中的每个简单数据库调用中打开和关闭一个 EntityManager!当然,数据库事务也是如此。应用程序中的数据库调用是使用一个预先计划好的序列进行的,它们被分组到原子的工作单元中。(请注意,这也意味着在应用程序中每个单独的 SQL 语句之后进行自动提交是无用的,这种模式是针对临时 SQL 控制台工作设计的。)

在多用户客户端/服务器应用程序中最常见的模式是 按请求使用实体管理器。在这个模型中,从客户端发送一个请求到服务器(JPA 持久层在其中运行),打开一个新的 EntityManager,并且在这个工作单元中执行所有数据库操作。一旦工作完成(并且已经准备了对客户端的响应),持久性上下文被刷新并关闭,同时关闭实体管理器对象。你还可以使用一个单一的数据库事务来响应客户端的请求。两者之间的关系是一对一的,并且这种模式非常适合许多应用程序。

这是 Java EE 环境中的默认 JPA 持久性模型(JTA 绑定,事务范围持久性上下文);注入(或查找)的实体管理器为特定的 JTA 事务共享同一个持久性上下文。JPA 的妙处在于,你不再需要关心这些内容,只需完全正交地通过实体管理器来查看数据访问,以及会话 bean 中的事务范围划定。

实现上述功能(和其它功能)的挑战是这并非是 EJB3 容器外部环境:不但必须正确启动和结束 EntityManager 和资源本地事务,而且它们还可以用于数据访问操作。作业单元的划分理想的情况是使用一个拦截器并在请求访问非 EJB3 容器服务器和发送响应时运行(例如,如果您使用一个独立 servlet 容器的话,可以采用 ServletFilter)。我们建议使用 ThreadLocal 变量将 EntityManager 绑定到用于服务的线程。这样做可以在该线程中运行的全部代码中轻松访问(如同访问静态变量)。根据所选数据库事务划分机制,还可以将事务内容保存在 ThreadLocal 变量中。实现模式众所周知是 Hibernate 社区中的 线程本地会话视图中打开会话。可以轻松扩展Hibernate参考文档中所示的 HibernateUtil 来实现此模式,不需要任何外部软件(事实上非常容易)。当然,必须想办法实现一个拦截器并在环境中进行设置。查看Hibernate网站,获取提示和示例。再强调一遍,首先选择自然是 EJB3 容器 - 最好是 JBoss 应用服务器那样精简模块化的服务器。

面向请求实体管理模式并非设计作业单元的唯一有用概念。很多业务流程需要与用户互动的一系列操作穿插数据库访问。在 Web 和企业应用中,数据库事务不能涵盖用户交互是不可以接受的,因为请求之间可能会间隔较长的等待时间。考虑以下示例

从用户的角度来看,我们将此作业单元称为长期运行 应用程序事务。有很多方法可以在应用中实现此动作。

首次天真的实现可能在用户思考的时间内保持EntityManager和数据库事务处于打开状态,同时在数据库中保留锁以防止并发修改并以保证独立性和原子性。这当然是一个反模式,即一种悲观的方法,因为锁定竞争将不允许应用程序随着并发用户数量的增加而扩展。

很明显,我们必须使用多个数据库事务来实现应用程序事务。在这种情况下,维护业务流程独立性变成应用程序层的部分责任。单个应用程序事务通常跨越多个数据库事务。如果这些数据库事务中只有一个(最后一个)存储已更新数据,则它将是原子的,所有其他事务仅读取数据(例如,在跨越多个请求/响应周期的向导式对话中)。这比听起来要容易实现,尤其是当您使用 JPA 实体管理器和持久性上下文功能时

每个请求一个实体管理器和分离对象每个应用程序事务一个实体管理器都具有优点和缺点,我们将在本章后面以乐观并发控制的背景下对其进行讨论。

TODO:此注释可能应该稍后出现。

应用程序可能在两个不同的持久性上下文中并发地访问相同的持久状态。但是,受管类的一个实例绝不会在两个持久性上下文之间共享。因此,有两种不同的标识概念

然后,对于附加到特定持久化上下文的对象(即,EntityManager 的作用域),这两个概念是相等的,并且 Hibernate 实体管理器保证了 JVM 标识对于数据库标识是有效的。但是,尽管应用程序可能在两个不同的持久化上下文中同时访问“相同”(持久化标识)业务对象,但这两个实例实际上是“不同的”(JVM 标识)。在刷新/提交时通过(自动版本控制)解决冲突,使用乐观方法。

此方法将并发问题留给 Hibernate 和数据库处理;它还提供了最佳的可扩展性,因为仅保证单线程工作单元中的标识无需昂贵的锁定或其他同步方法。只要应用程序坚持每个 EntityManager 使用单线程,就无需在任何业务对象上进行同步。在持久化上下文中,应用程序可以安全地使用 == 来比较实体。

但是,在持久化上下文外部使用 == 的应用程序可能会看到意外的结果。即使在一些意外的地方也可能发生这种情况,例如,如果你将两个分离的实例放入同一个 Set 中。两者可能具有相同的数据库标识(即,它们表示同一行),但对于分离状态的实例,JVM 标识在定义上并非保证的。开发人员必须覆盖持久类中的 equals()hashCode() 方法,并实现对象相等性的自己的概念。有一个警告:切勿使用数据库标识符来实现相等性,请使用业务密钥(唯一且通常不可变的属性的组合)。如果瞬态实体变得持久,数据库标识符将发生变化(请参阅 persist() 操作的约定)。如果瞬态实例(通常与分离实例一起)保存在 Set 中,则更改哈希码会破坏 Set 的约定。良好业务密钥的属性不必像数据库主键一样稳定,你只需保证在对象位于同一 Set 时稳定即可。有关此问题的更全面讨论,请访问 Hibernate 网站。还要注意,这不是 Hibernate 问题,而是只如何实现 Java 对象标识和相等性的问题。

切勿使用反模式per-user-session 实体管理器per-application 实体管理器(当然,此规则有极少数的例外,例如,在桌面应用程序中 per-application 实体管理器可以接受,并手动刷新持久化上下文)。请注意,以下一些问题也可能与推荐的模式一起出现,在做出设计决策之前,请确保你理解了含义

数据库(或系统)事务边界始终是必需的。任何与数据库的通信都不得发生在数据库事务之外(这似乎让很多习惯了自动提交模式的开发人员感到困惑)。始终使用明确的事务边界,即使是针对只读操作。这也许并不是根据你的隔离级别和数据库功能所必需的,但是如果你始终明确地划分事务,就不会有任何不利因素。不过,当你需要保留 EXTENDED 持久性上下文中的修改时,你将必须在事务之外执行操作。

JPA 应用程序可以在非托管(即独立、简单网络或 Swing 应用程序)和托管的 Java EE 环境中运行。在非托管环境中,一个 EntityManagerFactory 通常负责它自己的数据库连接池。应用程序开发人员必须手动设置事务边界,换句话说,自行开始、提交或回滚数据库事务。托管环境通常提供容器托管事务,事务组件通过 EJB 会话 Bean 的注释进行声明式定义,例如。然后,不再需要以编程方式划分事务,甚至会自动刷新 EntityManager

通常,结束一个工作单元涉及四个不同的阶段

我们现在将仔细查看在托管和非托管环境中的事务划分和异常处理。

如果 JPA 持久性层在非托管环境中运行,则数据库连接通常在幕后由 Hibernate 的池机制处理。常见的实体管理器和事务处理习语如下所示

// Non-managed environment idiom

EntityManager em = emf.createEntityManager();
EntityTransaction tx = null;
try {
    tx = em.getTransaction();
    tx.begin();
    // do some work
    ...
    tx.commit();
}
catch (RuntimeException e) {
    if ( tx != null && tx.isActive() ) tx.rollback();
    throw e; // or display error message
}
finally {
    em.close();
}

您不必明确地 flush() EntityManager - 调用 commit() 会自动触发同步。

调用 close() 标记 EntityManager 的结束。close() 的主要含义是释放资源 - 确保您始终在明确的 finally 块内进行关闭,而不要在外部。

在普通应用程序中,您很有可能永远不会在业务代码中看到这种写法;致命(系统)异常应始终在“顶部”捕获。换句话说,执行实体管理器调用(在持久性层中)的代码和处理 RuntimeException 的代码(并且通常只能清理并退出)位于不同的层中。这可能是一个自己设计时的挑战,当 J2EE/EJB 容器服务可用时,您应该使用这些服务。本章后面会讨论异常处理。

如果您的持久层在应用程序服务器中运行(例如在 EJB3 会话 Bean 后面),则实体管理器内部获得的每个数据源连接都将自动成为全局 JTA 事务的一部分。Hibernate 为此集成提供了两种策略。

如果您使用 bean 管理的事务 (BMT),代码将如下所示

// BMT idiom

@Resource public UserTransaction utx;
@Resource public EntityManagerFactory factory;
public void doBusiness() {
    EntityManager em = factory.createEntityManager();
    try {
    // do some work
    ...
    utx.commit();
}
catch (RuntimeException e) {
    if (utx != null) utx.rollback();
    throw e; // or display error message
}
finally {
    em.close();
}

在 EJB3 容器中的容器管理事务 (CMT) 中,事务划分是在会话 Bean 注释或部署描述符中进行的,而不是以编程方式完成的。 EntityManager 将在事务完成后自动刷新(如果您已注入或查找 EntityManager,它也将自动关闭)。如果在 EntityManager 使用期间发生异常,则当您不捕获异常时,事务回滚将自动进行。由于 EntityManager 异常是 RuntimeException,它们将根据 EJB 规范回滚事务(系统异常与应用程序异常)。

让 Hibernate EntityManager 定义 hibernate.transaction.factory_class(即不重写此值)非常重要。请记住还要设置 org.hibernate.transaction.manager_lookup_class

如果您在 CMT 环境中工作,可能还需要在代码的不同部分中使用相同的实体管理器。通常,在非托管环境中,您将使用 ThreadLocal 变量来保存实体管理器,但是单个 EJB 请求可能在不同的线程中执行(例如,会话 bean 调用另一个会话 bean)。EJB3 容器会为您负责持久化上下文传播。无论使用注入还是查找,EJB3 容器都会返回一个具有与 JTA 上下文(如果存在)绑定的相同持久化上下文的实体管理器,或创建一个新上下文并进行绑定(请参阅 第 1.2.4 节,“持久性上下文传播”)。

我们在 CMT 和 EJB3 容器使用中的实体管理器/事务管理习语被缩减为

//CMT idiom through injection

@PersistenceContext(name="sample") EntityManager em;

或者,如果您使用 Java 上下文和依赖注入 (CDI),则为

@Inject EntityManager em;

换句话说,您在托管环境中要做的全部工作就是注入 EntityManager,执行数据访问工作,然后将其他事情留给容器。事务边界在您的会话 bean 的注释或部署描述符中以声明的方式设置。实体管理器和持久化上下文的生命周期完全由容器管理。

由于 JTA 规范的一个愚蠢限制,Hibernate 无法自动清除由 scroll()iterate() 返回的任何未关闭的 ScrollableResultsIterator 实例。您必须通过从 finally 块显式调用 ScrollableResults.close()Hibernate.close(Iterator) 来释放底层数据库游标。(当然,大多数应用程序可以很容易地避免在 CMT 代码中使用 scroll()iterate()。)

如果 EntityManager 抛出异常(包括任何 SQLException),您应该立即回滚数据库事务,调用 EntityManager.close()(如果已调用 createEntityManager())并舍弃 EntityManager 实例。某些 EntityManager 方法不会将持久化上下文保留在一致的状态。实体管理器抛出的任何异常都不能被视为可恢复的。确保将调用 close()EntityManager 关闭在一个 finally 块中。请注意,容器托管的实体管理器将为您执行这项操作。您只需要让 RuntimeException 传播到容器中即可。

Hibernate 实体管理器通常会引发包含 Hibernate 核心异常的异常。EntityManager API 引发的常见异常为

HibernateException 包装了 Hibernate 持久层中可能发生的大多数错误,它是一个未经检查的异常。请注意,Hibernate 还有可能抛出其他未经检查的异常,它们不是 HibernateException。它们同样无法恢复,应采取相应的措施。

与数据库交互时抛出的 SQLException,Hibernate 用 JDBCException 包装了起来。实际上,Hibernate 会尝试将该异常转换成 JDBCException 的更具意义的子类。可始终通过 JDBCException.getCause() 获得基础 SQLException。Hibernate 使用附加到 SessionFactorySQLExceptionConverter,将 SQLException 转换成适当的 JDBCException 子类。默认情况下,SQLExceptionConverter 由配置的方言定义;但是,还可以插入自定义实现(有关详细信息,请参阅 SQLExceptionConverterFactory 类的 Java 文档)。标准 JDBCException 子类型为

所有以这种方式定义的应用程序托管实体管理器和容器托管持久上下文都是 EXTENDED。这意味着持久上下文类型超出了事务生命周期。因此,我们应该理解在事务的范围内之外执行的操作会发生什么。

EXTENDED 持久上下文中,实体管理器的所有只读操作都可以在事务之外执行 (find()getReference()refresh()detach() 和读取查询)。一些修改操作可以在事务之外执行,但它们被排队,直到持久上下文加入一个事务:这是 persist()merge()remove() 的情况。有些操作不能在事务外部调用:flush()lock(),以及更新/删除查询。

唯一与高并发、高可伸缩性一致的方法是乐观并发控制与版本控制。版本检查使用版本号或时间戳来检测冲突更新(并防止更新丢失)。Hibernate 提供三种可能的方法来编写使用乐观并发控制的应用程序代码。我们展示的用例是在较长的应用程序事务的上下文中,但版本检查也有好处,可以在单个数据库事务中防止更新丢失。

在持久机制没有太多帮助的实现中,与数据库的每一次交互发生在一个新的EntityManager 中,并且开发人员负责在操作它们之前从数据库重新加载所有持久实例。此方法强制应用程序执行它自己的版本检查,以确保应用程序事务隔离。这种方法在数据库访问方面效率最低。它与 EJB2 实体最相似的方法

// foo is an instance loaded by a previous entity manager

em = factory.createEntityManager();
EntityTransaction t = em.getTransaction();
t.begin();
int oldVersion = foo.getVersion();
Foo dbFoo = em.find( foo.getClass(), foo.getKey() ); // load the current state
if ( dbFoo.getVersion()!=foo.getVersion ) throw new StaleObjectStateException();
dbFoo.setProperty("bar");
t.commit();
em.close();

version 属性使用@Version 进行映射,如果实体被修改,实体管理器在刷新期间将自动增加它。

当然,如果您操作的低数据并发环境不需要版本检查,您可以使用此方法并跳过版本检查。在这种情况下,最后提交获胜 将成为您的长期应用程序事务的默认策略。请记住,这可能会使用应用的用户感到困惑,因为他们可能会在没有错误消息或合并冲突更改而不直接遇到更新丢失。

显然,手动版本检查只可能在非常简单的环境中进行,并且不适用于大多数应用程序。通常不仅单个实例,而且还需要检查完整的修改对象图。Hibernate 使用分离实例或扩展实体管理器和持久性上下文作为设计范例,提供自动版本检查。

整个应用程序事务使用一个持久性上下问。实体管理器在刷新时间检查实例版本,如果检测到并发修改,将抛出异常。开发人员需要负责捕获和处理此异常(常见选项是允许用户合并他的更改或使用非陈旧数据重新启动业务流程)。

EXTENDED 持久化上下文中,所有在活动事务之外执行的操作都已排队。当在活动事务(最差情况下在提交时间)中执行时,EXTENDED 持久化上下文被刷新。

当等待用户交互时,实体管理器 会断开与任何基础 JDBC 连接的连接。在应用程序托管的扩展实体管理器中,这种情况在事务完成时自动发生。在持有容器托管的扩展实体管理器的有状态会话 Bean 中(即用 @PersistenceContext(EXTENDED) 注释的 SFSB),这种情况也会透明地发生。从数据库访问的角度来看,此方法最有效。应用程序无需关注版本检查或合并分离实例,而且也不必在每个数据库事务中重新加载实例。对于那些可能担心打开和关闭的连接数量的人,请记住连接提供程序应该是连接池,因此不会造成性能影响。以下示例展示了非托管环境中的惯用法

// foo is an instance loaded earlier by the extended entity manager

em.getTransaction.begin(); // new connection to data store is obtained and tx started
foo.setProperty("bar");
em.getTransaction().commit();  // End tx, flush and check version, disconnect

foo 对象仍知道它是在哪个 持久化上下文中加载的。随着 getTransaction.begin(); 实体管理器获取一个新连接并继续持久化上下文。方法 getTransaction().commit() 不仅会刷新并检查版本,还会断开实体管理器与 JDBC 连接的连接,并将连接返回到池中。

如果在用户思考时间内持久化上下文太大以致无法存储或者不知道该将它存储在何处,则此模式会出问题。例如,HttpSession 应尽量保持较小。由于持久化上下文也是(强制的)一级缓存,并且包含所有加载的对象,因此我们可能只能在几个请求/响应周期内使用此策略。确实建议这么做,因为持久化上下文很快也会有陈旧的数据。

在请求期间你要将扩展实体管理器存储在何处由你决定,在 EJB3 容器内部只需像以上所述那样使用有状态会话 Bean。不要将其传输到 Web 层(或将其序列化到单独的层)以将其存储在 HttpSession 中。在非托管的两层环境中,HttpSession 可能确实是存储它的正确位置。