(接上文)
4.6 HQL 调优
4.6.1 索引调优
HQL 看起来和 SQL 很相似。从 HQL 的 WHERE 子句中通常可以猜到相应的 SQL WHERE 子句。WHERE 子句中的字段决定了数据库将选择的索引。
大多数 Hibernate 开发者所常犯的一个错误是无论何时,当需要新 WHERE 子句的时候都会创建一个新的索引。因为索引会带来额外的数据更新开销,所以应该争取创建少量索引来覆盖尽可能多的查询。
4.1 节让你使用一个集合来处理所有可能的数据搜索条件。如果这不太实际,那么你可以使用后端剖析工具来创建一个针对应用程序涉及的所有 SQL 的集合。基于那些搜索条件的分类,你最终会得到一个小的索引集。与此同时,还可以尝试向 WHERE 子句中添加额外的谓语来匹配其他 WHERE 子句。
范例 7
有两个 UI 搜索器和一个后端守护进程搜索器来搜索名为 iso_deals 的表。第一个 UI 搜索器在 unexpectedFlag、dealStatus、tradeDate 和 isold 属性上有谓语。
第二个 UI 搜索器基于用户键入的过滤器,其中包括的内容除 tradeDate 和 isold 以外还有其他属性。开始时所有这些过滤器属性都是可选的。
后端搜索器基于 isold、participantCode 和 transactionType 属性。
经过进一步业务分析,发现第二个 UI 搜索器实际是基于一些隐式的 unexpectedFlag 和 dealStatus 值来选择数据的。我们还让 tradeDate 成为过滤器的必要属性(为了使用数据库索引,每个搜索过滤器都应该有必要属性)。鉴于这一点,我们依次使用 unexpectedFlag、dealStatus、tradeDate 和 isold 构造了一个复合索引。两个 UI 搜索器都能共用它。(顺序很重要,如果你的谓语以不同的顺序指定这些属性或在它们前罗列了其他属性,数据库就不会选择该复合索引。)
后端搜索器和 UI 搜索器区别太大,因此我们不得不为它构造另一个复合索引,依次使用 isold、participantCode 和 transactionType。
4.6.2 绑定参数 vs. 字符串拼接
既可以使用绑定参数构造 HQL 的 WHERE 子句,也可以使用字符串拼接的方法,该决定对性能会有一定影响。使用绑定参数的原因是让数据库一次解析 SQL,对后续的重复请求复用生成好的执行计划,这样做节省了 CPU 时间和内存。然而,为达到最优的数据访问效率,不同的绑定值可能需要不同的 SQL 执行计划。
例如,一小段数据范围可能只返回数据总量的 5%,而一大段数据范围可能返回数据总量的 90%。前者使用索引更好,而后者则最好使用全表扫描。
建议 OLTP 使用绑定参数,数据仓库使用字符串拼接,因为 OLTP 通常在一个事务中重复插入和更新数据,只取少量数据;数据仓库通常只有少量 SQL 查询,有一个确定的执行计划比节省 CPU 时间和内存更为重要。
要是你知道你的 OLTP 搜索对不同绑定值应该使用相同执行计划又该怎么办呢?
Oracle 9i 及以后版本在第一次调用绑定参数并生成执行计划时能探出参数值。后续调用不会再探测,而是重用之前的执行计划。
4.6.3 聚合及排序
你可以在数据库中进行聚合和“order by”,也可以在应用程序的服务层中事先加载所有数据然后做聚合和“order by”操作。推荐使用前者,因为数据库在这方面通常会比你的应用程序做得好。此外,这样做还能节省网络带宽,这也是一种拥有跨数据库移植性的做法。
当你的应用程序对数据聚合和排序有 HQL 不支持的特定业务规则时除外。
4.6.4 覆盖抓取策略
详见4.7.1 节。
4.6.5 本地查询
本地查询调优其实并不直接与 HQL 有关。但 HQL 的确可以让你直接向底层数据库传递本地查询。我们并不建议这么做,因为本地查询在数据库间不可移植。
4.7 抓取策略调优
抓取策略决定了在应用程序需要访问关联对象时,Hibernate 以何种方式以及何时获取关联对象。HRD 中的第20 章“改善性能”对该主题作了很好的阐述,我们在此将关注它的使用方法。
4.7.1 覆盖抓取策略
不同的用户可能会有不同的数据抓取要求。Hibernate 允许在两个地方定义数据抓取策略,一处是在映射元数据中,另一处是在 HQL 或 Criteria 中覆盖它。
常见的做法是基于主要的抓取用例在映射元数据中定义默认抓取策略,针对少数用例在 HQL 和 Criteria 中覆盖抓取策略。
假设 pojoA 和 pojoB 是父子关系实例。如果根据业务规则,只是偶尔需要从实体两端加载数据,那你可以声明一个延迟加载集合或代理抓取(proxy fetching)。当你需要从实体两端获取数据时,可以用立即抓取(eager fetching)覆盖默认策略,例如使用 HQL 或 Criteria 配置连接抓取(join fetching)。
另一方面,如果业务规则在大多数时候需要从实体两端加载数据,那么你可以声明立即抓取并在 Criteria 中设置延迟加载集合或代理抓取来覆盖它(HQL 目前还不支持这样的覆盖)。
4.7.2 N+1 模式或是反模式?
select 抓取会导致 N+1 问题。如果你知道自己总是需要从关联中加载数据,那么就该始终使用连接抓取。在下面两个场景中,你可能会把 N+1 视为一种模式而非反模式。
第一种场景,你不知道用户是否会访问关联对象。如果他 / 她没有访问,那么你赢了;否则你仍然需要额外的 N 次 select SQL 语句。这是一种令人左右为难的局面。
第二种场景,pojoA 和很多其他 POJO 有 one-to-many 关联,例如 pojoB 和 pojoC。使用立即的内连接或外连接抓取会在结果集中将 pojoA 重复很多次。当 pojoA 中有很多非空属性时,你不得不将大量数据加载到持久层中。这种加载需要很多时间,既有网络带宽的原因,如果 Hibernate 的会话是有状态的,其中也会有会话缓存的原因(内存消耗和 GC 暂停)。
如果你有一个很长的 one-to-many 关联链,例如从 pojoA 到 pojoB 到 pojoC 以此类推,情况也是类似的。
你也许会去使用 HQL 中的 DISTINCT 关键字或 Cirteria 中的 distinct 功能或是 Java 的 Set 接口来消除重复数据。但所有这些都是在 Hibernate(在持久层)中实现的,而非数据库中。
如果基于你的网络和内存配置的测试表明 N+1 性能更好,那么你可以使用批量抓取、subselect 抓取或二级缓存来做进一步调优。
范例 8
以下是一个使用批量抓取的 HBM 文件片段:
复制代码
<<span color="#0080ff">class</span> <span color="#800000">name</span>=<i>"<span color="#0080ff">pojoA</span>" </i><span color="#800000">table</span>=<i>"<span color="#0080ff">pojoA</span>"</i>> … <<span color="#0080ff">set</span> <span color="#800000">name</span>=<i>"<span color="#0080ff">pojoBs</span>"</i> <span color="#800000">fetch</span>=<i>"<span color="#0080ff">select</span>"</i> <span color="#800000">batch-size</span>=<i>"<span color="#0080ff">10</span>"</i>> <<span color="#0080ff">key</span> <span color="#800000">column</span>=<i>"<span color="#0080ff">pojoa_id</span>"</i>/> … </<span color="#0080ff">set</span>> </<span color="#0080ff">class</span>>以下是多端 pojoB 生成的 SQL:
<b><span color="#800000">select</span></b> … <b><span color="#800000">from</span></b> pojoB <b><span color="#800000">where</span></b> pojoa_id <b><span color="#800000">in</span></b>(?,?,?,?,?, ?,?,?,?,?);
问号数量与 batch-size 值相等。因此 N 次额外的关于 pojoB 的 select SQL 语句被减少到了 N/10 次。如果将fetch="select"替换成fetch=“subselect”,pojoB 生成的 SQL 语句就是这样的:
<b><span color="#800000">select</span></b> … <b><span color="#800000">from</span></b> pojoB <b><span color="#800000">where</span></b> pojoa_id <b><span color="#800000">in</span></b>(<b><span color="#800000">select</span></b> id <b><span color="#800000">from</span></b> pojoA <b><span color="#800000">where</span></b> …);
尽管 N 次额外的 select 减少到 1 次,但这只在重复运行 pojoA 的查询开销很低时才有好处。如果 pojoA 中的 pojoB 集合很稳定,或 pojoB 有 pojoA 的 many-to-one 关联,而且 pojoA 是只读引用数据,那么你可以使用二级缓存来缓存 pojoA 以消除 N+1 问题(4.8.1 节中有一个例子)。
4.7.3 延迟属性抓取
除非有一张拥有很多你不需要的字段的遗留表,否则不应该使用这种抓取策略,因为它的延迟属性分组会带来额外的 SQL。
在业务分析和设计过程中,你应该将不同数据获取或修改分组放到不同的领域对象实体中,而不是使用这种抓取策略。
如果不能重新设计遗留表,可以使用 HQL 或 Criteria 提供的投影功能来获取数据。
4.8 二级缓存调优
HRD第 20.2 节 “二级缓存”中的描述对大多数开发者来说过于简单,无法做出选择。3.3 版及以后版本不再推荐使用基于“CacheProvider”的缓存,而用基于“RegionFactory”的缓存,这也让人更糊涂了。但是就算是最新的 3.5 参考文档也没有提及如何使用新缓存方法。
出于下述考虑,我们将继续关注于老方法:
- 所有流行的 Hibernate 二级缓存提供商中只有 JBoss Cache 2 、 Infinispan 4 和 Ehcache 2 支持新方法。 OSCache 、 SwarmCache 、 Coherence 和 Gigaspaces XAP-Data Grid 只支持老方法。
- 两种方法共用相同的
配置。例如,它们仍旧使用相同的 usage 属性值“transactional|read-write|nonstrict-read-write|read-only”。 - 多个 cache-region 适配器仍然内置老方法的支持,理解它能帮助你快速理解新方法。
4.8.1 基于 CacheProvider 的缓存机制
理解该机制是做出合理选择的关键。关键的类 / 接口是 CacheConcurrencyStrategy 和它针对 4 中不同缓存使用的实现类,还有 EntityUpdate/Delete/InsertAction。
针对并发缓存访问,有三种实现模式:
-
针对“read-only”的只读模式。
无论是锁还是事务都没影响,因为缓存自数据从数据库加载后就不会改变。 -
针对“read-write”和“nonstrict-read-write”的非事务感知(non-transaction-aware)读写模式。
对缓存的更新发生在数据库事务完成后。缓存需要支持锁。 -
针对“transactional”的事务感知读写。
对缓存和数据库的更新被包装在同一个 JTA 事务中,这样缓存与数据库总是保持同步的。数据库和缓存都必须支持 JTA。尽管缓存事务内部依赖于缓存锁,但 Hibernate 不会显式调用任何的缓存锁函数。
以数据库更新为例。EntityUpdateAction 对于事务感知读写、“read-write”的非事务感知读写,还有“nonstrict-read-write”的非事务感知读写相应有如下调用序列:
- 在一个 JTA 事务中更新数据库;在同一个事务中更新缓存。
- 软锁缓存;在一个事务中更新数据库;在上一个事务成功完成后更新缓存;否则释放软锁。
软锁只是一种特定的缓存值失效表述方式,在它获得新数据库值前阻止其他事务读写缓存。那些事务会转而直接读取数据库。
缓存必须支持锁;事务支持则不是必须的。如果缓存是一个集群,“更新缓存”的调用会将新值推送给所有副本,这通常被称为“推(push)”更新策略。
- 在一个事务中更新数据库;在上一个事务完成前就清除缓存;为了安全起见,无论事务成功与否,在事务完成后再次清除缓存。
既不需要支持缓存锁,也不需要支持事务。如果是缓存集群,“清除缓存”调用会让所有副本都失效,这通常被称为“拉(pull)”更新策略。
对于实体的删除或插入动作,或者集合变更,调用序列都是相似的。
实际上,最后两个异步调用序列仍能保证数据库和缓存的一致性(基本就是“read committed”的隔离了级别),这要归功于第二个序列中的软锁和“更新数据库”后的“更新缓存”,还有最后一个调用序列中的悲观“清除缓存”。
基于上述分析,我们的建议是:
-
如果数据是只读的,例如引用数据,那么总是使用“read-only”策略,因为它是最简单、最高效的策略,也是集群安全的策略。
-
除非你真的想将缓存更新和数据库更新放在一个 JTA 事务里,否则不要使用“transactional”策略,因为 JTA 需要漫长的两阶段提交处理,这导致它基本是性能最差的策略。
依笔者看来,二级缓存并非一级数据源,因此使用 JTA 也未必合理。实际上最后两个调用序列在大多数场景下是个不错的替代方案,这要归功于它们的数据一致性保障。 -
如果你的数据读很多或者很少有并发缓存访问和更新,那么可以使用“nonstrict-read-write”策略。感谢它的轻量级“拉”更新策略,它通常是性能第二好的策略。
-
如果你的数据是又读又写的,那么使用“read-write”策略。这通常是性能倒数第二的策略,因为它要求有缓存锁,缓存集群中使用重量级的“推”更新策略。
范例 9
以下是一个 ISO 收费类型的 HBM 文件片段:
复制代码
<<span color="#0080ff">class</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">IsoChargeType</span>"></i> <<span color="#0080ff">property</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">isoId</span>"</i> <span color="#800000">column</span>=<i>"<span color="#0000ff">ISO_ID</span>"</i> <span color="#800000">not-null</span>=<i>"<span color="#0000ff">true</span>"</i>/> <<span color="#0080ff">many-to-one</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">estimateMethod</span>"</i> <span color="#800000">fetch</span>=<i>"<span color="#0000ff">join</span>"</i> <span color="#800000">lazy</span>=<i>"<span color="#0000ff">false</span>"</i>/> <<span color="#0080ff">many-to-one</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">allocationMethod</span>"</i> <span color="#800000">fetch</span>=<i>"<span color="#0000ff">join</span>"</i> <span color="#800000">lazy</span>=<i>"<span color="#0000ff">false</span>"</i>/> <<span color="#0080ff">many-to-one</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">chargeTypeCategory</span>"</i> <span color="#800000">fetch</span>=<i>"<span color="#0000ff">join</span>"</i> <span color="#800000">lazy</span>=<i>"<span color="#0000ff">false</span>"</i>/> </<span color="#0080ff">class</span>>一些用户只需要 ISO 收费类型本身;一些用户既需要 ISO 收费类型,还需要它的三个关联对象。简单起见,开发者会立即加载所有三个关联对象。如果项目中没人负责 Hibernate 调优,这是很常见的。
4.7.1 节中讲过了最好的方法。因为所有的关联对象都是只读引用数据,另一种方法是使用延迟抓取,打开这些对象的二级缓存以避免 N+1 问题。实际上前一种方法也能从引用数据缓存中获益。
因为大多数项目都有很多被其他数据引用的只读引用数据,上述两种方法都能改善全局系统性能。
4.8.2 RegionFactory
下表是新老两种方法中对应的主要类 / 接口:
新方法
老方法
RegionFactory
CacheProvider
Region
Cache
EntityRegionAccessStrategy
CacheConcurrencyStrategy
CollectionRegionAccessStrategy
CacheConcurrencyStrategy
第一个改进是 RegionFactory 构建了特定的 Region,例如 EntityRegion 和 TransactionRegion,而不是使用一个通用的访问 Region。第二个改进是对于特定缓存的“usage”属性值,Region 要求构建自己的访问策略,而不是所有 Region 都一直使用 CacheConcurrencyStrategy 的 4 种实现。
要使用新方法,应该设置 factory_class 而非 provider_class 配置属性。以 Ehcache 2.0 为例:
<property name="hibernate.cache.region.factory_class"> net.sf.ehcache.hibernate.EhCacheRegionFactory </property>
其他相关的 Hibernate 缓存配置都和老方法一样。
新方法也能向后兼容遗留方法。如果还是只配了 CacheProvider,新方法中将使用下列自说明(self-explanatory)适配器和桥隐式地调用老的接口 / 类:
RegionFactoryCacheProviderBridge、EntityRegionAdapter、CollectionRegionAdapter、QueryResultsRegionAdapter、EntityAccessStrategyAdapter 和 CollectionAccessStrategyAdapter
4.8.3 查询缓存
二级缓存也能缓存查询结果。如果查询开销很大而且要重复运行,这也会很有帮助。
4.9 批量处理调优
大多数 Hibernate 的功能都很适合那些每个事务都通常只处理少量数据的 OLTP 系统。但是,如果你有一个数据仓库或者事务需要处理大量数据,那么就另当别论了。
4.9.1 使用有状态会话的非 DML 风格批处理
如果你已经在使用常规会话了,那这是最自然的方法。你需要做三件事:
- 配置下列 3 个属性以开启批处理特性:
hibernate.jdbc.batch_size 30 hibernate.jdbc.batch_versioned_data true hibernate.cache.use_second_level_cache false
batch_size 设置为正值会开启 JDBC2 的批量更新,Hibernate 的建议值是 5 到 30。基于我们的测试,极低值和极高值性能都很差。只要取值在合理范围内,区别就只有几秒而已。如果网络够快,这个结果是一定的。
第二个配置设为 true,这要求 JDBC 驱动在 executeBatch() 方法中返回正确的行数。对于 Oracle 用户而言,批量更新时不能将其设为 true。请阅读 Oracle 的《JDBC Developer’s Guide and Reference》中的“标准批处理的Oracle 实现中的更新计数”( Update Counts in the Oracle Implementation of Standard Batching )以获得更多详细信息。因为它对批量插入来说还是安全的,所以你可以为批量插入创建单独的专用数据源。最后一个配置项是可选的,因为你可以在会话中显式关闭二级缓存。
- 像如下范例中那样定期刷新(flush)并清除一级会话缓存:
Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); <p> for ( int i=0; i<100000; i++ ) {<br></br> Customer customer = new Customer(.....);<br></br> //if your hibernate.cache.use_second_level_cache is true, call the following:<br></br> session.setCacheMode(CacheMode.IGNORE);<br></br> session.save(customer);<br></br> if (i % 50 == 0) { //50, same as the JDBC batch size<br></br> //flush a batch of inserts and release memory:<br></br> session.flush();<br></br> session.clear();<br></br> }<br></br> }<br></br> tx.commit();<br></br> session.close();</p>
批处理通常不需要数据缓存,否则你会将内存耗尽并大量增加 GC 开销。如果内存有限,那这种情况会很明显。
- 总是将批量插入嵌套在事务中。
每次事务修改的对象数量越少就意味着会有更多数据库提交,正如4.5 节所述每次提交都会带来磁盘相关的开销。
另一方面,每次事务修改的对象数量越多就意味着锁定变更时间越长,同时数据库需要更大的 redo log。
4.9.2 使用无状态会话的非 DML 风格批处理
无状态会话执行起来比上一种方法更好,因为它只是 JDBC 的简单包装,而且可以绕开很多常规会话要求的操作。例如,它不需要会话缓存,也不和任何二级缓存或查询缓存有交互。
然而它的用法并不简单。尤其是它的操作并不会级联到所关联的实例上;你必须自己来处理它们。
4.9.3 DML 风格
使用 DML 风格的插入、更新或删除,你直接在数据库中操作数据,这和前两种方法在 Hibernate 中操作数据的情况有所不同。
因为一个 DML 风格的更新或删除相当于前两种方法中的多个单独的更新或删除,所以如果更新或删除中的 WHERE 子句暗示了恰当的数据库索引,那么使用 DML 风格的操作能节省网络开销,执行得更好。
强烈建议结合使用 DML 风格操作和无状态会话。如果使用有状态会话,不要忘记在执行 DML 前清除缓存,否则 Hibernate 将会更新或清除相关缓存(见下面的范例 10)。
4.9.4 批量加载
如果你的 HQL 或 Criteria 会返回很多数据,那么要注意两件事:
- 用下列配置开启批量抓取特性:
hibernate.jdbc.fetch_size 10
fetch_size 设置为正值将开启 JDBC 批量抓取特性。相对快速网络,在慢速网络中这一点更为重要。Oracle 建议的经验值是 10。你应该基于自己的环境进行测试。
- 在使用上述任一方法时都要关闭缓存,因为批量加载一般是一次性任务。受限于内存容量,向缓存中加载大量数据通常也意味着它们很快会被清除出去,这会增加 GC 开销。
范例 10
我们有一个后台任务,分段加载大量的 IsoDeal 数据用于后续处理。我们还会在分段数据交给下游系统处理前将其更新为处理中状态。最大的一段有 50 万行数据。以下是原始代码中截取出来的一段:
复制代码
Query query = session.createQuery("<span color="#0000ff">FROM IsoDeal d WHERE chunk-clause</span>"); query.setLockMode(<span color="#0000ff">"d"</span>, LockMode.<i><span color="#0000ff">UPGRADE</span></i>); //for Inprocess status update List<IsoDeal> isoDeals = query.list(); for (IsoDeal isoDeal : isoDeals) { //update status to Inprocess isoDeal.setStatus<span color="#0000ff">("Inprocess"</span>); } return isoDeals;包含上述代码的方法加上了 Spring 2.5 声明式事务的注解。加载并更新 50 万行数据大约花了 10 分钟。我们识别出了以下这些问题:
- 由于会话缓存和二级缓存的原因,系统会频繁地内存溢出。
- 就算没有内存溢出,当内存消耗很高时 GC 的开销也会很大。
- 我们还未设置 fetch_size。
- 就算我们设置了 batch_size,for 循环也创建了太多 update SQL 语句。
不幸的是 Spring 2.5 不支持 Hibernate 无状态会话,所以我们只能关闭二级缓存;设置 fetch_size;用 DML 风格的更新来代替 for 循环,以此改善性能。
但是,执行时间还是要 6 分钟。将 Hibernate 的日志级别调成 trace 后,我们发现是更新会话缓存造成了延时。通过在 DML 更新前清除会话缓存,我们将时间缩短到了 4 分钟,全部都是将数据加载到会话缓存中花费的时间。
4.10 SQL 生成调优
本节将向你展示如何减少 SQL 生成的数量。
4.10.1 N+1 抓取问题
“select 抓取”策略会导致 N+1 问题。如果“连接抓取”策略适合你的话,你应该始终使用该策略避免 N+1 问题。
但是,如果“连接抓取”策略执行效果不理想,就像4.7.2 节中那样,你可以使用“subselect 抓取”、“批量抓取”或“延迟集合抓取”来减少所需的额外 SQL 语句数。
4.10.2 Insert+Update 问题
范例 11
我们的 ElectricityDeal 与 DealCharge 有单向 one-to-many 关联,如下列 HBM 文件片段所示:
复制代码
<<span color="#0080ff">class</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">ElectricityDeal</span>"</i> <span color="#800000">select-before-update</span>=<i>"<span color="#0000ff">true</span>"</i> <span color="#800000">dynamic-update</span>=<i>"<span color="#0000ff">true</span>"</i> <span color="#800000">dynamic-insert</span>=<i>"<span color="#0000ff">true</span>"</i>> <span color="#0080ff"><id</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">key</span>"</i> <span color="#800000">column</span>=<i>"<span color="#0000ff">ID</span>"</i>> <span color="#0080ff"><generator</span> <span color="#800000">class</span>=<i>"<span color="#0000ff">sequence</span>"</i>> <span color="#0080ff"><param</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">sequence</span>"</i>>SEQ_ELECTRICITY_DEALS<<span color="#0080ff">/param</span>> <span color="#0080ff"></generator></span> <span color="#0080ff"></id><br></br> …<br></br> <set</span> <span color="#800000">name</span>=<i>"<span color="#0000ff">dealCharges</span>" </i><span color="#800000">cascade</span>=<i>"<span color="#0000ff">all-delete-orphan</span>"></i> <span color="#0080ff"><key </span><span color="#800000">column</span>=<i>"<span color="#0000ff">DEAL_KEY</span>"</i> <span color="#800000">not-null</span>=<i>"<span color="#0000ff">false</span>"</i> <span color="#800000">update</span>=<i>"<span color="#0000ff">true</span>"</i> <span color="#800000">on-delete</span>=<i>"<span color="#0000ff">noaction</span>"</i>/> <span color="#0080ff"><one-to-many</span> <span color="#0000ff">class</span>=<i>"<span color="#0000ff">DealCharge</span>"</i>/> <span color="#0080ff"></set> </class></span>
在“key”元素中,“not-null”和“update”对应的默认值是 false 和 true,上述代码为了明确这些取值,将它们写了出来。
如果你想创建一个 ElectricityDeal 和十个 DealCharge,会生成如下 SQL 语句:
- 1 句 ElectricityDeal 的插入语句;
- 10 句 DealCharge 的插入语句,其中不包括外键“DEAL_KEY”;
- 10 句 DealCharge 字段“DEAL_KEY”的更新语句。
为了消除那额外的 10 句更新语句,可以在那 10 句 DealCharge 插入语句中包含“DEAL_KEY”,你需要将“not-null”和“update”分别修改为 true 和 false。
另一种做法是使用双向或 many-to-one 关联,让 DealCharge 来管理关联。
4.10.3 更新前执行 select
在范例 11 中,我们为 ElectricityDeal 加上了 select-before-update,这会对瞬时(transient)对象或分离(detached)对象产生额外的 select 语句,但却能避免不必要的数据库更新。
你应该做出一些权衡,如果对象没多少属性,不需要防止不必要的数据库更新,那么就不要使用该特性,因为你那些有限的数据既没有太多网络传输开销,也不会带来太多数据库更新开销。
如果对象的属性较多,例如是一张大的遗留表,那你应该开启该特性,和“dynamic-update”结合使用以避免太多数据库更新开销。
4.10.4 级联删除
在范例 11 中,如果你想删除 1 个 ElectricityDeal 和它的 100 个 DealCharge,Hibernate 会对 DealCharge 做 100 次删除。
如果将“on-delete”修改为“cascade”,Hibernate 不会执行 DealCharge 的删除动作;而是让数据库根据 ON CASCADE DELETE 约束自动删除那 100 个 DealCharge。不过,需要让 DBA 开启 ON CASCADE DELETE 约束,大多数 DBA 不愿意这么做,因为他们想避免父对象的意外删除级联到它的依赖对象上。此外,还要注意,该特性会绕过 Hibernate 对版本数据(versioned data)的常用乐观锁策略。
4.10.5 增强的序列标识符生成器
范例 11 中使用 Oracle 的序列作为标识符生成器。假设我们保存 100 个 ElectricityDeal,Hibernate 会将下面的 SQL 语句执行 100 次来获取下一个可用的标识符:
<b><span color="#800000">select</span></b> SEQ_ELECTRICITY_DEALS.NEXTVAL <b><span color="#800000">from</span></b> dual;
如果网络不是很快,那这无疑会降低效率。3.2.3 及后续版本中增加了一个增强的生成器“SequenceStyleGenerator”,它带了两个优化器:hilo 和 pooled。尽管 HRD 的第 5 章“基础 O/R 映射” 讲到了这两个优化器,不过内容有限。两个优化器都使用了 HiLo 算法,该算法生成的标识符等于 Hi 值加上 Lo 值,其中 Hi 值代表组号,Lo 值顺序且重复地从 1 迭代到最大组大小,组号在 Lo 值“转回到”1 时加 1。
假设组大小是 5(可以用 max_lo 或 increment_size 参数来表示),下面是个例子:
-
hilo 优化器
组号取自数据库序列的下一个可用值,Hi 值由 Hibernate 定义,是组号乘以 increment_size 参数值。 -
pooled 优化器
Hi 值直接取自数据库序列的下一个可用值。数据库序列的增量应该设置为 increment_size 参数值。
直到内存组中的值耗尽后,两个优化器才会去访问数据库,上面的例子每 5 个标识值符访问一次数据库。使用 hilo 优化器时,你的序列不能再被其他应用程序使用,除非它们使用与 Hibernate 相同的逻辑。使用 pooled 优化器,在其他应用程序使用同一序列时则相当安全。
两个优化器都有一个问题,如果 Hibernate 崩溃,当前组内的一些标识符值就会丢失,然而大多数应用程序都不要求拥有连续的标识符值(如果你的数据库,比方说 Oracle,缓存了序列值,当它崩溃时你也会丢失标识符值)。
如果在范例 11 中使用 pooled 优化器,新的 id 配置如下:
<span color="#0080ff"><id</span> <span color="#800000">name</span>=<i>"key"</i> <span color="#800000">column</span>=<i>"<span color="#0000ff">ID"</span></i><span color="#0000ff">></span> <span color="#0080ff"><generator</span> <span color="#800000">class</span>=<span color="#0000ff"><i>"org.hibernate.id.enhance</i>.<i>SequenceStyleGenerator"</i>><br></br></span> <span color="#0080ff"><param</span> <span color="#800000">name</span>=<span color="#0000ff"><i>"sequence_name"</i>></span>SEQ_ELECTRICITY_DEALS<span color="#0080ff"></param></span> <span color="#0080ff"><param</span> <span color="#800000">name</span>=<span color="#0000ff"><i>"initial_value"</i>></span>0<span color="#0080ff"></param></span> <span color="#0080ff"><param</span> <span color="#800000">name</span>=<span color="#0000ff"><i>"increment_size"</i>></span>100<span color="#0080ff"></param></span> <span color="#0080ff"><param</span> <span color="#800000">name</span>=<span color="#0000ff"><i>"optimizer "</i>></span>pooled<span color="#0080ff"></param></span> <span color="#0080ff"></generator></span> <span color="#0080ff"></id></span>
5 总结
本文涵盖了大多数你在 Hibernate 应用程序调优时会觉得很有用的调优技巧,其中的大多数时间都在讨论那些行之有效却缺乏文档的调优主题,例如继承映射、二级缓存和增强的序列标识符生成器。
它还提到了一些 Hibernate 调优所必需的数据库知识。一些范例中包含了你可能遇到的问题的实际解决方案。
除此之外,值得一提的是 Hibernate 也可以和 In-Memory Data Grid(IMDG)一起使用,例如 Oracle 的 Coherance 或 GigaSpaces IMDG,这能让你的应用程序达到毫秒级别。
6 资源
[1] Latest Hibernate Reference Documentation on jboss.com
[2] Oracle 9i Performance Tuning Guide and Reference
[3] Performance Engineering on Wikipedia
[4] Program Optimization on Wikipedia
[5] Pareto Principle (the 80/20 rule) on Wikipedia
[6] Premature Optimization on acm.org
[7] Java Performance Tuning by Jack Shirazi
[8] The Law of Leaky Abstractions by Joel Spolsky
[9] Hibernate’s StatisticsService Mbean configuration with Spring
[11] Java VisualVM
[12] Column-oriented DBMS on Wikipedia
[13] Apache DBCP BasicDataSource
[14] JDBC Connection Pool by Oracle
[15] Connection Failover by Oracle
[16] Last Resource Commit Optimization (LRCO)
[17] GigaSpaces for Hibernate ORM Users
关于作者
Yongjun Jiao是 SunGard Consulting Services 的技术主管。过去 10 年中他一直是专业软件开发者,他的专长包括 Java SE、Java EE、Oracle 和应用程序调优。他最近的关注点是高性能计算,包括内存数据网格、并行计算和网格计算。
Stewart Clark是 SunGard Consulting Services 的负责人。过去 15 年中他一直是专业软件开发者和项目经理,他的专长包括 Java 核心编程、Oracle 和能源交易。
查看英文原文: Revving Up Your Hibernate Engine
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论