HIbernate提升性能 20.1. 抓取策略(Fetching strategies) Hibernate3 定义了如下几种抓取策略: •连接抓取(Join fetching) - Hibernate通过 在SELECT语句使用OUTER JOIN(外连接)来 获得对象的关联实例或者关联集合。 •查询抓取(Select fetching) - 另外发送一条 SELECT 语句抓取当前对象的关联实体或集合。除非你显式的指定lazy="false"禁止 延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。 •子查询抓取(Subselect fetching) - 另外发送一条SELECT 语句抓取在前面查询到(或者抓取到)的所有实体对象的关联集合。除非你显式的指定lazy="false" 禁止延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。 •批量抓取(Batch fetching) - 对查询抓取的优化方案, 通过指定一个主键或外键列表,Hibernate使用单条SELECT语句获取一批对象实例或集合。 Hibernate会区分下列各种情况: •Immediate fetching,立即抓取 - 当宿主被加载时,关联、集合或属性被立即抓取。 •Lazy collection fetching,延迟集合抓取- 直到应用程序对集合进行了一次操作时,集合才被抓取。(对集合而言这是默认行为。) •Proxy fetching,代理抓取 - 对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行get操作时才抓取。 •Lazy attribute fetching,属性延迟加载 - 对属性或返回单值的关联而言,当其实例变量被访问的时候进行抓取(需要运行时字节码强化)。这一方法很少是必要的。 这里有两个正交的概念:关联何时被抓取,以及被如何抓取(会采用什么样的SQL语句)。不要混淆它们!我们使用抓取来改善性能。我们使用延迟来定义一些契约,对某特定类的某个脱管的实例,知道有哪些数据是可以使用的。 20.1.1. 操作延迟加载的关联 注意:假若你设置了hibernate.default_batch_fetch_size,Hibernate会对延迟加载采取批量抓取优化措施(这种优化也可能会在更细化的级别打开)。 然而,你必须了解延迟抓取带来的一个问题。在一个打开的Hibernate session上下文之外调用延迟集合会导致一次意外。比如: s = sessions.openSession(); 除此之外,通过对关联映射指定lazy="false",我们也可以使用非延迟的集合或关联。但是, 对绝大部分集合来说,更推荐使用延迟方式抓取数据。如果在你的对象模型中定义了太多的非延迟关联,Hibernate最终几乎需要在每个事务中载入整个数据库到内存中! 但是,另一方面,在一些特殊的事务中,我们也经常需要使用到连接抓取(它本身上就是非延迟的),以代替查询抓取。 下面我们将会很快明白如何具体的定制Hibernate中的抓取策略。在Hibernate3中,具体选择哪种抓取策略的机制是和选择 单值关联或集合关联相一致的。 20.1.2. 调整抓取策略(Tuning fetch strategies) <set name="permissions" •通过get()或load()方法取得数据。 •只有在关联之间进行导航时,才会隐式的取得数据(延迟抓取)。 •条件查询 通常情况下,我们并不使用映射文档进行抓取策略的定制。更多的是,保持其默认值,然后在特定的事务中, 使用HQL的左连接抓取(left join fetch) 对其进行重载。这将通知 Hibernate在{dy}次查询中使用外部关联(outer join),直接得到其关联数据。 在条件查询 API中,应该调用 setFetchMode(FetchMode.JOIN)语句。 也许你喜欢仅仅通过条件查询,就可以改变get() 或 load()语句中的数据抓取策略。例如: User user = (User) session.createCriteria(User.class) 截然不同的一种避免N+1次查询的方法是,使用二级缓存。 20.1.3. 单端关联代理(Single-ended association proxies) 默认的,Hibernate3将会为所有的持久对象产生代理(在启动阶段),然后使用他们实现 多对一(many-to-one)关联和一对一(one-to-one) 关联的延迟抓取。 在映射文件中,可以通过设置proxy属性为目标class声明一个接口供代理接口使用。 默认的,Hibernate将会使用该类的一个子类。 注意:被代理的类必须实现一个至少包可见的默认构造函数,我们建议所有的持久类都应拥有这样的构造函数 在如此方式定义一个多态类的时候,有许多值得注意的常见性的问题,例如: <class name="Cat" proxy="Cat"> Cat cat = (Cat) session.load(Cat.class, id); // instantiate a proxy (does not hit the db) Cat cat = (Cat) session.load(Cat.class, id); // instantiate a Cat proxy cat.setWeight(11.0); // hit the db to initialize the proxy {zh1},如果你的持久化对象在实例化时需要某些资源(例如,在实例化方法、默认构造方法中), 那么代理对象也同样需要使用这些资源。实际上,代理类是持久化类的子类。 这些问题都源于Java的单根继承模型的天生限制。如果你希望避免这些问题,那么你的每个持久化类必须实现一个接口, 在此接口中已经声明了其业务方法。然后,你需要在映射文档中再指定这些接口。例如: <class name="CatImpl" proxy="Cat"> Cat cat = (Cat) session.load(CatImpl.class, catid); 但是,在有些方法中是不需要使用代理的。例如: •equals()方法,如果持久类没有重载equals()方法。 •hashCode()方法,如果持久类没有重载hashCode()方法。 •标志符的getter方法。 Hibernate将会识别出那些重载了equals()、或hashCode()方法的持久化类。 20.1.4. 实例化集合和代理(Initializing collections and proxies) 有时候我们需要保证某个代理或者集合在Session关闭前就已经被初始化了。 当然,我们可以通过强行调用cat.getSex()或者cat.getKittens().size()之类的方法来确保这一点。 但是这样的程序会造成读者的疑惑,也不符合通常的代码规范。 静态方法Hibernate.initialized() 为你的应用程序提供了一个便捷的途径来延迟加载集合或代理。 只要它的Session处于open状态,Hibernate.initialize(cat) 将会为cat强制对代理实例化。 同样,Hibernate.initialize( cat.getKittens() ) 对kittens的集合具有同样的功能。 还有另外一种选择,就是保持Session一直处于open状态,直到所有需要的集合或代理都被载入。 在某些应用架构中,特别是对于那些使用Hibernate进行数据访问的代码,以及那些在不同应用层和不同物理进程中使用Hibernate的代码。 在集合实例化时,如何保证Session处于open状态经常会是一个问题。有两种方法可以解决此问题: •在一个基于Web的应用中,可以利用servlet过滤器(filter),在用户请求(request)结束、页面生成 结束时关闭Session(这里使用了在展示层保持打开Session模式(Open Session in View)), 当然,这将依赖于应用框架中异常需要被正确的处理。在返回界面给用户之前,乃至在生成界面过程中发生异常的情况下, 正确关闭Session和结束事务将是非常重要的, Servlet过滤器必须如此访问Session,才能保证正确使用Session。 我们推荐使用ThreadLocal 变量保存当前的Session (可以参考第 1.4 节 “与Cat同乐”的例子实现)。 •在一个拥有单独业务层的应用中,业务层必须在返回之前,为web层“准备”好其所需的数据集合。这就意味着 业务层应该载入所有表现层/web层所需的数据,并将这些已实例化完毕的数据返回。通常,应用程序应该 为web层所需的每个集合调用Hibernate.initialize()(这个调用必须发生咱session关闭之前); 或者使用带有FETCH从句,或FetchMode.JOIN的Hibernate查询, 事先取得所有的数据集合。如果你在应用中使用了Command模式,代替Session Facade , 那么这项任务将会变得简单的多。 •你也可以通过merge()或lock()方法,在访问未实例化的集合(或代理)之前, 为先前载入的对象绑定一个新的Session。 显然,Hibernate将不会,也不应该自动完成这些任务,因为这将引入一个特殊的事务语义。 有时候,你并不需要xx实例化整个大的集合,仅需要了解它的部分信息(例如其大小)、或者集合的部分内容。 你可以使用集合过滤器得到其集合的大小,而不必实例化整个集合: ( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()这里的createFilter()方法也可以被用来有效的抓取集合的部分内容,而无需实例化整个集合: s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();20.1.5. 使用批量抓取(Using batch fetching) 类/实体级别的批量抓取很容易理解。假设你在运行时将需要面对下面的问题:你在一个Session中载入了25个 Cat实例,每个Cat实例都拥有一个引用成员owner, 其指向Person,而Person类是代理,同时lazy="true"。 如果你必须遍历整个cats集合,对每个元素调用getOwner()方法,Hibernate将会默认的执行25次SELECT查询, 得到其owner的代理对象。这时,你可以通过在映射文件的Person属性,显式声明batch-size,改变其行为: <class name="Person" batch-size="10">...</class>随之,Hibernate将只需要执行三次查询,分别为10、10、 5。 你也可以在集合级别定义批量抓取。例如,如果每个Person都拥有一个延迟载入的Cats集合, 现在,Sesssion中载入了10个person对象,遍历person集合将会引起10次SELECT查询, 每次查询都会调用getCats()方法。如果你在Person的映射定义部分,允许对cats批量抓取, 那么,Hibernate将可以预先抓取整个集合。请看例子: <class name="Person"> 如果你的模型中有嵌套的树状结构,例如典型的帐单-原料结构(bill-of-materials pattern),集合的批量抓取是非常有用的。 (尽管在更多情况下对树进行读取时,嵌套集合(nested set)或原料路径(materialized path)(××) 是更好的解决方法。) 20.1.6. 使用子查询抓取(Using subselect fetching) 20.1.7. 使用延迟属性抓取(Using lazy property fetching) 可以在映射文件中对特定的属性设置lazy,定义该属性为延迟载入。 <class name="Document"> 你可以在Ant的Task中,进行如下定义,对持久类代码加入“二进制指令。” <target name="instrument" depends="compile"> 有时你需要在HQL中通过抓取所有属性,强行抓取所有内容。 20.2. 二级缓存(The Second Level Cache) 默认情况下,Hibernate使用EHCache进行JVM级别的缓存(目前,Hibernate已经废弃了对JCS的支持,未来版本中将会去掉它)。 你可以通过设置hibernate.cache.provider_class属性,指定其他的缓存策略, 该缓存策略必须实现org.hibernate.cache.CacheProvider接口。 表 20.1. 缓存策略提供商(Cache Providers) Cache Provider class Type Cluster Safe Query Cache Supported 20.2.1. 缓存映射(Cache mappings) <cache 另外({sx}?), 你可以在hibernate.cfg.xml中指定<class-cache>和 <collection-cache> 元素。 这里的usage 属性指明了缓存并发策略(cache concurrency strategy)。 20.2.2. 策略:只读缓存(Strategy: read only) <class name="eg.Immutable" mutable="false"> <class name="eg.Cat" .... > 20.2.5. 策略:事务缓存(transactional) 没有一种缓存提供商能够支持上列的所有缓存并发策略。下表中列出了各种提供器、及其各自适用的并发策略。 表 20.2. 各种缓存提供商对缓存并发策略的支持情况(Cache Concurrency Strategy Support) Cache read-only nonstrict-read-write read-write transactional 20.3. 管理缓存(Managing the caches) 当随后flush()方法被调用时,对象的状态会和数据库取得同步。 如果你不希望此同步操作发生,或者你正处理大量对象、需要对有效管理内存时,你可以调用evict() 方法,从一级缓存中去掉这些对象及其集合。 ScrollableResult cats = sess.createQuery("from Cat as cat").scroll(); //a huge result set 如若要把所有的对象从session缓存中彻底xx,则需要调用Session.clear()。 对于二级缓存来说,在SessionFactory中定义了许多方法, xx缓存中实例、整个类、集合实例或者整个集合。 sessionFactory.evict(Cat.class, catId); //evict a particular Cat •CacheMode.NORMAL - 从二级缓存中读、写数据。 •CacheMode.GET - 从二级缓存中读取数据,仅在数据更新时对二级缓存写数据。 •CacheMode.PUT - 仅向二级缓存写数据,但不从二级缓存中读数据。 •CacheMode.REFRESH - 仅向二级缓存写数据,但不从二级缓存中读数据。通过 hibernate.cache.use_minimal_puts的设置,强制二级缓存从数据库中读取数据,刷新缓存内容。 如若需要查看二级缓存或查询缓存区域的内容,你可以使用统计(Statistics) API。 Map cacheEntries = sessionFactory.getStatistics() hibernate.generate_statistics true hibernate.cache.use_query_cache true该设置将会创建两个缓存区域 - 一个用于保存查询结果集(org.hibernate.cache.StandardQueryCache); 另一个则用于保存最近查询的一系列表的时间戳(org.hibernate.cache.UpdateTimestampsCache)。 请注意:在查询缓存中,它并不缓存结果集中所包含的实体的确切状态;它只缓存这些实体的标识符属性的值、以及各值类型的结果。 所以查询缓存通常会和二级缓存一起使用。 绝大多数的查询并不能从查询缓存中受益,所以Hibernate默认是不进行查询缓存的。如若需要进行缓存,请调用 Query.setCacheable(true)方法。这个调用会让查询在执行过程中时先从缓存中查找结果, 并将自己的结果集放到缓存中去。 如果你要对查询缓存的失效政策进行xx的控制,你必须调用Query.setCacheRegion()方法, 为每个查询指定其命名的缓存区域。 List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger") 20.5. 理解集合性能(Understanding Collection performance) 20.5.1. 分类(Taxonomy) •值数据集合 •一对多关联 •多对多关联 这个分类是区分了不同的表和外键关系类型,但是它没有告诉我们关系模型的所有内容。 要xx理解他们的关系结构和性能特点,我们必须同时考虑“用于Hibernate更新或删除集合行数据的主键的结构”。 因此得到了如下的分类: •有序集合类 •集合(sets) •包(bags) 所有的有序集合类(maps, lists, arrays)都拥有一个由<key>和 <index>组成的主键。 这种情况下集合类的更新是非常高效的——主键已经被有效的索引,因此当Hibernate试图更新或删除一行时,可以迅速找到该行数据。 集合(sets)的主键由<key>和其他元素字段构成。 对于有些元素类型来说,这很低效,特别是组合元素或者大文本、大二进制字段; 数据库可能无法有效的对复杂的主键进行索引。 另一方面,对于一对多、多对多关联,特别是合成的标识符来说,集合也可以达到同样的高效性能。( 附注:如果你希望SchemaExport为你的<set>创建主键, 你必须把所有的字段都声明为not-null="true"。) <idbag>映射定义了代理键,因此它总是可以很高效的被更新。事实上, <idbag>拥有着{zh0}的性能表现。 Bag是最差的。因为bag允许重复的元素值,也没有索引字段,因此不可能定义主键。 Hibernate无法判断出重复的行。当这种集合被更改时,Hibernate将会先完整地移除 (通过一个(in a single DELETE))整个集合,然后再重新创建整个集合。 因此Bag是非常低效的。 请注意:对于一对多关联来说,“主键”很可能并不是数据库表的物理主键。 但就算在此情况下,上面的分类仍然是有用的。(它仍然反映了Hibernate在集合的各数据行中是如何进行“定位”的。) 20.5.2. Lists, maps 和sets用于更新效率{zg} 可论证的是对于多对多关联、值数据集合而言,有序集合类比集合(set)有一个好处。因为Set的内在结构, 如果“改变”了一个元素,Hibernate并不会更新(UPDATE)这一行。 对于Set来说,只有在插入(INSERT)和删除(DELETE) 操作时“改变”才有效。再次强调:这段讨论对“一对多关联”并不适用。 注意到数组无法延迟载入,我们可以得出结论,list, map和idbags是{zg}效的(非反向)集合类型,set则紧随其后。 在Hibernate中,set应该时最通用的集合类型,这时因为“set”的语义在关系模型中是最自然的。 但是,在设计良好的Hibernate领域模型中,我们通常可以看到更多的集合事实上是带有inverse="true" 的一对多的关联。对于这些关联,更新操作将会在多对一的这一端进行处理。因此对于此类情况,无需考虑其集合的更新性能。 20.5.3. Bag和list是反向集合类中效率{zg}的 Parent p = (Parent) sess.load(Parent.class, id); 假设我们在一个长度为20的集合类中新增加了一个元素,然后再删除两个。 Hibernate会安排一条INSERT语句和两条DELETE语句(除非集合类是一个bag)。 这当然是显而易见的。 但是,假设我们删除了18个数据,只剩下2个,然后新增3个。则有两种处理方式: •逐一的删除这18个数据,再新增三个; •删除整个集合类(只用一句DELETE语句),然后增加5个数据。 Hibernate还没那么聪明,知道第二种选择可能会比较快。 (也许让Hibernate不这么聪明也是好事,否则可能会引发意外的“数据库触发器”之类的问题。) 幸运的是,你可以强制使用第二种策略。你需要取消原来的整个集合类(解除其引用), 然后再返回一个新的实例化的集合类,只包含需要的元素。有些时候这是非常有用的。 显然,一次性删除并不适用于被映射为inverse="true"的集合。 20.6. 监测性能(Monitoring performance) 20.6.1. 监测SessionFactory 此外,如果你打开StatisticsService MBean选项,那么Hibernate则可以使用JMX技术 发布其数据记录。你可以让应用中所有的SessionFactory同时共享一个MBean,也可以每个 SessionFactory分配一个MBean。下面的代码即是其演示代码: // MBean service registration for a specific SessionFactory 你可以通过以下方法打开或关闭SessionFactory的监测功能: •在配置期间,将hibernate.generate_statistics设置为true或false; •在运行期间,则可以可以通过sf.getStatistics().setStatisticsEnabled(true) 或hibernateStatsBean.setStatisticsEnabled(true) 你也可以在程序中调用clear()方法重置统计数据,调用logSummary() 在日志中记录(info级别)其总结。 20.6.2. 数据记录(Metrics) •使用Session的普通数据记录,例如打开的Session的个数、取得的JDBC的连接数等; •实体、集合、查询、缓存等内容的统一数据记录 •和具体实体、集合、查询、缓存相关的详细数据记录 例如:你可以检查缓存的命中成功次数,缓存的命中失败次数,实体、集合和查询的使用概率,查询的平均时间等。请注意 Java中时间的近似精度是毫秒。Hibernate的数据精度和具体的JVM有关,在有些平台上其精度甚至只能xx到10秒。 你可以直接使用getter方法得到全局数据记录(例如,和具体的实体、集合、缓存区无关的数据),你也可以在具体查询中通过标记实体名、 或HQL、SQL语句得到某实体的数据记录。请参考Statistics、EntityStatistics、 CollectionStatistics、SecondLevelCacheStatistics、 和QueryStatistics的API文档以抓取更多信息。下面的代码则是个简单的例子: Statistics stats = HibernateUtil.sessionFactory.getStatistics(); Hibernate程序性能优化的考虑要点 初用HIBERNATE的人也许都遇到过性能问题,实现同一功能,用HIBERNATE与用JDBC性能相差十几倍很正常,如果不及早调整,很可能影响整个项目的进度。 大体上,对于HIBERNATE性能调优的主要考虑点如下: •数据库设计调整 a) 降低关联的复杂性 b) 尽量不使用联合主键 c) ID的生成机制,不同的数据库所提供的机制并不xx一样 d) 适当的冗余数据,不过分追求高范式 2、 HQL优化 HQL如果抛开它同HIBERNATE本身一些缓存机制的关联,HQL的优化技巧同普通的SQL优化技巧一样,可以很容易在网上找到一些经验之谈。 3、 主配置 a) 查询缓存,同下面讲的缓存不太一样,它是针对HQL语句的缓存,即xx一样的语句再次执行时可以利用缓存数据。但是,查询缓存在一个交易系统(数据变更频繁,查询条件相同的机率并不大)中可能会起反作用:它会白白耗费大量的系统资源但却难以派上用场。 b) fetch_size,同JDBC的相关参数作用类似,参数并不是越大越好,而应根据业务特征去设置 c) batch_size同上。 d) 生产系统中,切记要关掉SQL语句打印。 4、 缓存 a) 数据库级缓存:这级缓存是{zg}效和安全的,但不同的数据库可管理的层次并不一样,比如,在ORACLE中,可以在建表时指定将整个表置于缓存当中。 b) SESSION缓存:在一个HIBERNATE SESSION有效,这级缓存的可干预性不强,大多于HIBERNATE自动管理,但它提供xx缓存的方法,这在大批量增加/更新操作是有效的。比如,同时增加十万条记录,按常规方式进行,很可能会发现OutofMemeroy的异常,这时可能需要手动xx这一级缓存:Session.evict以及Session.clear c) 应用缓存:在一个SESSIONFACTORY中有效,因此也是优化的重中之重,因此,各类策略也考虑的较多,在将数据放入这一级缓存之前,需要考虑一些前提条件: i. 数据不会被第三方修改(比如,是否有另一个应用也在修改这些数据?) ii. 数据不会太大 iii. 数据不会频繁更新(否则使用CACHE可能适得其反) iv. 数据会被频繁查询 v. 数据不是关键数据(如涉及钱,安全等方面的问题)。 缓存有几种形式,可以在映射文件中配置:read-only(只读,适用于很少变更的静态数据/历史数据),nonstrict-read-write,read-write(比较普遍的形式,效率一般),transactional(JTA中,且支持的缓存产品较少) d) 分布式缓存:同c)的配置一样,只是缓存产品的选用不同,在目前的HIBERNATE中可供选择的不多,oscache, jboss cache,目前的大多数项目,对它们的用于集群的使用(特别是关键交易系统)都持保守态度。在集群环境中,只利用数据库级的缓存是xxx的。 5、 延迟加载 a) 实体延迟加载:通过使用动态代理实现 b) 集合延迟加载:通过实现自有的SET/LIST,HIBERNATE提供了这方面的支持 c) 属性延迟加载: 6、 方法选用 a) 完成同样一件事,HIBERNATE提供了可供选择的一些方式,但具体使用什么方式,可能用性能/代码都会有影响。显示,一次返回十万条记录(List/Set/Bag/Map等)进行处理,很可能导致内存不够的问题,而如果用基于游标(ScrollableResults)或Iterator的结果集,则不存在这样的问题。 b) Session的load/get方法,前者会使用二级缓存,而后者则不使用。 c) Query和list/iterator,如果去仔细研究一下它们,你可能会发现很多有意思的情况,二者主要区别(如果使用了Spring,在HibernateTemplate中对应find,iterator方法): i. list只能利用查询缓存(但在交易系统中查询缓存作用不大),无法利用二级缓存中的单个实体,但list查出的对象会写入二级缓存,但它一般只生成较少的执行SQL语句,很多情况就是一条(无关联)。 ii. iterator则可以利用二级缓存,对于一条查询语句,它会先从数据库中找出所有符合条件的记录的ID,再通过ID去缓存找,对于缓存中没有的记录,再构造语句从数据库中查出,因此很容易知道,如果缓存中没有任何符合条件的记录,使用iterator会产生N+1条SQL语句(N为符合条件的记录数) iii. 通过iterator,配合缓存管理API,在海量数据查询中可以很好的解决内存问题,如: while(it.hasNext()){ YouObject object = (YouObject)it.next(); session.evict(youObject); sessionFactory.evice(YouObject.class, youObject.getId()); } 如果用list方法,很可能就出OutofMemory错误了。 iv. 通过上面的说明,我想你应该知道如何去使用这两个方法了。 7、 集合的选用 在HIBERNATE 3.1文档的“19.5. Understanding Collection performance”中有详细的说明。 8、 事务控制 事务方面对性能有影响的主要包括:事务方式的选用,事务隔离级别以及锁的选用 a) 事务方式选用:如果不涉及多个事务管理器事务的话,不需要使用JTA,只有JDBC的事务控制就可以。 b) 事务隔离级别:参见标准的SQL事务隔离级别 c) 锁的选用:悲观锁(一般由具体的事务管理器实现),对于长事务效率低,但安全。乐观锁(一般在应用级别实现),如在HIBERNATE中可以定义VERSION字段,显然,如果有多个应用操作数据,且这些应用不是用同一种乐观锁机制,则乐观锁会失效。因此,针对不同的数据应有不同的策略,同前面许多情况一样,很多时候我们是在效率与安全/准确性上找一个平衡点,无论如何,优化都不是一个纯技术的问题,你应该对你的应用和业务特征有足够的了解。 <p class="c |