加速你的hibernate引擎
2010年11月01日
1. 简介
Hibernate是最流行提供数据固话和查询的ORM引擎之一。
在你的项目中引入Hibernate并使其可以工作是非常简单的。然而,使其工作的非常好则需要会费很多的时间以及大量的经验。
通过我们使用Hibernate3.3.1以及Oracle9i的energy项目中的一些例子,这篇文章介绍了Hibernate调优用到的一些技术。
我们假设您已对Hibernate具有最基本的了解。对于某些在Hibernate官方文档(HRD)或者其他的调优文章中有所讲述,我们将仅仅提供一个文档的引用以及从一个不同视角的简单解释。我们主要聚焦在一些很有效但是又缺乏文档的条有方法。 调优是围绕着软件开发生命周期(SDLC)所有阶段的一个持续不断的过程。在一个典型的利用Hibernate做固化的J2EE应用中,调优覆盖下面几个领域: 业务规则调优
设计调优
Hibernate调优
java GC调优
应用容器调优
底层系统,如数据库和操作系统的调优。
在没有一个规划好的技术方案的情况下就对以上几个方面进行调优将会是很耗时间,并且可能是不起作用的。优秀的调优方案中一个很重要的方面就是确定调优领域的优先级。根据帕拉图原理(亦即二八原则)的解释,80%的应用性能提高来自于所有性能问题中最主要的20%[5]。
相对于基于网络的访问,基于内存和CPU的访问具有较低的延迟以及较好的吞吐率。鉴于此, 基于IO的Hibernate调优以及底层系统IO部分的调优优先于基于内存和CPU的GC调优以及底层系统中基于内存和CPU部分的调优。
示例一 我们将一个用于查询电信交易的HQL从耗时30秒调优至少于一秒。如果我们对垃圾回收器(GC)进行调优,提高的效果将会小很多-或许仅仅是几毫秒,最多也就是几秒钟(译注:java的操作都是毫秒级的,所以提高的效果有限),相对于HQL的性能提高,这种提高几乎可以忽略不计。 优秀调优方案的另外一个重要问题就是决定何时做优化。【4】
积极优化的倡导者建议从项目的开始就进行优化,例如在业务规则和设计阶段,在整个项目开发生命周期(SDLC)中持续优化,因为他们认为i,在后期改变业务规则和设计的代价经常会非常大。 消极优化的倡导者则建议在SDLC的后期进行优化。因为他们认为,早期的优化将会使设计和编码复杂化。他们经常以Donald Knuth的"过早的优化时罪恶的根源(premature optimization is the root of all evil)"作为论据【6】 我们需要一个调优和编码的权衡。根据笔者的经验,早期的适当优化,将会带来更谨慎的设计以及更仔细的编码。许多项目由于应用调优而失败,原因就是之前提到的"过早优化"被断章取义,从而导致优化被推到项目的极端后期或者投入非常少的资源。
不过,也不可能进行很多的前期优化,因为如果没有事先的分析(profiling),你根本不知道应用的瓶颈在哪里,而且在此之上,应用经常还在不断的变化中。
通过对多层的企业级应用的监控可以发现,大部分的应用平均仅仅占用了20%-50%的CPU。剩余的CPU仅仅是超额得等待数据库和网络相关的IO。
基于以上的分析,我们推断出:与业务规则和设计一起的Hibernate调优属于二八原则中的20%,他们应该具有更高的优先级。
如下是一个有效的行为: 确定一些主要的瓶颈-可以预见的是大部分的瓶颈出现在Hibernate,业务规则和设计当中(具体的数目取决于你调优的目标,通常3到5个是一个不错的开端)。
修改你的应用来消除这些瓶颈。
测试你的应用,然后重复以上步骤,直到达到你的调优目标。
关于性能调优策略的更一般性建议,请参阅 Jack Shirazi's book "Java Performance Tuning" [7].
以下的章节中,我们将根据调优效果大小的顺序讲述一些调优的方法(越靠前的往往调优效果越大) 如果不对Hibernate应用进行足够的监控和剖析,你将无从得知性能的瓶颈所在以及何处需要优化。 尽管使用Hibernate的主要目的是使你避免直接处理SQL,但是为了对你的项目进行调优,你必须直到Hibernate生成的SQL。 Joel Splosky 在他的论文"The Law of Leaky Abstractions."详尽得描述了这个问题。
在log4j的配置文件中,只要将包org.hibernate.SQL的log 级别设置为DEBUG就可以看到生成的SQL。你也可能需要将其它的包的log级别设置成DEBUG,甚至TRACE,从而精确定位一些性能问题。 如果你打开了hibernate.generate.statistics, 通过SessionFactory.getStatistics(),Hibernate将会展示对调优非常有用的entity, collection, session, second level cache, query 以及 session factory的数据.。为了简单化,通过使用MBean "org.hibernate.jmx.StatisticsService",Hibernate亦可以展示这些数据。您可以从以下链接得到详细信息 a configuration sample from this web site. 一个好的剖析工具不仅仅有利于Hibernate的调优,同样有利于其它模块的调优。然而,大部分的商业工具,例如JProbe,都是非常昂贵的。幸运的是, Sun/Oracle 的 JDK 1.6自带了一个名为 "Java VisualVM" [11]的剖析接口。相对于商业竞争对手它还非常简单,但是它的确可以提供大量调试和调优的信息。 尽管业务规则和设计的调优不属于Hibernate调优的范畴,基于这个调优做出的决定将对以后的Hibernate调优产生很大的影响。基于此,我们重点关注了和Hibernate相关的几个关键点。
通过业务需求的收集和调优,你应该知晓: 数据检索的特征包括引用数据,只读的数据,读取组,读取的大小,检索条件以及数据分组和聚合。
数据修改的特征包括数据的修改,修改组,修改大小,错误修改的订正,相关的数据库(所以数据都在同一个数据库还是跨越多个数据库),修改的频率以及并发性,修改的响应时间以及吞吐率的要求。
数据关系,诸如关联,泛化,实现以及依赖等。
基于业务需求,你将提供一个最理想的设计方案。在设计中,你将定义应用的类型(在线的事务流程(OLTP), 数据仓库,或者和两者相近的东东),分层的结构(分成persistence和service层或者组合在一起),创建领域对象,也就是常说的POJO,并且决定数据聚合的地方(聚合在数据库可以利用数据库功能强大的特点,并且节省网络带宽;然而,除了基本的诸如COUNT, AVG, MIN以及MAX,它并不具有可移植性。 存放在应用服务器上可以让你支持更复杂的业务逻辑,不过你需要先将这些详细的数据加载到应用层)。
示例2 分析员需要查看取自一个很大的数据库表的电力ISO(Independent System Operator)的聚合列表。开始他们希望可以列出大部分的数据库字段。尽管数据库在一分钟内作出了响应,应用还是花费了大约30分钟将多大100万行的数据加载到前端的UI。经过重新分析后,分析员移除了大部分的列而只留下14列。由于很多高基数列被移除,剩下列的聚合组将返回比之前少的多的数据,而数据加载的时间在大部分情况也也被缩减到可以接受的值。 示例3 电量分时交易经常会修改24个定形的(shaped)时段,这些时段包括2个属性: 分时的电量和价格("定形的"就是指每个小时可以有自己的用量和价格;如果24个小时具有相同的用量和价格,我们称之为标准的("standard"))。 之前我们使用HIbernate的"select-before-update"功能,这意味着当我们需要update24行数据的时候,我们需要24个查询操作。因为我们只有2个属性,而且当一个使用量或者price没有变化的时候,又没有业务规则禁止这种update,我们禁用了"select-before-update"这个功能,从而避免了24个选择查询。 尽管集成映射是领域对象的一部分,由于其重要性,我们将单独对待它。 已经有很好讲述,这里我们主要关注SQL的生成策略以及对应的推荐的调优方案。
下面是HRD中的class diagram:
[这幅图中包括一个"CreditCardType"的属性, 不过一下的SQL中都用 "cc_type"引用] [b]4.2.1 每个类层次一张表(注:即将所有的父类和子类放在同一张表中,hibernate本身支持这种功能的) [/b]
实际上只需要一张表即可。一个多态的查询将生成如下的SQL: 对子类,如CashPayment的查询将生成如下的SQL: 第一个查询只在一张表内,方便检索,而且很容易和其他表关联。此外,第二个查询也不包括在其他子类的属性。这些特点使得性能调优要比其他策略方便的多。由于不需要关联(join)其他的表,所以这种方式和适合数据仓库系统。
最大缺点就是在整个类层次中,需要维护具有所有属性的一个很大的表。如果这个类层次中有很多子类具有特有的一些属性,数据库中将会有很多值为null的列,这将给基于行的数据库SQL调优带来很大的困难(数据仓库系统经常使用基于列的DBMS来处理这种场景)。除非做了切分,否则因为这张表很容易成为热点,很典型的,如OLTP就不能够很好的工作。 [b]4.2.2 每个子类一张表(译注:即父类对应的表保存共有的属性,子类的表保存子类特有属性,子类的表通过外键和父类表关联) [/b]
这时需要四张表,如果是一个多态查询生成的SQL: 对于子类,如CashPayment的查询将生成如下的SQL: 优点是具有简洁的表结构(没有不需要的,可以为NUll的列),数据被切分到三个子类的表中,并且可以很容易通过顶层的父类和其他表关联。简洁的表结构可以优化基于行的数据库的存储块,从而是SQL具有更好的性能。 数据切分增加了数据修改的并发(除了父类,没有热点),OLTP系统通常可以处理之。
其次,第二个query也不需要引入其他子类的属性。
缺点是在所有策略中,它使用的表以及表连接最多,SQL语句也很复杂(看看Hibernate动态鉴别器长长的CASE子句)。相对于单张表,数据库需要花费更多时间来调优数据表连接,使用该策略时数据仓库的功效通常不太理想。
因为你不能使用跨越父类和子类表的列来创建复合索引,所以当你需要在这些列做查询的时候(译注:即同时对父类和子类表的某些列做查询),性能会变得很糟。此外,父类的任何改变将会影响到两张表:父类表和子类表 。 [b]4.2.3 每个具体类一张表(译注:即父类不建表,而每个子类一张表,不同子类表之间会有重复的列) [/b]
需要引入三张或者更多的表。多态查询生成的SQL如下所示: select p.id, p.amount, p.currency, p.rtn, p. credit_card_type, p.clazz from (select id, amount, currency, null as rtn,null as credit_card type, 1 as clazz from cash_payment union all select id, amount, null as currency, rtn,null as credit_card type, 2 as clazz from cheque_payment union all select id, amount, null as currency, null as rtn,credit_card type, 3 as clazz from credit_payment) p; 对具体类,如CashPayment 的查询生成的SQL如下所示: 优点和上面的"每个子类一张表"策略很相似。由于父类通常是abstract(译注:即父类只是个abstract class,不会建表),实际上只需要三张表。任何数据改动只会影响一张表,所以运行的速度较快。
缺点是会产生复杂的SQL(FROM中的子查询以及union all)。不过,大部分的数据库都可以很好的优化这类的sql。
如果一个类想与父类Payment关联,数据库做不到真正意义上的关联(译注:因为父类并不是真正存在的),你只有通过触发器来做到这点。这将会给数据库的性能带来影响。 只需要3张表。一个在Payment上的多态查询对每个子类生成独立的SQL。Hibernate引擎通过反射可以为Payment找出所有的三个子类。对每个具体类的查询只会生成它特有的SQL。这些SQL语句非常简单,所以就不在此列出。
优点和以上的类似,简洁的表结构,数据被切分到子类的表中,任何数据的改动只会影响一张表。
缺点是用3条独立的SQL语句替代之前的一条带UNION的SQL语句,将会带来更大的网络IO消耗。JAVA的反射机制同样会消耗时间。可以设想一下,如果你有一大堆的领域对象,需要查询最顶层的类究竟会花费多少时间。
对映射策略做一个合适的选择并不容易。这需要你仔细调优你的业务需求,然后基于不同的数据场景做出合适的选择。
如下式一些建议: 设计细粒度的类层次结构和粗粒度的数据库表结构。细粒度的表结构意味着需要更多的表关联,从而导致复杂的查询。
除非需要,不要使用多态查询。如上所述,对具体类的查询只会选择必需的数据,而不会有不需要的表关联和表联合。
"每个类层次一张表"的策略适用于数据仓库系统(基于列的数据库)和具有较低的并发性,而且大部分的列式共享的OLTP系统。
"每个子类一张表"适用于具有高并发性,查询简单,并且很少列的共享的OLTP系统。如果你希望通过数据库实现真实的关联以及强制关联,这也是个不错的选择。
"每个具体类一张表"适用于具有高并发,复杂查询,较少列共享的OLTP系统。当然,你将不得不牺牲父类和子类之间的关联关系。
混合使用这些策略,比如你可以在"每个子类一张表"中嵌入"每个类层次一张表",这样你就可以利用不同策略的优点了。随着你的项目的演进,当你不得不重新设计映射策略的时候,你就会求助于这种方法。
"利用隐式多台的每个具体类每张表"策略是不被推荐的,因为它繁琐的配置,复杂的关联,使用"any"元素的语法以及隐式多态的潜在危险。
如下是一个交易查询应用的领域对象类图的一部分:
开始的时候,这个项目只有GasDeal以及少量的用户。它使用的是"每个类层次一张表"的策略。后来随着越来越多的业务要求的提出,增加了OilDeal和ElectricityDeal。映射策略没有改变。然而,ElectricityDeal具有太多自己特有的属性,与之相伴的是很多与之相关的,可以为null的列被添加到了Deal 这张表中。随着数据量的增加,数据变化逐渐变缓。作为重新设计,我们采用两个独立的表来保存Gas/Oil和electricity特有的属性。新的映射是"每个类层次一张表"和"每个子类一张表"的混合体。我们也重新设计了查询语句,使之可以在具体类上查询,从而去除了不必要的列以及关联。 基于 Section 4.1中描述的对业务规则和设计的优化,我们可以得到通过POJO描述领域对象的类图。我们的建议如下: 将注入引用之类的只读数据和以读为主的数据从读写数据中分离出来(译注:类似我们常说的读写分离)。对于只读数据,二级缓存是最有效的方案,其次是对以读为主的数据的非严格读写。将只读的POJO标记为immutable(不可变的)也是一个调优点。如果Service层的方法只是对只读数据的处理,你可以将其事务标为只读,这也是优化HIbernate和底层的JDBC driver的一个方案。
细粒度的POJO和粗粒度的数据库表: 基于数据更改的频率和并发性等,将一个大的POJO分割成小的POJO。尽管你可以定义一个力度非常细的对象模型,但是粒度过细的表将带来过多的表连接,而这是数据仓库所不能接受的。
优先使用非final类:Hibernate利用CGLIB 代理实现的延迟关联抓取只会对非final的类起作用。如果你关联的类是final的,Hibernate会直接将所有数据加载进来,这将对性能产生很大的破坏。
对于游离的(detached )实例,利用你的业务规则实现equals()和hashCode()方法。在多层的系统中,人们通常对游离对象使用乐观锁,从而提高系统的并发性,以获得较高的性能。
定义一个verison或者timestamp的属性:在长对话(conversion)中(应用级事务),对于乐观锁,这样的一个列是必需的。(译注:hibernate本身支持version这个功能的,应该不需要自己单独写的吧)
优先使用组合对象:前端UI所使用的数据通常来自于几个不同的POJO。传送一个组合的POJO到UI比传递多个独立的POJO具有较好的网络性能。有两种方法在Service层构建这个组合的POJO,其一是先将所有需要的POJO加载出来,然后将锁需要的属性提取出来放到组合POJO中;另外一个方法是通过HQL直接从数据库中查出所需要的属性。如果这些独立的POJO还会被其他的POJO引用,并且他们是放在二级缓存中的,推荐第一种方案。否则建议使用第二种。
如果关联关系可以使用one-to-one, one-to-many或者many-to-one,就不要使用many-to-many。多对多的关联将会需要一个而外的映射表。尽管在java代码中尼只需要处理两端的POJO,但是数据库在查询的时候需要关联额外的映射表,在修改的时候也需要而外的添加或者删除操作。
Due to the many-to-many nature, loading from one side of a bidirectional association can trigger loading of the other side which can further trigger extra data loading of the original side, and so on.
You can make similar arguments for bidirectional one-to-many and many-to-one when you navigate from the one side (the children entities) to the many side (the parent entity).
This back and forth loading takes time and may not be what you want.
Tuning collection
Use the "order-by" attribute instead of "sort" if your collection sorting logic can be implemented by the underlying database because the database usually does a better sorting job than you.
Collections can either model value types (element or composite-element) or entity reference types (one-to-many or many-to-many associations). Tuning the collection of reference types is mainly tuning fetch strategy. For tuning collections of value types, Section 20.5 "Understanding Collection Performance" in HRD [1] already has good coverage.
Tuning fetch strategy. Please see Section 4.7Example 5
We have a core POJO called ElectricityDeals to capture electricity deals. From a business perspective, it has dozens of many-to-one associations with reference POJOs such as Portfolio, Strategy and Trader, just to name a few. Because the reference data is pretty stable, they are cached at the front end and can be quickly looked up based on their ID properties. In order to have good loading performance, the ElectricityDeal mapping metadata only defines the value-typed ID properties of those reference POJOs because the front end can quickly look up the portfolio from cache based on a portfolioKey if needed:
This implicit association avoids database table joins and extra selections, and cuts down data transfer size. [b]4.4 Tuning the Connection Pool [/b]
Because making a physical database connection is time consuming, you should always use a connection pool. Furthermore, you should always use a production level connection pool instead of Hibernate's internal rudimentary pooling algorithm. You usually provide Hibernate with a datasource which provides the pooling function. A popular open source and production level datasource is Apache DBCP's BasicDataSource [13]. Most database vendors also implement their own JDBC 3.0-compliant connection pools. For example, you can also get connection load balancing and failover using the Oracle provided JDBC connection pool [14] along with Oracle Real Application Cluster [15]. Needless to say you can find plenty of connection pool tuning techniques on the web. Accordingly we will only mention common tuning parameters that are shared by most pools: Min pool size: the minimum number of connections that can remain in the pool.
Max pool size: the maximum number of connection that can be allocated from the pool.
If your application has high concurrency and your maximum pool size is too small, your connection pool will often experience waiting. On the other hand, if your minimum pool size is too large, you may have allocated unnecessary connections.
Max idle time: the maximum time a connection may sit idle in the pool before being physically closed.
Max wait time: the maximum time the pool will wait for a connection to be returned. This can prevent runaway transactions.
Validation query: the SQL query that is used to validate connections before returning them to the caller. This is because some databases are configured to kill long idle connections and a network or database related exception may also kill a connection. In order to reduce this overhead, a connection pool can run validation while it is idle.
[b]4.5 Tuning Transactions and Concurrency [/b]
Short database transactions are essential for any highly performing and scalable applications. You deal with transactions using a session which represents a conversation request to process a single unit of work. Regarding the scope of unit of work and transaction boundary demarcation, there are 3 patterns: Session-per-operation. Each database call needs a new session and transaction. Because your true business transaction usually encompasses several such operations and a large number of small transactions generally incur more database activities (the primary one is the database needs to flush changes to disk for each commit), application performance suffers. Accordingly it is an anti-pattern and shouldn't be used.
Session-per-request-with-detached-objects. Each client request has a new session and a single transaction. You use Hibernate's "current session" feature to associate the two together.
In a multi-tier system, users usually initiate long conversations (or application transactions). Most times we use Hibernate's automatic versioning and detached objects to achieve optimistic concurrent control and high performance an
Session-per-conversion-with-extended (or long)-session. You keep the session open for a long conversation which may span several transactions. Although it saves you from reattachment, the session may grow out of memory and probably has stale data for high concurrency systems.
You also should be aware of the following points. Use local transactions if you don't need to use JTA because JTA requires many more resources and is much slower than local transactions. Even when you have more than one datasource, you don't need JTA unless you have transactions spanning more than one datasource. In this last case you can consider using local transactions on each datasource using a technique similar to "Last Resource Commit Optimization" [16] (see Example 6below for details).
Mark your transaction as read-only if it doesn't involve data changes as mentioned in Section 4.3.1
Always set up a default transaction timeout. It ensures that no misbehaving transaction can tie up resources while returning no response to the user. It even works for local transactions.
Optimistic locking will not work if Hibernate is not the sole database user, unless you create database triggers to increment the version column for the same data change by other applications.
Example 6
Our application has several service layer methods which only deal with database "A" in most instances; however occasionally they also retrieve read-only data from database "B". Because database "B" only provides read-only data, we still use local transactions on both databases for those methods. The service layer does have one method involving data changes on both databases. Here is the pseudo-code: //Make sure a local transaction on database A exists @Transactional (readOnly=false, propagation=Propagation.REQUIRED) publicvoid saveIsoBids() { //it participates in the above annotated local transaction insertBidsInDatabaseA(); //it runs in its own local transaction on database B insertBidRequestsInDatabaseB(); //must be the last operation
Because insertBidRequestsInDatabaseB() is the last operation in saveIsoBids (), only the following scenario can cause data inconsistency: The local transaction on database "A" fails to commit when the execution returns from saveIsoBids (). However even if you use JTA for saveIsoBids (), you still get data inconsistency when the second commit phase fails in the two phase commit (2PC) process. So if you can deal with the above data inconsistency and really don't want JTA complexities for just one or a few methods, you should use local transactions.
2010年11月01日
1. 简介
Hibernate是最流行提供数据固话和查询的ORM引擎之一。
在你的项目中引入Hibernate并使其可以工作是非常简单的。然而,使其工作的非常好则需要会费很多的时间以及大量的经验。
通过我们使用Hibernate3.3.1以及Oracle9i的energy项目中的一些例子,这篇文章介绍了Hibernate调优用到的一些技术。
我们假设您已对Hibernate具有最基本的了解。对于某些在Hibernate官方文档(HRD)或者其他的调优文章中有所讲述,我们将仅仅提供一个文档的引用以及从一个不同视角的简单解释。我们主要聚焦在一些很有效但是又缺乏文档的条有方法。 调优是围绕着软件开发生命周期(SDLC)所有阶段的一个持续不断的过程。在一个典型的利用Hibernate做固化的J2EE应用中,调优覆盖下面几个领域: 业务规则调优
设计调优
Hibernate调优
java GC调优
应用容器调优
底层系统,如数据库和操作系统的调优。
在没有一个规划好的技术方案的情况下就对以上几个方面进行调优将会是很耗时间,并且可能是不起作用的。优秀的调优方案中一个很重要的方面就是确定调优领域的优先级。根据帕拉图原理(亦即二八原则)的解释,80%的应用性能提高来自于所有性能问题中最主要的20%[5]。
相对于基于网络的访问,基于内存和CPU的访问具有较低的延迟以及较好的吞吐率。鉴于此, 基于IO的Hibernate调优以及底层系统IO部分的调优优先于基于内存和CPU的GC调优以及底层系统中基于内存和CPU部分的调优。
示例一 我们将一个用于查询电信交易的HQL从耗时30秒调优至少于一秒。如果我们对垃圾回收器(GC)进行调优,提高的效果将会小很多-或许仅仅是几毫秒,最多也就是几秒钟(译注:java的操作都是毫秒级的,所以提高的效果有限),相对于HQL的性能提高,这种提高几乎可以忽略不计。 优秀调优方案的另外一个重要问题就是决定何时做优化。【4】
积极优化的倡导者建议从项目的开始就进行优化,例如在业务规则和设计阶段,在整个项目开发生命周期(SDLC)中持续优化,因为他们认为i,在后期改变业务规则和设计的代价经常会非常大。 消极优化的倡导者则建议在SDLC的后期进行优化。因为他们认为,早期的优化将会使设计和编码复杂化。他们经常以Donald Knuth的"过早的优化时罪恶的根源(premature optimization is the root of all evil)"作为论据【6】 我们需要一个调优和编码的权衡。根据笔者的经验,早期的适当优化,将会带来更谨慎的设计以及更仔细的编码。许多项目由于应用调优而失败,原因就是之前提到的"过早优化"被断章取义,从而导致优化被推到项目的极端后期或者投入非常少的资源。
不过,也不可能进行很多的前期优化,因为如果没有事先的分析(profiling),你根本不知道应用的瓶颈在哪里,而且在此之上,应用经常还在不断的变化中。
通过对多层的企业级应用的监控可以发现,大部分的应用平均仅仅占用了20%-50%的CPU。剩余的CPU仅仅是超额得等待数据库和网络相关的IO。
基于以上的分析,我们推断出:与业务规则和设计一起的Hibernate调优属于二八原则中的20%,他们应该具有更高的优先级。
如下是一个有效的行为: 确定一些主要的瓶颈-可以预见的是大部分的瓶颈出现在Hibernate,业务规则和设计当中(具体的数目取决于你调优的目标,通常3到5个是一个不错的开端)。
修改你的应用来消除这些瓶颈。
测试你的应用,然后重复以上步骤,直到达到你的调优目标。
关于性能调优策略的更一般性建议,请参阅 Jack Shirazi's book "Java Performance Tuning" [7].
以下的章节中,我们将根据调优效果大小的顺序讲述一些调优的方法(越靠前的往往调优效果越大) 如果不对Hibernate应用进行足够的监控和剖析,你将无从得知性能的瓶颈所在以及何处需要优化。 尽管使用Hibernate的主要目的是使你避免直接处理SQL,但是为了对你的项目进行调优,你必须直到Hibernate生成的SQL。 Joel Splosky 在他的论文"The Law of Leaky Abstractions."详尽得描述了这个问题。
在log4j的配置文件中,只要将包org.hibernate.SQL的log 级别设置为DEBUG就可以看到生成的SQL。你也可能需要将其它的包的log级别设置成DEBUG,甚至TRACE,从而精确定位一些性能问题。 如果你打开了hibernate.generate.statistics, 通过SessionFactory.getStatistics(),Hibernate将会展示对调优非常有用的entity, collection, session, second level cache, query 以及 session factory的数据.。为了简单化,通过使用MBean "org.hibernate.jmx.StatisticsService",Hibernate亦可以展示这些数据。您可以从以下链接得到详细信息 a configuration sample from this web site. 一个好的剖析工具不仅仅有利于Hibernate的调优,同样有利于其它模块的调优。然而,大部分的商业工具,例如JProbe,都是非常昂贵的。幸运的是, Sun/Oracle 的 JDK 1.6自带了一个名为 "Java VisualVM" [11]的剖析接口。相对于商业竞争对手它还非常简单,但是它的确可以提供大量调试和调优的信息。 尽管业务规则和设计的调优不属于Hibernate调优的范畴,基于这个调优做出的决定将对以后的Hibernate调优产生很大的影响。基于此,我们重点关注了和Hibernate相关的几个关键点。
通过业务需求的收集和调优,你应该知晓: 数据检索的特征包括引用数据,只读的数据,读取组,读取的大小,检索条件以及数据分组和聚合。
数据修改的特征包括数据的修改,修改组,修改大小,错误修改的订正,相关的数据库(所以数据都在同一个数据库还是跨越多个数据库),修改的频率以及并发性,修改的响应时间以及吞吐率的要求。
数据关系,诸如关联,泛化,实现以及依赖等。
基于业务需求,你将提供一个最理想的设计方案。在设计中,你将定义应用的类型(在线的事务流程(OLTP), 数据仓库,或者和两者相近的东东),分层的结构(分成persistence和service层或者组合在一起),创建领域对象,也就是常说的POJO,并且决定数据聚合的地方(聚合在数据库可以利用数据库功能强大的特点,并且节省网络带宽;然而,除了基本的诸如COUNT, AVG, MIN以及MAX,它并不具有可移植性。 存放在应用服务器上可以让你支持更复杂的业务逻辑,不过你需要先将这些详细的数据加载到应用层)。
示例2 分析员需要查看取自一个很大的数据库表的电力ISO(Independent System Operator)的聚合列表。开始他们希望可以列出大部分的数据库字段。尽管数据库在一分钟内作出了响应,应用还是花费了大约30分钟将多大100万行的数据加载到前端的UI。经过重新分析后,分析员移除了大部分的列而只留下14列。由于很多高基数列被移除,剩下列的聚合组将返回比之前少的多的数据,而数据加载的时间在大部分情况也也被缩减到可以接受的值。 示例3 电量分时交易经常会修改24个定形的(shaped)时段,这些时段包括2个属性: 分时的电量和价格("定形的"就是指每个小时可以有自己的用量和价格;如果24个小时具有相同的用量和价格,我们称之为标准的("standard"))。 之前我们使用HIbernate的"select-before-update"功能,这意味着当我们需要update24行数据的时候,我们需要24个查询操作。因为我们只有2个属性,而且当一个使用量或者price没有变化的时候,又没有业务规则禁止这种update,我们禁用了"select-before-update"这个功能,从而避免了24个选择查询。 尽管集成映射是领域对象的一部分,由于其重要性,我们将单独对待它。 已经有很好讲述,这里我们主要关注SQL的生成策略以及对应的推荐的调优方案。
下面是HRD中的class diagram:
[这幅图中包括一个"CreditCardType"的属性, 不过一下的SQL中都用 "cc_type"引用] [b]4.2.1 每个类层次一张表(注:即将所有的父类和子类放在同一张表中,hibernate本身支持这种功能的) [/b]
实际上只需要一张表即可。一个多态的查询将生成如下的SQL: 对子类,如CashPayment的查询将生成如下的SQL: 第一个查询只在一张表内,方便检索,而且很容易和其他表关联。此外,第二个查询也不包括在其他子类的属性。这些特点使得性能调优要比其他策略方便的多。由于不需要关联(join)其他的表,所以这种方式和适合数据仓库系统。
最大缺点就是在整个类层次中,需要维护具有所有属性的一个很大的表。如果这个类层次中有很多子类具有特有的一些属性,数据库中将会有很多值为null的列,这将给基于行的数据库SQL调优带来很大的困难(数据仓库系统经常使用基于列的DBMS来处理这种场景)。除非做了切分,否则因为这张表很容易成为热点,很典型的,如OLTP就不能够很好的工作。 [b]4.2.2 每个子类一张表(译注:即父类对应的表保存共有的属性,子类的表保存子类特有属性,子类的表通过外键和父类表关联) [/b]
这时需要四张表,如果是一个多态查询生成的SQL: 对于子类,如CashPayment的查询将生成如下的SQL: 优点是具有简洁的表结构(没有不需要的,可以为NUll的列),数据被切分到三个子类的表中,并且可以很容易通过顶层的父类和其他表关联。简洁的表结构可以优化基于行的数据库的存储块,从而是SQL具有更好的性能。 数据切分增加了数据修改的并发(除了父类,没有热点),OLTP系统通常可以处理之。
其次,第二个query也不需要引入其他子类的属性。
缺点是在所有策略中,它使用的表以及表连接最多,SQL语句也很复杂(看看Hibernate动态鉴别器长长的CASE子句)。相对于单张表,数据库需要花费更多时间来调优数据表连接,使用该策略时数据仓库的功效通常不太理想。
因为你不能使用跨越父类和子类表的列来创建复合索引,所以当你需要在这些列做查询的时候(译注:即同时对父类和子类表的某些列做查询),性能会变得很糟。此外,父类的任何改变将会影响到两张表:父类表和子类表 。 [b]4.2.3 每个具体类一张表(译注:即父类不建表,而每个子类一张表,不同子类表之间会有重复的列) [/b]
需要引入三张或者更多的表。多态查询生成的SQL如下所示: select p.id, p.amount, p.currency, p.rtn, p. credit_card_type, p.clazz from (select id, amount, currency, null as rtn,null as credit_card type, 1 as clazz from cash_payment union all select id, amount, null as currency, rtn,null as credit_card type, 2 as clazz from cheque_payment union all select id, amount, null as currency, null as rtn,credit_card type, 3 as clazz from credit_payment) p; 对具体类,如CashPayment 的查询生成的SQL如下所示: 优点和上面的"每个子类一张表"策略很相似。由于父类通常是abstract(译注:即父类只是个abstract class,不会建表),实际上只需要三张表。任何数据改动只会影响一张表,所以运行的速度较快。
缺点是会产生复杂的SQL(FROM中的子查询以及union all)。不过,大部分的数据库都可以很好的优化这类的sql。
如果一个类想与父类Payment关联,数据库做不到真正意义上的关联(译注:因为父类并不是真正存在的),你只有通过触发器来做到这点。这将会给数据库的性能带来影响。 只需要3张表。一个在Payment上的多态查询对每个子类生成独立的SQL。Hibernate引擎通过反射可以为Payment找出所有的三个子类。对每个具体类的查询只会生成它特有的SQL。这些SQL语句非常简单,所以就不在此列出。
优点和以上的类似,简洁的表结构,数据被切分到子类的表中,任何数据的改动只会影响一张表。
缺点是用3条独立的SQL语句替代之前的一条带UNION的SQL语句,将会带来更大的网络IO消耗。JAVA的反射机制同样会消耗时间。可以设想一下,如果你有一大堆的领域对象,需要查询最顶层的类究竟会花费多少时间。
对映射策略做一个合适的选择并不容易。这需要你仔细调优你的业务需求,然后基于不同的数据场景做出合适的选择。
如下式一些建议: 设计细粒度的类层次结构和粗粒度的数据库表结构。细粒度的表结构意味着需要更多的表关联,从而导致复杂的查询。
除非需要,不要使用多态查询。如上所述,对具体类的查询只会选择必需的数据,而不会有不需要的表关联和表联合。
"每个类层次一张表"的策略适用于数据仓库系统(基于列的数据库)和具有较低的并发性,而且大部分的列式共享的OLTP系统。
"每个子类一张表"适用于具有高并发性,查询简单,并且很少列的共享的OLTP系统。如果你希望通过数据库实现真实的关联以及强制关联,这也是个不错的选择。
"每个具体类一张表"适用于具有高并发,复杂查询,较少列共享的OLTP系统。当然,你将不得不牺牲父类和子类之间的关联关系。
混合使用这些策略,比如你可以在"每个子类一张表"中嵌入"每个类层次一张表",这样你就可以利用不同策略的优点了。随着你的项目的演进,当你不得不重新设计映射策略的时候,你就会求助于这种方法。
"利用隐式多台的每个具体类每张表"策略是不被推荐的,因为它繁琐的配置,复杂的关联,使用"any"元素的语法以及隐式多态的潜在危险。
如下是一个交易查询应用的领域对象类图的一部分:
开始的时候,这个项目只有GasDeal以及少量的用户。它使用的是"每个类层次一张表"的策略。后来随着越来越多的业务要求的提出,增加了OilDeal和ElectricityDeal。映射策略没有改变。然而,ElectricityDeal具有太多自己特有的属性,与之相伴的是很多与之相关的,可以为null的列被添加到了Deal 这张表中。随着数据量的增加,数据变化逐渐变缓。作为重新设计,我们采用两个独立的表来保存Gas/Oil和electricity特有的属性。新的映射是"每个类层次一张表"和"每个子类一张表"的混合体。我们也重新设计了查询语句,使之可以在具体类上查询,从而去除了不必要的列以及关联。 基于 Section 4.1中描述的对业务规则和设计的优化,我们可以得到通过POJO描述领域对象的类图。我们的建议如下: 将注入引用之类的只读数据和以读为主的数据从读写数据中分离出来(译注:类似我们常说的读写分离)。对于只读数据,二级缓存是最有效的方案,其次是对以读为主的数据的非严格读写。将只读的POJO标记为immutable(不可变的)也是一个调优点。如果Service层的方法只是对只读数据的处理,你可以将其事务标为只读,这也是优化HIbernate和底层的JDBC driver的一个方案。
细粒度的POJO和粗粒度的数据库表: 基于数据更改的频率和并发性等,将一个大的POJO分割成小的POJO。尽管你可以定义一个力度非常细的对象模型,但是粒度过细的表将带来过多的表连接,而这是数据仓库所不能接受的。
优先使用非final类:Hibernate利用CGLIB 代理实现的延迟关联抓取只会对非final的类起作用。如果你关联的类是final的,Hibernate会直接将所有数据加载进来,这将对性能产生很大的破坏。
对于游离的(detached )实例,利用你的业务规则实现equals()和hashCode()方法。在多层的系统中,人们通常对游离对象使用乐观锁,从而提高系统的并发性,以获得较高的性能。
定义一个verison或者timestamp的属性:在长对话(conversion)中(应用级事务),对于乐观锁,这样的一个列是必需的。(译注:hibernate本身支持version这个功能的,应该不需要自己单独写的吧)
优先使用组合对象:前端UI所使用的数据通常来自于几个不同的POJO。传送一个组合的POJO到UI比传递多个独立的POJO具有较好的网络性能。有两种方法在Service层构建这个组合的POJO,其一是先将所有需要的POJO加载出来,然后将锁需要的属性提取出来放到组合POJO中;另外一个方法是通过HQL直接从数据库中查出所需要的属性。如果这些独立的POJO还会被其他的POJO引用,并且他们是放在二级缓存中的,推荐第一种方案。否则建议使用第二种。
如果关联关系可以使用one-to-one, one-to-many或者many-to-one,就不要使用many-to-many。多对多的关联将会需要一个而外的映射表。尽管在java代码中尼只需要处理两端的POJO,但是数据库在查询的时候需要关联额外的映射表,在修改的时候也需要而外的添加或者删除操作。
Due to the many-to-many nature, loading from one side of a bidirectional association can trigger loading of the other side which can further trigger extra data loading of the original side, and so on.
You can make similar arguments for bidirectional one-to-many and many-to-one when you navigate from the one side (the children entities) to the many side (the parent entity).
This back and forth loading takes time and may not be what you want.
Tuning collection
Use the "order-by" attribute instead of "sort" if your collection sorting logic can be implemented by the underlying database because the database usually does a better sorting job than you.
Collections can either model value types (element or composite-element) or entity reference types (one-to-many or many-to-many associations). Tuning the collection of reference types is mainly tuning fetch strategy. For tuning collections of value types, Section 20.5 "Understanding Collection Performance" in HRD [1] already has good coverage.
Tuning fetch strategy. Please see Section 4.7Example 5
We have a core POJO called ElectricityDeals to capture electricity deals. From a business perspective, it has dozens of many-to-one associations with reference POJOs such as Portfolio, Strategy and Trader, just to name a few. Because the reference data is pretty stable, they are cached at the front end and can be quickly looked up based on their ID properties. In order to have good loading performance, the ElectricityDeal mapping metadata only defines the value-typed ID properties of those reference POJOs because the front end can quickly look up the portfolio from cache based on a portfolioKey if needed:
This implicit association avoids database table joins and extra selections, and cuts down data transfer size. [b]4.4 Tuning the Connection Pool [/b]
Because making a physical database connection is time consuming, you should always use a connection pool. Furthermore, you should always use a production level connection pool instead of Hibernate's internal rudimentary pooling algorithm. You usually provide Hibernate with a datasource which provides the pooling function. A popular open source and production level datasource is Apache DBCP's BasicDataSource [13]. Most database vendors also implement their own JDBC 3.0-compliant connection pools. For example, you can also get connection load balancing and failover using the Oracle provided JDBC connection pool [14] along with Oracle Real Application Cluster [15]. Needless to say you can find plenty of connection pool tuning techniques on the web. Accordingly we will only mention common tuning parameters that are shared by most pools: Min pool size: the minimum number of connections that can remain in the pool.
Max pool size: the maximum number of connection that can be allocated from the pool.
If your application has high concurrency and your maximum pool size is too small, your connection pool will often experience waiting. On the other hand, if your minimum pool size is too large, you may have allocated unnecessary connections.
Max idle time: the maximum time a connection may sit idle in the pool before being physically closed.
Max wait time: the maximum time the pool will wait for a connection to be returned. This can prevent runaway transactions.
Validation query: the SQL query that is used to validate connections before returning them to the caller. This is because some databases are configured to kill long idle connections and a network or database related exception may also kill a connection. In order to reduce this overhead, a connection pool can run validation while it is idle.
[b]4.5 Tuning Transactions and Concurrency [/b]
Short database transactions are essential for any highly performing and scalable applications. You deal with transactions using a session which represents a conversation request to process a single unit of work. Regarding the scope of unit of work and transaction boundary demarcation, there are 3 patterns: Session-per-operation. Each database call needs a new session and transaction. Because your true business transaction usually encompasses several such operations and a large number of small transactions generally incur more database activities (the primary one is the database needs to flush changes to disk for each commit), application performance suffers. Accordingly it is an anti-pattern and shouldn't be used.
Session-per-request-with-detached-objects. Each client request has a new session and a single transaction. You use Hibernate's "current session" feature to associate the two together.
In a multi-tier system, users usually initiate long conversations (or application transactions). Most times we use Hibernate's automatic versioning and detached objects to achieve optimistic concurrent control and high performance an
Session-per-conversion-with-extended (or long)-session. You keep the session open for a long conversation which may span several transactions. Although it saves you from reattachment, the session may grow out of memory and probably has stale data for high concurrency systems.
You also should be aware of the following points. Use local transactions if you don't need to use JTA because JTA requires many more resources and is much slower than local transactions. Even when you have more than one datasource, you don't need JTA unless you have transactions spanning more than one datasource. In this last case you can consider using local transactions on each datasource using a technique similar to "Last Resource Commit Optimization" [16] (see Example 6below for details).
Mark your transaction as read-only if it doesn't involve data changes as mentioned in Section 4.3.1
Always set up a default transaction timeout. It ensures that no misbehaving transaction can tie up resources while returning no response to the user. It even works for local transactions.
Optimistic locking will not work if Hibernate is not the sole database user, unless you create database triggers to increment the version column for the same data change by other applications.
Example 6
Our application has several service layer methods which only deal with database "A" in most instances; however occasionally they also retrieve read-only data from database "B". Because database "B" only provides read-only data, we still use local transactions on both databases for those methods. The service layer does have one method involving data changes on both databases. Here is the pseudo-code: //Make sure a local transaction on database A exists @Transactional (readOnly=false, propagation=Propagation.REQUIRED) publicvoid saveIsoBids() { //it participates in the above annotated local transaction insertBidsInDatabaseA(); //it runs in its own local transaction on database B insertBidRequestsInDatabaseB(); //must be the last operation
Because insertBidRequestsInDatabaseB() is the last operation in saveIsoBids (), only the following scenario can cause data inconsistency: The local transaction on database "A" fails to commit when the execution returns from saveIsoBids (). However even if you use JTA for saveIsoBids (), you still get data inconsistency when the second commit phase fails in the two phase commit (2PC) process. So if you can deal with the above data inconsistency and really don't want JTA complexities for just one or a few methods, you should use local transactions.