12.1. Session 和事务范围(transaction scope)

优质
小牛编辑
126浏览
2023-12-01

SessionFactory 对象的创建代价很昂贵,它是线程安全的对象,它为所有的应用程序线程所共享。它只创建一次,通常是在应用程序启动的时候,由一个 Configuraion 的实例来创建。

Session 对象的创建代价比较小,是非线程安全的,对于单个请求,单个会话、单个的 工作单元而言,它只被使用一次,然后就丢弃。只有在需要的时候,一个 Session 对象 才会获取一个 JDBC 的 Connection(或一个Datasource)对象,因此假若不使用的时候它不消费任何资源。

此外我们还要考虑数据库事务。数据库事务应该尽可能的短,降低数据库中的锁争用。数据库长事务会阻止你的应用程序扩展到高的并发负载。因此,假若在用户思考期间让数据库事务开着,直到整个工作单元完成才关闭这个事务,这绝不是一个好的设计。

一个操作单元(Unit of work)的范围是多大?单个的 Hibernate Session 能跨越多个数据库事务吗?还是一个 Session 的作用范围对应一个数据库事务的范围?应该何时打开 Session,何时关闭 Session,你又如何划分数据库事务的边界呢?我们将在后续章节解决这些问题。

12.1.1. 操作单元(Unit of work)

First, let's define a unit of work. A unit of work is a design pattern described by Martin Fowler as “ [maintaining] a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems. ”[PoEAA] In other words, its a series of operations we wish to carry out against the database together. Basically, it is a transaction, though fulfilling a unit of work will often span multiple physical database transactions (see 第 12.1.2 节 “长对话”). So really we are talking about a more abstract notion of a transaction. The term "business transaction" is also sometimes used in lieu of unit of work.

首先,别用 session-per-operation 这种反模式了,也就是说,在单个线程中, 不要因为一次简单的数据库调用,就打开和关闭一次 Session! 数据库事务也是如此。 应用程序中的数据库调用是按照计划好的次序,分组为原子的操作单元。(注意,这也意味着,应用程 序中,在单个的 SQL 语句发送之后,自动事务提交(auto-commit)模式失效了。这种模式专门为SQL控制台操作设计的。 Hibernate 禁止立即自动事务提交模式,或者期望应用服务器禁止立即自动事务提交模式。)数据库事务绝不是可有可无的,任何与数据库之间的通讯都必须在某个事务中进 行,不管你是在读还是在写数据。对读数据而言,应该避免 auto-commit 行为,因为很多小的事务比一个清晰定义的工作单元性能差。后者也更容易维护和扩展。

在多用户的 client/server 应用程序中,最常用的模式是 每个请求一个会话(session-per-request)。 在这种模式下,来自客户端的请求被发送到服务器端(即 Hibernate 持久化层运行的地方),一个新的 Hibernate Session 被打开,并且执行这个操作单元中所有的数据库操作。一旦操作完成(同时对客户端的响应也准备就绪),session 被同步,然后关闭。你也可以使用单 个数据库事务来处理客户端请求,在你打开 Session 之后启动事务,在你关闭 Session 之前提交事务。会话和请求之间的关系是一对一的关系,这种模式对 于大多数应用程序来说是很棒的。

实 现才是真正的挑战。Hibernate 内置了对"当前 session(current session)" 的管理,用于简化此模式。你要做的一切就是在服务器端要处理请求的时候,开启事务,在响应发送给客户之前结束事务。你可以用任何方式来完成这一操作,通常 的方案有 ServletFilter,在 service 方法中进行 pointcut 的 AOP 拦截器,或者 proxy/interception 容器。EJB 容器是实现横切诸如 EJB session bean 上的事务分界,用 CMT 对事务进行声明等方面的标准手段。假若你决定使用编程式的事务分界,请参考本章后面讲到的 Hibernate Transaction API,这对易用性和代码可移植性都有好处。

Your application code can access a "current session" to process the request by calling sessionFactory.getCurrentSession(). You will always get a Session scoped to the current database transaction. This has to be configured for either resource-local or JTA environments, see 第 2.5 节 “上下文相关的会话(Contextual Session)”.

有时,将 Session 和数据库事务的边界延伸到"展示层被渲染后"会带来便利。有些 serlvet 应用程序在对请求进行处理后,有个单独的渲染期,这种延伸对这种程序特别有用。假若你实现你自己的拦截器,把事务边界延伸到展示层渲染结束后非常容易。然 而,假若你依赖有容器管理事务的 EJB,这就不太容易了,因为事务会在 EJB 方法返回后结束,而那是在任何展示层渲染开始之前。请访问 Hibernate 网站和论坛,你可以找到 Open Session in View 这一模式的提示和示例。

12.1.2. 长对话

session-per-request 模式不仅仅是一个可以用来设计操作单元的有用概念。很多业务处理都需 要一系列完整的与用户之间的交互,而这些用户是指对数据库有交叉访问的用户。在基于 web 的应用和企业应用中,跨用户交互的数据库事务是无法接受的。考虑下面的例子:

  • 在界面的第一屏,打开对话框,用户所看到的数据是被一个特定的 Session 和数据 库事务载入(load)的。用户可以随意修改对话框中的数据对象。

  • 5 分钟后,用户点击“保存”,期望所做出的修改被持久化;同时他也期望自己是唯一修改这个信息的人,不会出现修改冲突。

从用户的角度来看,我们把这个操作单元称为长时间运行的对话(conversation),或者应用事务(application transaction)。在你的应用程序中,可以有很多种方法来实现它。

头一个幼稚的做法是,在用户思考的过程中,保持 Session 和数据库事务是打开的,保持数据库锁定,以阻止并发修改,从而保证数据库事务隔离级别和原子操作。这种方式当然是一个反模式,因为锁争用会导致应用程序无法扩展并发用户的数目。

很 明显,我们必须使用多个数据库事务来实现这个对话。在这个例子中,维护业务处理的 事务隔离变成了应用程序层的部分责任。一个对话通常跨越多个数据库事务。如果仅仅只有一个数据库事务(最后的那个事务)保存更新过的数据,而所有其他事务 只是单纯的读取数据(例如在一个跨越多个请求/响应周期的向导风格的对话框中),那么应用程序事务将保证其原子性。这种方式比听起来还要容易实现,特别是 当你使用了 Hibernate 的下述特性的时候:

  • 自动版本化:Hibernate 能够自动进行乐观并发控制,如果在用户思考的过程中发生并发修改,Hibernate 能够自动检测到。一般我们只在对话结束时才检查。

  • 脱管对象(Detached Objects):如果你决定采用前面已经讨论过的 session-per-request 模式,所有载入的实例在用户思考的过程中都处于与 Session 脱离的状态。Hibernate 允许你把与 Session 脱离的对象重新关联到 Session 上,并且对修改进行持久化,这种模式被称为 session-per-request-with-detached-objects。自动版本化被用来隔离并发修改。

  • Extended (or Long) Session:Hibernate 的 Session 可以在数据库事务提交之后和底层的 JDBC 连接断开,当一个新的客户端请求到来的时候,它又重新连接上底层的 JDBC 连接。这种模式被称之为session-per-conversation,这种情况可 能会造成不必要的 Session 和 JDBC 连接的重新关联。自动版本化被用来隔离并发修改,Session 通常不允许自动 flush,而是显性地 flush。

session-per-request-with-detached-objectssession-per-conversation 各有优缺点,我们在本章后面乐观并发控制那部分再进行讨论。

12.1.3. 关注对象标识(Considering object identity)

应用程序可能在两个不同的 Session 中并发访问同一持久化状态,但是,一个持久化类的实例无法在两个 Session 中共享。因此有两种不同的标识语义:

数据库标识

foo.getId().equals( bar.getId() )

JVM 标识

foo==bar

对于那些关联到 特定 Session(也就是在单个 Session 的范围内)上的对象来说,这两种标识的语义是等价的,与数据库标识对应的 JVM 标识是由 Hibernate 来保证的。不过,当应用程序在两个不同的 session 中并发访问具有同一持久化标识的业务对象实例的时候,这个业务对象的两个实例事实上是不相同的(从 JVM 识别来看)。这种冲突可以通过在同步和提交的时候使用自动版本化和乐观锁定方法来解决。

这种方式把关于并发的头疼问题留给了 Hibernate 和数据库;由于在单个线程内,操作单元中的对象识别不 需要代价昂贵的锁定或其他意义上的同步,因此它同时可以提供最好的可伸缩性。只要在单个线程只持有一个 Session,应用程序就不需要同步任何业务对象。在 Session 的范围内,应用程序可以放心的使用 == 进行对象比较。

不过,应用程序在 Session 的外面使用 == 进行对象比较可能会 导致无法预期的结果。在一些无法预料的场合,例如,如果你把两个脱管对象实例放进同一个 Set 的时候,就可能发生。这两个对象实例可能有同一个数据库标识(也就是说, 他们代表了表的同一行数据),从 JVM 标识的定义上来说,对脱管的对象而言,Hibernate 无法保证他们 的的 JVM 标识一致。开发人员必须覆盖持久化类的 equals() 方法和 hashCode() 方法,从而实现自定义的对象相等语义。警告:不要使用数据库标识来实现对象相等,应该使用业务键值,由唯一的,通常不变的属性组成。当一个瞬时对象被持久化的时候,它的数据库标识会发生改变。如果一个瞬时对象(通常也包括脱管对象实例)被放入一个 Set,改变它的 hashcode 会导致与这个 Set 的关系中断。虽 然业务键值的属性不象数据库主键那样稳定不变,但是你只需要保证在同一个 Set 中的对象属性的稳定性就足够了。请到 Hibernate 网站去寻求这个问题更多的详细的讨论。请注意,这不是一个有关 Hibernate 的问题,而仅仅是一个关于 Java 对象标识和判等行为如何实现的问题。

12.1.4. 常见问题

决不要使用反模式 session-per-user-session 或者 session-per-application(当然,这个规定几乎没有例外)。请注意,下述一些问题可能也会出现在我们推荐的模式中,在你作出某个设计决定之前,请务必理解该模式的应用前提。

  • Session 对象是非线程安全的。如果一个 Session 实例允许共享的话,那些支持并发运行的东东,例如 HTTP request,session beans 或者是 Swing workers,将会导致出现资源争用(race condition)。如果在 HttpSession 中有 Hibernate 的 Session 的话(稍后讨论),你应该考虑同步访问你的 Http session。 否则,只要用户足够快的点击浏览器的“刷新”,就会导致两个并发运行线程使用同一个 Session

  • 一个由 Hibernate 抛出的异常意味着你必须立即回滚数据库事务,并立即关闭 Session(稍后会展开讨论)。如果你的 Session 绑定到一个应用程序上,你必须停止该应用程序。回滚数据库事务并不会把你的业务对象退回到事务启动时候的状态。这意味着数据库状态和业务对象状态不同步。通常情况下,这不是什么问题,因为异常是不可恢复的,你必须在回滚之后重新开始执行。

  • The Session caches every object that is in a persistent state (watched and checked for dirty state by Hibernate). If you keep it open for a long time or simply load too much data, it will grow endlessly until you get an OutOfMemoryException. One solution is to call clear() and evict() to manage the Session cache, but you should consider a Stored Procedure if you need mass data operations. Some solutions are shown in 第 14 章 批量处理(Batch processing). Keeping a Session open for the duration of a user session also means a higher probability of stale data.