第 12 章 事务和并发.docx
- 文档编号:29123658
- 上传时间:2023-07-20
- 格式:DOCX
- 页数:16
- 大小:28.32KB
第 12 章 事务和并发.docx
《第 12 章 事务和并发.docx》由会员分享,可在线阅读,更多相关《第 12 章 事务和并发.docx(16页珍藏版)》请在冰豆网上搜索。
第12章事务和并发
第 12 章 事务和并发
Hibernate的事务和并发控制很容易掌握。
Hibernate直接使用JDBC连接和JTA资源,不添加任何附加锁定行为。
我们强烈推荐你花点时间了解JDBC编程,ANSISQL查询语言和你使用的数据库系统的事务隔离规范。
Hibernate只添加自动版本管理,而不会锁定内存中的对象,也不会改变数据库事务的隔离级别。
基本上,使用Hibernate就好像直接使用JDBC(或者JTA/CMT)来访问你的数据库资源。
除了自动版本管理,针对行级悲观锁定,Hibernate也提供了辅助的API,它使用了SELECTFORUPDATE的SQL语法。
本章后面会讨论这个API。
我们从Configuration层、SessionFactory层,和Session层开始讨论Hibernate的并行控制、数据库事务和应用程序的长事务。
12.1. Session和事务范围(transactionscopes)
一个SessionFactory对象的创建代价很昂贵,它是线程安全的对象,它被设计成可以为所有的应用程序线程所共享。
它只创建一次,通常是在应用程序启动的时候,由一个Configuraion的实例来创建。
一个Session的对象是轻型的,非线程安全的,对于单个业务进程,单个的工作单元而言,它只被使用一次,然后就丢弃。
只有在需要的时候,Session才会获取一个JDBC的Connection(或一个Datasource)对象。
所以你可以放心的打开和关闭Session,甚至当你并不确定一个特定的请求是否需要数据访问时,你也可以这样做。
(一旦你实现下面提到的使用了请求拦截的模式,这就变得很重要了。
此外我们还要考虑数据库事务。
数据库事务应该尽可能的短,降低数据库锁定造成的资源争用。
数据库长事务会导致你的应用程序无法扩展到高的并发负载。
一个操作单元(Unitofwork)的范围是多大?
单个的HibernateSession能跨越多个数据库事务吗?
还是一个Session的作用范围对应一个数据库事务的范围?
应该何时打开Session,何时关闭Session?
,你又如何划分数据库事务的边界呢?
12.1.1. 操作单元(Unitofwork)
首先,别再用session-per-operation这种反模式了,也就是说,在单个线程中,不要因为一次简单的数据库调用,就打开和关闭一次Session!
数据库事务也是如此。
应用程序中的数据库调用是按照计划好的次序,分组为原子的操作单元。
(注意,这也意味着,应用程序中,在单个的SQL语句发送之后,自动事务提交(auto-commit)模式失效了。
这种模式专门为SQL控制台操作设计的。
Hibernate禁止立即自动事务提交模式,或者期望应用服务器禁止立即自动事务提交模式。
)
在多用户的client/server应用程序中,最常用的模式是每个请求一个会话(session-per-request)。
在这种模式下,来自客户端的请求被发送到服务器端(即Hibernate持久化层运行的地方),一个新的HibernateSession被打开,并且执行这个操作单元中所有的数据库操作。
一旦操作完成(同时发送到客户端的响应也准备就绪),session被同步,然后关闭。
你也可以使用单个数据库事务来处理客户端请求,在你打开Session之后启动事务,在你关闭Session之前提交事务。
会话和请求之间的关系是一对一的关系,这种模式对于大多数应用程序来说是很棒的。
真正的挑战在于如何去实现这种模式:
不仅Session和事务必须被正确的开始和结束,而且他们也必须能被数据访问操作访问。
用拦截器来实现操作单元的划分,该拦截器在客户端请求达到服务器端的时候开始,在服务器端发送响应(即,ServletFilter)之前结束。
我们推荐使用一个ThreadLocal变量,把Session绑定到处理客户端请求的线程上去。
这种方式可以让运行在该线程上的所有程序代码轻松的访问Session(就像访问一个静态变量那样)。
你也可以在一个ThreadLocal变量中保持事务上下文环境,不过这依赖于你所选择的数据库事务划分机制。
这种实现模式被称之为ThreadLocalSession和OpenSessioninView。
你可以很容易的扩展本文前面章节展示的HibernateUtil辅助类来实现这种模式。
当然,你必须找到一种实现拦截器的方法,并且可以把拦截器集成到你的应用环境中。
请参考Hibernate网站上面的提示和例子。
12.1.2. 应用程序事务(Applicationtransactions)
session-per-request模式不仅仅是一个可以用来设计操作单元的有用概念。
很多业务处理流程都需要一系列完整的和用户之间的交互,即用户对数据库的交叉访问。
在基于web的应用和企业应用中,跨用户交互的数据库事务是无法接受的。
考虑下面的例子:
∙在界面的第一屏,打开对话框,用户所看到的数据是被一个特定的Session和数据库事务载入(load)的。
用户可以随意修改对话框中的数据对象。
∙5分钟后,用户点击“保存”,期望所做出的修改被持久化;同时他也期望自己是唯一修改这个信息的人,不会出现修改冲突。
从用户的角度来看,我们把这个操作单元称为应用程序长事务(applicationtransaction)。
在你的应用程序中,可以有很多种方法来实现它。
头一个幼稚的做法是,在用户思考的过程中,保持Session和数据库事务是打开的,保持数据库锁定,以阻止并发修改,从而保证数据库事务隔离级别和原子操作。
这种方式当然是一个反模式,因为数据库锁定的维持会导致应用程序无法扩展并发用户的数目。
很明显,我们必须使用多个数据库事务来实现一个应用程序事务。
在这个例子中,维护业务处理流程的事务隔离变成了应用程序层的部分责任。
单个应用程序事务通常跨越多个数据库事务。
如果仅仅只有一个数据库事务(最后的那个事务)保存更新过的数据,而所有其他事务只是单纯的读取数据(例如在一个跨越多个请求/响应周期的向导风格的对话框中),那么应用程序事务将保证其原子性。
这种方式比听起来还要容易实现,特别是当你使用了Hibernate的下述特性的时候:
∙自动版本化-Hibernate能够自动进行乐观并发控制,如果在用户思考的过程中发生并发修改冲突,Hibernate能够自动检测到。
∙脱管对象(DetachedObjects)-如果你决定采用前面已经讨论过的session-per-request模式,所有载入的实例在用户思考的过程中都处于与Session脱离的状态。
Hibernate允许你把与Session脱离的对象重新关联到Session上,并且对修改进行持久化,这种模式被称为session-per-request-with-detached-objects。
自动版本化被用来隔离并发修改。
∙长生命周期的Session(LongSession)-Hibernate的Session可以在数据库事务提交之后和底层的JDBC连接断开,当一个新的客户端请求到来的时候,它又重新连接上底层的JDBC连接。
这种模式被称之为session-per-application-transaction,这种情况可能会造成不必要的Session和JDBC连接的重新关联。
自动版本化被用来隔离并发修改。
session-per-request-with-detached-objects和session-per-application-transaction各有优缺点,我们在本章后面乐观并发控制那部分再进行讨论。
12.1.3. 关注对象标识(Consideringobjectidentity)
应用程序可能在两个不同的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实例允许共享的话,那些支持并发运行的东东,例如HTTPrequest,sessionbeans,或者是Swingworkers,将会导致出现资源争用(racecondition)。
如果在HttpSession中有Hibernate的Session的话(稍后讨论),你应该考虑同步访问你的Httpsession。
否则,只要用户足够快的点击浏览器的“刷新”,就会导致两个并发运行线程使用同一个Session。
∙一个由Hibernate抛出的异常意味着你必须立即回滚数据库事务,并立即关闭Session(稍后会展开讨论)。
如果你的Session绑定到一个应用程序上,你必须停止该应用程序。
回滚数据库事务并不会把你的业务对象退回到事务启动时候的状态。
这意味着数据库状态和业务对象状态不同步。
通常情况下,这不是什么问题,因为异常是不可恢复的,你必须在回滚之后重新开始执行。
∙Session缓存了处于持久化状态的每个对象(Hibernate会监视和检查脏数据)。
这意味着,如果你让Session打开很长一段时间,或是仅仅载入了过多的数据,Session占用的内存会一直增长,直到抛出OutOfMemoryException异常。
这个问题的一个解决方法是调用clear()和evict()来管理Session的缓存,但是如果你需要大批量数据操作的话,最好考虑使用存储过程。
在第 14 章批量处理(Batchprocessing)中有一些解决方案。
在用户会话期间一直保持Session打开也意味着出现脏数据的可能性很高。
12.2. 数据库事务声明
数据库(或者系统)事务的声明总是必须的。
在数据库事务之外,就无法和数据库通讯(这可能会让那些习惯于自动提交事务模式的开发人员感到迷惑)。
永远使用清晰的事务声明,即使只读操作也是如此。
进行显式的事务声明并不总是需要的,这取决于你的事务隔离级别和数据库的能力,但不管怎么说,声明事务总归有益无害。
一个Hibernate应用程序可以运行在非托管环境中(也就是独立运行的应用程序,简单Web应用程序,或者Swing图形桌面应用程序),也可以运行在托管的J2EE环境中。
在一个非托管环境中,Hibernate通常自己负责管理数据库连接池。
应用程序开发人员必须手工设置事务声明,换句话说,就是手工启动,提交,或者回滚数据库事务。
一个托管的环境通常提供了容器管理事务,例如事务装配通过可声明的方式定义在EJBsessionbeans的部署描述符中。
可编程式事务声明不再需要,即使是Session的同步也可以自动完成。
让持久层具备可移植性是人们的理想。
Hibernate提供了一套称为Transaction的封装API,用来把你的部署环境中的本地事务管理系统转换到Hibernate事务上。
这个API是可选的,但是我们强烈推荐你使用,除非你用CMTsessionbean。
通常情况下,结束Session包含了四个不同的阶段:
∙同步session(flush,刷出到磁盘)
∙提交事务
∙关闭session
∙处理异常
session的同步(flush,刷出)前面已经讨论过了,我们现在进一步考察在托管和非托管环境下的事务声明和异常处理。
12.2.1. 非托管环境
如果Hibernat持久层运行在一个非托管环境中,数据库连接通常由Hibernate的连接池机制来处理。
session/transaction处理方式如下所示:
//Non-managedenvironmentidiom
Sessionsess=factory.openSession();
Transactiontx=null;
try{
tx=sess.beginTransaction();
//dosomework
...
mit();
}
catch(RuntimeExceptione){
if(tx!
=null)tx.rollback();
throwe;//ordisplayerrormessage
}
finally{
sess.close();
}
你不需要显式flush()Session-对commit()的调用会自动触发session的同步。
调用close()标志session的结束。
close()方法重要的暗示是,session释放了JDBC连接。
这段Java代码是可移植的,可以在非托管环境和JTA环境中运行。
你很可能从未在一个标准的应用程序的业务代码中见过这样的用法;致命的(系统)异常应该总是在应用程序“顶层”被捕获。
换句话说,执行Hibernate调用的代码(在持久层)和处理RuntimeException异常的代码(通常只能清理和退出应用程序)应该在不同的应用程序逻辑层。
这对于你设计自己的软件系统来说是一个挑战,只要有可能,你就应该使用J2EE/EJB容器服务。
异常处理将在本章稍后进行讨论。
请注意,你应该选择org.hibernate.transaction.JDBCTransactionFactory(这是默认选项).
12.2.2. 使用JTA
如果你的持久层运行在一个应用服务器中(例如,在EJBsessionbeans的后面),Hibernate获取的每个数据源连接将自动成为全局JTA事务的一部分。
Hibernate提供了两种策略进行JTA集成。
如果你使用bean管理事务(BMT),可以通过使用Hibernate的TransactionAPI来告诉应用服务器启动和结束BMT事务。
因此,事务管理代码和在非托管环境下是一样的。
//BMTidiom
Sessionsess=factory.openSession();
Transactiontx=null;
try{
tx=sess.beginTransaction();
//dosomework
...
mit();
}
catch(RuntimeExceptione){
if(tx!
=null)tx.rollback();
throwe;//ordisplayerrormessage
}
finally{
sess.close();
}
在CMT方式下,事务声明是在sessionbean的部署描述符中,而不需要编程。
除非你设置了属性hibernate.transaction.flush_before_completion和hibernate.transaction.auto_close_session为true,否则你必须自己同步和关闭Session。
Hibernate可以为你自动同步和关闭Session。
你唯一要做的就是当发生异常时进行事务回滚。
幸运的是,在一个CMTbean中,事务回滚甚至可以由容器自动进行,因为由sessionbean方法抛出的未处理的RuntimeException异常可以通知容器设置全局事务回滚。
这意味着在CMT中,你完全无需使用Hibernate的TransactionAPI。
请注意,当你配置Hibernate事务工厂的时候,在一个BMTsessionbean中,你应该选择org.hibernate.transaction.JTATransactionFactory,在一个CMTsessionbean中选择org.hibernate.transaction.CMTTransactionFactory。
记住,同时也要设置org.hibernate.transaction.manager_lookup_class。
如果你使用CMT环境,并且让容器自动同步和关闭session,你可能也希望在你代码的不同部分使用同一个session。
一般来说,在一个非托管环境中,你可以使用一个ThreadLocal变量来持有这个session,但是单个EJB方法调用可能会在不同的线程中执行(举例来说,一个sessionbean调用另一个sessionbean)。
如果你不想在应用代码中被传递Session对象实例的问题困扰的话,那么SessionFactory提供的getCurrentSession()方法就很适合你,该方法返回一个绑定到JTA事务上下文环境中的session实例。
这也是把Hibernate集成到一个应用程序中的最简单的方法!
这个“当前的”session总是可以自动同步和自动关闭(不考虑上述的属性设置)。
我们的session/transaction管理代码减少到如下所示:
//CMTidiom
Sessionsess=factory.getCurrentSession();
//dosomework
...
换句话来说,在一个托管环境下,你要做的所有的事情就是调用SessionFactory.getCurrentSession(),然后进行你的数据访问,把其余的工作交给容器来做。
事务在你的sessionbean的部署描述符中以可声明的方式来设置。
session的生命周期完全由Hibernate来管理。
对after_statement连接释放方式有一个警告。
因为JTA规范的一个很愚蠢的限制,Hibernate不可能自动清理任何未关闭的ScrollableResults或者Iterator,它们是由scroll()或iterate()产生的。
你must通过在finally块中,显式调用ScrollableResults.close()或者Hibernate.close(Iterator)方法来释放底层数据库游标。
(当然,大部分程序完全可以很容易的避免在CMT代码中出现scroll()或iterate()。
)
12.2.3. 异常处理
如果Session抛出异常(包括任何SQLException),你应该立即回滚数据库事务,调用Session.close(),丢弃该Session实例。
Session的某些方法可能会导致session处于不一致的状态。
所有由Hibernate抛出的异常都视为不可以恢复的。
确保在finally代码块中调用close()方法,以关闭掉Session。
HibernateException是一个非检查期异常(这不同于Hibernate老的版本),它封装了Hibernate持久层可能出现的大多数错误。
我们的观点是,不应该强迫应用程序开发人员在底层捕获无法恢复的异常。
在大多数软件系统中,非检查期异常和致命异常都是在相应方法调用的堆栈的顶层被处理的(也就是说,在软件上面的逻辑层),并且提供一个错误信息给应用软件的用户(或者采取其他某些相应的操作)。
请注意,Hibernate也有可能抛出其他并不属于HibernateException的非检查期异常。
这些异常同样也是无法恢复的,应该采取某些相应的操作去处理。
在和数据库进行交互时,Hibernate把捕获的SQLException封装为Hibernate的JDBCException。
事实上,Hibernate尝试把异常转换为更有实际含义的JDBCException异常的子类。
底层的SQLException可以通过JDBCException.getCause()来得到。
Hibernate通过使用关联到SessionFactory上的SQLExceptionConverter来把SQLException转换为一个对应的JDBCException异常的子类。
默认情况下,SQLExceptionConverter可以通过配置dialect选项指定;此外,也可以使用用户自定义的实现类(参考javadocsSQLExceptionConverterFactory类来了解详情)。
标准的JDBCException子类型是:
∙JDBCConnectionException-指明底层的JDBC通讯出现错误
∙SQLGrammarException-指明发送的SQL语句的语法或者格式错误
∙ConstraintViolationException-指明某种类型的约束违例错误
∙LockAcquisitionException-指明了在执行请求操作时,获取所需的锁级别时出现的错误。
∙GenericJDBCException-不属于任何其他种类的原生异常
12.3. 乐观并发控制(Optimisticconcurrencycontrol)
唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的乐观并发
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 12 事务和并发 事务 并发
![提示](https://static.bdocx.com/images/bang_tan.gif)