Hibernate性能

来源:互联网 发布:海带缠潜艇淘宝 编辑:程序博客网 时间:2024/06/01 19:16

用过springmvc+hibernate,已经struts2+hibernate。从没有考虑过性能问题,一是用的都是最简单的增删改查,跟hibernate相关最繁杂的也不过是写个复杂一点的sql语句。突然发现hibernate使用不好也会导致性能不好,很是表示自己的无知。

一般都是没有用到二级缓存,一般都是一个请求会生成一个session。

以下文章转载http://forking003.blog.sohu.com/73720954.html。

 数据库访问优化的原则:

    1.set:batch-size=n属性.尽量少的执行SQL语句,也就是对数据库的访问;
    2.fetch="select/join..."属性.在少执行数据库访问的基础上,还要少执行复杂的SQL语句,比如很多表连接查询,对数据量大的表尽量单独查询;
    3.不要一次装载过多的数据到内存,对有很多数据的表,一定要用分页查询,也就是利用rownum分页,一次转载的数据量最多不要超过100个;
    4.使用二级缓存是很好的解决方案,但要注意缓存的同步问题,删除和添加集合元素,要同步更新其关联的类进行更新,否则在页面上看不到任何效果(删除的数据行还显示在页面).及时的清空和同步缓存才能充分利用缓存的优势,否则就会适得其反.也就是说用缓存并不是永远不去数据库查数据了,而是能够细粒度的控制每个OID的持久对象的缓存和其关联的对象或集合的缓存,这样才是正确的运用了缓存带来的好处.
    5.lazy延迟加载.对于一些页面,不需要将所有关联对象都显示出来,那么可以少执行很多数据库查询,但因为在视图上有时候"延迟加载"会导致没有初始化的异常,所以在什么时候"延迟加载",什么时候"立即加载",对我们的来说,要有很清晰的思路.
    6.inverse和cascade的设置也会影响到是否会执行多余的SQL语句;
    7.查询缓存的用处,对于查询缓存,list()也会造成N+1次查询(第2次查询),这点与iterator()有同样的问题;
    8.批量更新和删除,直接用JDBC来做,一句话执行批量更新和删除,效率高,而且不像Session.delete()一样要加载行数据到内存再一一删除,这点就节约了很多内存.

 

    根据以上的几点,我总结了一些优化方法,当然可能有不太正确的地方,对不同的项目而言都有不同的优化方案,因此不可一味的套用.

    1.一对多关系是最常见的关联关系,也是优化的重点.因为缓存只缓存普通字段和集合的实例OID,所以当要用到集合的实例的时候,还要一条一条的去数据库载入,相当于load()方法,而执行的SQL语句就是这样的:
    select id,... from table where id=?
    select id,... from table where id=?
    ...
    如果这个对象关联的多个对象有成百上千个,那就要执行成百上千这样的查询,数据库服务器估计会很吃力,应用服务器的内存估计也用的差不多了.
    解决办法:
    a.手动清除Session级别的缓存,调用Session.evict()来将该持久对象从一级缓存清除,节约内存,这时候持久对象成为"脱管"状态,或者叫"游离"状态,应用层和视图层可以照常使用该对象,最后保存的时候,Hibernate会自动处理好与数据库同步的问题;

    b.使用set的batch-size属性,这个属性可以明显的减少执行SQL语句数目,因为它执行的是这样的语句:
    如果batch-size=10,而集合有13个关联对象,那么它会执行如下语句:
    select id,... from table where id in (?,?,?,?,?,?,?,?,?,?)
    select id,... from table where id in (?,?,?)
    总共2条语句,如果不设置batch-size,就要执行13条SQL语句,虽然in语句也不是很高效的语句,但还是比执行N条语句快很多,我做了个初步的测试,用Oracle 10g测试,相同的数据,用两种不同的查询方式,每个运行10次,算出平均查询时间,batch-size方式查询时间少一半,也就是快一倍,虽然是很粗糙的测试,但足以说明性能上的差距.如果你的集合很大,那么你的batch-size也不能设的太大,如果有1000条数据,那么你的batch-size最好不要超过50,否则性能也不会好到哪里去.
    以上两条结合,能够较好的解决执行过多SQL语句的问题和内存不足的问题.

    2.fatch方式的设置.在<one-to-many>和<mangy-to-one>关系中fetch="select"和fetch="join",两者产生的SQL语句不同."select"表示将关联对象用另外一句SQL语句查询出来,而且第二条SQL语句默认是延迟加载的,也就是如果不用到关联对象,这句SQL语句就不会执行了;"join"是采用左外连接语句来查询的,将两个表通过外键产生一个连接查询,如果两个表总字段数很多,那么这句SQL语句会很长,而且对它设置的"lazy=true"也会失效,也就变成了"立即加载",在某些情况下会造成很多不必要的查询和持久对象load到内存.
    虽然"select"方式多执行了一条SQL语句,但对性能影响不大,因为这条语句很简单,只有一个where条件:外键=?,所以解决方案是:采用"select"配置

    3.数据量很大的表(一般是上百万或千万数据),查询一次很费时间,而如果一次把所有查询结果都转成持久化类,那么内存肯定会罢工的.根据我以往的经验,对于这样的表,都是要分页查询的,除非哪个项目非常变态的要在一页内显示所有数据(也可以解决,但麻烦一点,就是分多次查,每查到一定量数据就清空一级缓存,然后将数据加入到List或Set),但基本上不会有必须这样做的理由.
    做分页也有意思的,其实分页功能抽象出来,最简单的就只有3个int属性:总记录数total(数据库count出来),每页记录数page_size(通过常量或参数设置得到),游标偏移量offset(通过页参数传递得到).其他数据都可以由这3个推导出来,况且它们这些整数之间的乘除加减速度都是很快的,比如:
    总页数=[(total-1)/page_size]+1
    当前页数=(offset/page_size)+1
    其中偏移量offset主要是控制查询区间的开始位置,而offset+page_size就是查询区间的结束位置,适用于Oracle的rownum和Mysql的limit.其他数据库也可以将offset换成当前页curr_page,方便查询.
    而且分页功能可以单独抽象出来做成一个与具体业务数据无关的类,我取名叫PageBean,这是一个SimpleBean,只有3个属性和对应的set,get方法.而如果需要分页,只要将这3个属性设置好,传递在各层之间,查数据的时候,将offset和offset+page_size设置到查询即可.
    那么total怎么办?每次翻页都去查?不需要.我的做法是,将查询条件封装成一个bean对象(SearchBean),然后将这个bean保存在一个Map的key中,同时将总记录数放在相应的value中,每次来一个新的SearchBean,都去Map中查,如果有相等(equals)的SearchBean,直接就total=value;如果没有就去count一下.当然Map是有大小限制的,如果超过大小限制,就从Map中清除value最小的一个(尽量保存结果大的查询).
    那么更新了相关数据怎么办?这个好办,在数据库的增删改方法中清空对应类的Map即可,因为数据修改了,无法确定哪些查询结果会受影响,如果要精确控制哪些查询受影响,还要分析修改的数据,那就非常复杂了,没这个必要.

    4.二级缓存.这是个让人又爱又恨的东东,好处是可以大大减少查询数据库的操作,但有时候数据无法及时更新,与数据库同步,造成一些让人费解的问题.
    通过Hibernate对数据库的更改(即使通过Hibernate的JDBC操作也算),二级缓存都会自己更新,所以不存在数据不一致的问题.Hibernate之外的数据库更改,只能通过强行同步数据库来更新缓存,这里不详述.
    我碰到的最大的问题是一对多关联的集合无法更新缓存的情况.在Hibernate里,可以对<class>和<set>标签设置缓存.问题就在<set>上,如果只对<class>设置<cache>而没有对<set>设置<cache>标签,那么该class的持久类将只缓存class的普通字段和set里元素的OID,而不是set元素的完整实体,那么调用这个set的元素的时候,还是要一条条的去执行SQL语句,load每一条数据,因此要设置<set>的<cache>就可以避免这个问题,这就有一个问题:当我delete()一个set中存在的实体元素的时候,它不会去自动更新set,让set把这个已经delete的元素自动remove()掉,这时候就需要手动调用SessionFactory.evictCollection()来清空这个集合的缓存,比较幸运的是,这个evictCollection()方法可以根据OID去清空指定OID的持久类的关联集合,在清空后需要一步非常重要的操作:Session.get(Class,OID),这时就会去重新加载集合.但奇怪的是添加一个元素,却不需要清空缓存,它自己就会加入关联对象的集合中了.
    也许你会问:我干么不在delete()的时候,手动从关联对象的set中remove掉要删除的对象呢?这样也可以,而且看起来更加符合通常的处理方法,不过我是把通用的SAVE,DELETE,UPDATE方法都写在一个基类里,这个基类是不区分对象是什么类型的,只要给个OBJECT,HIBERNATE就会自动保存到对应的表,大部分人也是这样做的.这就有个问题,我不知道传进来的是什么类,也就不知道它是否有集合属性,需要用反射去找到该类(其实我已经找到更好的办法,就是实现一个IsCollectionPropertyElement接口,有一个getCollection()方法和一个getOneId(),这样就可以拿到这个集合和关联OID了,前提是关联类持久化过).最后这个remove掉元素的持久化实例还是要同步到数据库(UPDATE)一次,而其实根据我们一般的一对多关系来说,这个UPDATE什么事情都没做(因为都是"多"那头有"一"的外键ID,而"一"这头不保存任何与"多"有关的数据),所以删除一个"多"那头的元素对"一"的行数据没有任何影响.
    所以我选择了SessionFactory.evictCollection(),这样就只需要重新从数据库加载一次该OID的持久类的关联对象(一个SELECT语句).两种方法效果一样,但是对数据库查询不一样,性能上还是清空缓存来的快.

 5.lazy,也就是延迟加载,这一直是Hibernate大力宣传的一个提高性能的特性,但在实际开发中,往往受到很大的限制,具体会在后面讲解.
    lazy使用受到限制的根源在于一个地方:必须在同一个Session里面完成lazy属性或集合的初始化.也就是说,如果你配置了lazy="true",而且查询也满足lazy的条件,但对于任何GUI程序来说,基本都是分层架构,那么实体对象需要在层与层之间传递,而Session一般在查询完成后就关闭了,以便尽快的释放数据库连接,这样做有个好处,就是业务逻辑层(一般我们叫做Service层)如果出现异常或者死循环导致程序死锁,那么数据库连接也已经释放,不会造成太大的数据库性能问题,最多只是应用层的问题.数据库则一般是多个应用共用的,出现问题会导致多个应用挂掉.
    lazy的限制就导致了这样一个约束:在视图显示完成之前,Session是一直打开的(另外开Session一个都不行!),如果应用层,或者视图层,或者网络传输部分,任何一个部分出现问题,都将导致数据库连接无法释放,所以lazy并不是一个通用的解决方案.下面讲讲在GUI程序中应用lazy的方法:
    a.使用Spring的OpenSessionInView,从名字就可以看出来,就是用过滤器(Servlet)或拦截器(需要使用Spring的Web框架)来管理Session,让视图显示完成之前保持Session打开,然后自动关闭;这种应用场景一般仅限于局域网系统,因为网络延迟小,如果是公网系统,就很有可能造成数据库连接占用过多的问题.
    b.不使用关联功能,只使用Hibernate的Mapping功能,这样编码会复杂一些,但配合对象级的二级缓存,可以使性能和开发效率取得一个比较好的平衡,受的限制也很少,可以在大多数场景中使用.

 6.inverse和cascade.inverse(英文意思是:倒转的, 反转的)属性是维持关联对象之间的关系,为true表示由关联的对象来维持关联关系,反之,就是自己来维持关联关系.在一对多关系中,一般是由"多"的一方来维持关系,这样如果"多"的一方修改了,而"一"的一方没有改变,则不会更新"一"的一方,反之会多一条更新"一"的语句而不管"一"的一方有没有改变.所以在一对多的关系中,一的一方配置"inverse=true";
    cascade是级联,可以想像成是数据库的级联.不过级联更新会造成不必要的SQL更新,一般我用的时候都是配置成"cascade=delete",即级联删除,不过这个属性也不是必须的,最好的办法是数据库配置级联删除,更高效,又可以保持数据完整性.

 7.查询缓存,一般只适用于条件不变的分页查询,适用范围窄,实际项目中较少使用.

 8.批量更新和删除,使用JDBC,用SQL语句直接操作效率最高,



以下文章装载自http://developer.51cto.com/art/200906/129539.htm

一、在处理大数据量时,会有大量的数据缓冲保存在Session的一级缓存中,这缓存大太时会严重显示性能,所以在使用Hibernate处理大数据量的,可以使用session. clear()或者session. evict(Object) 在处理过程中,清除全部的缓存或者清除某个对象。

二、对大数据量查询时,慎用list()或者iterator()返回查询结果,

<1>. 使用List()返回结果时,Hibernate会所有查询结果初始化为持久化对象,结果集较

大时,会占用很多的处理时间。

<2>. 而使用iterator()返回结果时,在每次调用iterator.next()返回对象并使用对象时,

Hibernate才调用查询将对应的对象初始化,对于大数据量时,每调用一次查询都会花

费较多的时间。当结果集较大,但是含有较大量相同的数据,或者结果集不是全部都会

使用时,使用iterator()才有优势。

<3>. 对于大数据量,使用qry.scroll()可以得到较好的处理速度以及性能。而且直接对结

果集向前向后滚动。

三、对于关联操作,Hibernate虽然可以表达复杂的数据关系,但请慎用,使数据关系较为

简单时会得到较好的效率,特别是较深层次的关联时,性能会很差。

四、对含有关联的PO(持久化对象)时,若default-cascade="all"或者 “save-update”,新增PO时,请注意对PO中的集合的赋值操作,因为有可能使得多执行一次update操作。

五、在一对多、多对一的关系中,使用延迟加载机制,会使不少的对象在使用时才会初始化,这样可使得节省内存空间以及减少数据库的负荷,而且若PO中的集合没有被使用时,就可减少互数据库的交互从而减少处理时间。

六、对于大数据量新增、修改、删除操作或者是对大数据量的查询,与数据库的交互次数是决定处理时间的最重要因素,减少交互的次数是提升效率的最好途径,所以在开发过程中,请将show_sql设置为true,深入了解Hibernate的处理过程,尝试不同的方式,可以使得效率提升。

七、Hibernate是以JDBC为基础,但是Hibernate是对JDBC的优化,其中使用Hibernate的缓冲机制会使性能提升,如使用二级缓存以及查询缓存,若命中率较高明,性能会是到大幅提升。

八、Hibernate可以通过设置hibernate.jdbc.fetch_size,hibernate.jdbc.batch_size等属性,对Hibernate进行优化。

九、不过值得注意的是,一些数据库提供的主键生成机制在效率上未必最佳,大量并发insert数据时可能会引起表之间的互锁。数据库提供的主键生成机制,往往是通过在一个内部表中保存当前主键状态(如对于自增型主键而言,此内部表中就维护着当前的最大值和递增量),之后每次插入数据会读取这个最大值,然后加上递增量作为新记录的主键,之后再把这个新的最大值更新回内部表中,这样,一次Insert操作可能导致数据库内部多次表读写操作,同时伴随的还有数据的加锁解锁操作,这对性能产生了较大影响。因此,对于并发Insert要求较高的系统,推荐采用uuid.hex 作为主键生成机制。

十、Dynamic Update 如果选定,则生成Update SQL 时不包含未发生变动的字段属性,这样可以在一定程度上提升SQL执行效能.Dynamic Insert 如果选定,则生成Insert SQL 时不包含未发生变动的字段属性,这样可以在一定程度上提升SQL执行效能

十一、在编写代码的时候请,对将POJO的getter/setter方法设定为public,如果设定为private,Hibernate将无法对属性的存取进行优化,只能转而采用传统的反射机制进行操作,这将导致大量的性能开销(特别是在1.4之前的Sun JDK版本以及IBM JDK中,反射所带来的系统开销相当可观)。

十二、在one-to-many 关系中,将many 一方设为主动方(inverse=false)将有助性能的改善。

十三、由于多对多关联的性能不佳(由于引入了中间表,一次读取操作需要反复数次查询),因此在设计中应该避免大量使用。



0 0
原创粉丝点击