hibernate缓存详解

转载整理自:

https://blog.csdn.net/woshichenxu/article/details/586361

https://blog.csdn.net/xtayhicbladwin/article/details/4739852

http://www.cnblogs.com/xiaoming0601/p/5882980.html

https://www.jianshu.com/p/78267e834091

1.     关于hibernate缓存的问题:

1.1.1.         基本的缓存原理

Hibernate缓存分为二级,第一级存放于session中称为一级缓存,默认带有且不能卸载。

 

第二级是由sessionFactory控制的进程级缓存。是全局共享的缓存,凡是会调用二级缓存的查询方法 都会从中受益。只有经正确的配置后二级缓存才会发挥作用。同时在进行条件查询时必须使用相应的方法才能从缓存中获取数据。比如Query.iterate()方法、loadget方法等。必须注意的是session.find方法永远是从数据库中获取数据,不会从二级缓存中获取数据,即便其中有其所需要的数据也是如此。

 

查询时使用缓存的实现过程为:首先查询一级缓存中是否具有需要的数据,如果没有,查询二级缓存,如果二级缓存中也没有,此时再执行查询数据库的工作。要注意的是:此3种方式的查询速度是依次降低的。

1.2.   存在的问题

1.2.1.      一级缓存的问题以及使用二级缓存的原因

     因为Session的生命期往往很短,存在于Session内部的第一级最快缓存的生命期当然也很短,所以第一级缓存的命中率是很低的。其对系统性能的改善也是很有限的。当然,这个Session内部缓存的主要作用是保持Session内部数据状态同步。并非是hibernate为了大幅提高系统性能所提供的。

为了提高使用hibernate的性能,除了常规的一些需要注意的方法比如:

使用延迟加载、迫切外连接、查询过滤等以外,还需要配置hibernate的二级缓存。其对系统整体性能的改善往往具有立竿见影的效果!

(经过自己以前作项目的经验,一般会有3~4倍的性能提高)

 

1.2.2.      N+1次查询的问题

执行条件查询时,iterate()方法具有著名的 n+1”次查询的问题,也就是说在第一次查询时iterate方法会执行满足条件的查询结果数再加一次(n+1)的查询。但是此问题只存在于第一次查询时,在后面执行相同查询时性能会得到极大的改善。此方法适合于查询数据量较大的业务数据。

但是注意:当数据量特别大时(比如流水线数据等)需要针对此持久化对象配置其具体的缓存策略,比如设置其存在于缓存中的最大记录数、缓存存在的时间等参数,以避免系统将大量的数据同时装载入内存中引起内存资源的迅速耗尽,反而降低系统的性能!!!

这里介绍一下n+1次查询的问题:

  • 举个例子,我们数据库中有两张表,一个是Users,一个是BlogsBlogs中含有一个外键user_id,指向了Users的主键id
    想要得到所有User以及其分别对应的Blog,一种写法是
SELECT * FROM Users; 
SELECT * FROM Users WHERE Blogs.user_id = #{user.id}

这样我们实际对数据库做了N+1次查询:选择所有User一次得到N个User,对于N个User分别选择其对应的Blog一共N次。所以,一共执行了N+1次查询,这就是N+1问题。

再举个例子:

在Session的缓存中存放的是相互关联的对象图。默认情况下,当Hibernate从数据库中加载Customer对象时,会同时加载所有关联的Order对象。以Customer和Order类为例,假定ORDERS表的CUSTOMER_ID外键允许为null,图1列出了CUSTOMERS表和ORDERS表中的记录。

 

n+1查询

以下Session的find()方法用于到数据库中检索所有的Customer对象:

List customerLists=session.find("from Customer as c");

运行以上find()方法时,Hibernate将先查询CUSTOMERS表中所有的记录,然后根据每条记录的ID,到ORDERS表中查询有参照关系的记录,Hibernate将依次执行以下select语句:

select * from CUSTOMERS; 
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;

通过以上5条select语句,Hibernate最后加载了4个Customer对象和5个Order对象,在内存中形成了一幅关联的对象图,参见图2。

n+1查询

Hibernate在检索与Customer关联的Order对象时,使用了默认的立即检索策略。这种检索策略存在两大不足:

(1) select语句的数目太多,需要频繁的访问数据库,会影响检索性能。如果需要查询n个Customer对象,那么必须执行n+1次select查询语句。这就是经典的n+1次select查询问题。 这种检索策略没有利用SQL的连接查询功能,例如以上5条select语句完全可以通过以下1条select语句来完成:

select * from CUSTOMERS left outer join ORDERS 
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID 

以上select语句使用了SQL的左外连接查询功能,能够在一条select语句中查询出CUSTOMERS表的所有记录,以及匹配的ORDERS表的记录。

(2)在应用逻辑只需要访问Customer对象,而不需要访问Order对象的场合,加载Order对象完全是多余的操作,这些多余的Order对象白白浪费了许多内存空间。
为了解决以上问题,Hibernate提供了其他两种检索策略:延迟检索策略和迫切左外连接检索策略。延迟检索策略能避免多余加载应用程序不需要访问的关联对象,迫切左外连接检索策略则充分利用了SQL的外连接查询功能,能够减少select语句的数目。

 

 

hibernate解决n+1的方法:

 

    Hibernate的检索策略包括类级别检索策略和关联级别检索策略。 

    类级别检索策略有立即检索和延迟检索,默认的检索策略是立即检索。在Hibernate映射文件中,通过在<class>上配置 lazy属性来确定检索策略。对于Session的检索方式,类级别检索策略仅适用于load方法;也就说,对于get、qurey检索,持久化对象都会被立即加载而不管lazy是false还是true.一般来说,我们检索对象就是要访问它,因此立即检索是通常的选择。由于load方法在检索不到对象时会抛出异常(立即检索的情况下),因此我个人并不建议使用load检索;而由于<class>中的lazy属性还影响到多对一及一对一的检索策略,因此使用load方法就更没必要了。

   关联级别检索策略有立即检索、延迟检索和迫切左外连接检索。对于关联级别检索,又可分为一对多和多对多、多对一和一对一两种情况讨论。

   1)立即检索:这是一对多默认的检索策略,此时lazy=false,outer-join=false.尽管这是默认的检索策略,但如果关联的集合是无用的,那么就不要使用这种检索方式。

  2)延迟检索:此时lazy=true,outer-join=false(outer-join=true是无意义的),这是优先考虑的检索方式。

  3)迫切左外连接检索:此时 lazy=false,outer-join=true,这种检索策略只适用于依靠id检索方式(load、get),而不适用于query的集合检索(它会采用立即检索策略)。相比于立即检索,这种检索策略减少了一条sql语句,但在Hibernate中,只能有一个<set>配置成 outer-join=true.

 

多对一和一对一检索策略一般使用<many-to-one>、<one-to-one>配置。<many- to-one>中需要配置的属性是 outer-join,同时还需要配置one端关联的<class>的lazy属性(配置的可不是<many-to-one>中的lazy哦),它们的组合后的检索策略如下:

 

1) outer-join=auto:这是默认值,如果lazy=true为延迟检索,如果lazy=false为迫切左外连接检索

 

2) outer-join=true,无关于lazy,都为迫切左外连接检索。

 

3) outer-join=false,如果lazy=true为延迟检索,否则为立即检索。

 

   可以看到,在默认的情况下(outer-join=auto,lazy=false),对关联的one端对象Hibernate采用的迫切左外连接检索。依我看,很多情况下,我们并不需要加载one端关联的对象(很可能我们需要的仅仅是关联对象的id);另外,如果关联对象也采用了迫切左外连接检索,就会出现select语句中有多个外连接表,如果个数多的话会影响检索性能,这也是为什么Hibernate通过 hibernate.max_fetch_depth属性来控制外连接的深度。对于迫切左外连接检索,query的集合检索并不适用,它会采用立即检索策略。

 

对于检索策略,需要根据实际情况进行选择。对于立即检索和延迟检索,它们的优点在于select语句简单(每张表一条语句)、查询速度快,缺点在于关联表时需要多条select语句,增加了访问数据库的频率。因此在选择即检索和延迟检索时,可以考虑使用批量检索策略来减少select语句的数量(配置batch-size属性)。对于切左外连接检索,优点在于select较少,但缺点是select语句的复杂度提高,多表之间的关联会是很耗时的操作。另外,配置文件是死的,但程序是活的,可以根据需要在程序里显示的指定检索策略(可能经常需要在程序中显示指定迫切左外连接检索)。为了清楚检索策略的配置效果如何,可以配置show_sql属性查看程序运行时Hibernate执行的sql语句。


 

1.3.   使用hibernate二级缓存的其他注意事项:

1.3.1.      关于数据的有效性

另外,hibernate会自行维护二级缓存中的数据,以保证缓存中的数据和数据库中的真实数据的一致性!无论何时,当你调用save()update() saveOrUpdate()方法传递一个对象时,或使用load() get()list()iterate() scroll()方法获得一个对象时该对象都将被加入到Session的内部缓存中。 当随后flush()方法被调用时,对象的状态会和数据库取得同步。

 

也就是说删除、更新、增加数据的时候,同时更新缓存。当然这也包括二级缓存!

 

只要是调用hibernate API执行数据库相关的工作。hibernate都会为你自动保证 缓存数据的有效性!!

 

但是,如果你使用了JDBC绕过hibernate直接执行对数据库的操作。此时,Hibernate不会/也不可能自行感知到数据库被进行的变化改动,也就不能再保证缓存中数据的有效性!!

 

这也是所有的ORM产品共同具有的问题。幸运的是,Hibernate为我们暴露了Cache的清除方法,这给我们提供了一个手动保证数据有效性的机会!!

一级缓存,二级缓存都有相应的清除方法。

 

其中二级缓存提供的清除方法为:

按对象class清空缓存

                按对象class和对象的主键id清空缓存

                清空对象的集合中的缓存数据等。

   

1.3.2.      适合使用的情况

并非所有的情况都适合于使用二级缓存,需要根据具体情况来决定。同时可以针对某一个持久化对象配置其具体的缓存策略。

 

适合于使用二级缓存的情况:

1、数据不会被第三方修改;

 

一般情况下,会被hibernate以外修改的数据最好不要配置二级缓存,以免引起不一致的数据。但是如果此数据因为性能的原因需要被缓存,同时又有可能被第3方比如SQL修改,也可以为其配置二级缓存。只是此时需要在sql执行修改后手动调用cache的清除方法。以保证数据的一致性

 

  2、数据大小在可接收范围之内;

 

     如果数据表数据量特别巨大,此时不适合于二级缓存。原因是缓存的数据量过大可能会引起内存资源紧张,反而降低性能。

 

如果数据表数据量特别巨大,但是经常使用的往往只是较新的那部分数据。此时,也可为其配置二级缓存。但是必须单独配置其持久化类的缓存策略,比如最大缓存数、缓存过期时间等,将这些参数降低至一个合理的范围(太高会引起内存资源紧张,太低了缓存的意义不大)。

 

  3、数据更新频率低;

 

     对于数据更新频率过高的数据,频繁同步缓存中数据的代价可能和 查询缓存中的数据从中获得的好处相当,坏处益处相抵消。此时缓存的意义也不大。

 

 

  4、非关键数据(不是财务数据等)

 

  财务数据等是非常重要的数据,绝对不允许出现或使用无效的数据,所以此时为了安全起见最好不要使用二级缓存。

  因为此时 “正确性”的重要性远远大于 “高性能”的重要性。

 

2.     目前系统中使用hibernate缓存的建议

1.4.   目前情况

 一般系统中有三种情况会绕开hibernate执行数据库操作:

1、多个应用系统同时访问一个数据库

   此种情况使用hibernate二级缓存会不可避免的造成数据不一致的问题,

   此时要进行

详细的设计。比如在设计上避免对同一数据表的同时的写入操作,

   使用数据库各种级别的锁定机制等。

 

2、动态表相关

   所谓“动态表”是指在系统运行时根据用户的操作系统自动建立的数据表。

   比如“自定义表单”等属于用户自定义扩展开发性质的功能模块,因为此时数据表是运行时建立的,所以不能进行hibernate的映射。因此对它的操作只能是绕开hibernate的直接数据库JDBC操作。

      如果此时动态表中的数据没有设计缓存,就不存在数据不一致的问题。

   如果此时自行设计了缓存机制,则调用自己的缓存同步方法即可。

3、使用sql对hibernate持久化对象表进行批量删除时

     此时执行批量删除后,缓存中会存在已被删除的数据。

分析: 

   当执行了第3条(sql批量删除)后,后续的查询只可能是以下三种方式:

a. session.find()方法:

根据前面的总结,find方法不会查询二级缓存的数据,而是直接查询数据库。

所以不存在数据有效性的问题。

b. 调用iterate方法执行条件查询时:

根据iterate查询方法的执行方式,其每次都会到数据库中查询满足条件的id值,然后再根据此id 到缓存中获取数据,当缓存中没有此id的数据才会执行数据库查询;

如果此记录已被sql直接删除,则iterate在执行id查询时不会将此id查询出来。所以,即便缓存中有此条记录也不会被客户获得,也就不存在不一致的情况。(此情况经过测试验证)

 

c. getload方法按id执行查询:

 

客观上此时会查询得到已过期的数据。但是又因为系统中执行sql批量删除一般是

针对中间关联数据表,对于

中间关联表的查询一般都是采用条件查询 ,id来查询某一条关联关系的几率很低,所以此问题也不存在!

 

   如果某个值对象确实需要按id查询一条关联关系,同时又因为数据量大使用 sql执行批量删除。当满足此两个条件时,为了保证按id 的查询得到正确的结果,可以使用手动清楚二级缓存中此对象的数据的方法!!

(此种情况出现的可能性较小)

 

1.5.   建议

1、建议不要使用sql直接执行数据持久化对象的数据的更新,但是可以执行 批量删除。(系统中需要批量更新的地方也较少)

 

2、如果必须使用sql执行数据的更新,必须清空此对象的缓存数据。调用

SessionFactory.evict(class)

SessionFactory.evict(class,id)

等方法。

 

3、在批量删除数据量不大的时候可以直接采用hibernate的批量删除,这样就不存在绕开hibernate执行sql产生的缓存数据一致性的问题。

 

4、不推荐采用hibernate的批量删除方法来删除大批量的记录数据。

原因是hibernate的批量删除会执行1条查询语句外加 满足条件的n条删除语句。而不是一次执行一条条件删除语句!!

当待删除的数据很多时会有很大的性能瓶颈!!!如果批量删除数据量较大,比如超过50,可以采用JDBC直接删除。这样作的好处是只执行一条sql删除语句,性能会有很大的改善。同时,缓存数据同步的问题,可以采用 hibernate清除二级缓存中的相关数据的方法。

调用 SessionFactory.evict(class) SessionFactory.evict(class,id)等方法。

 

所以说,对于一般的应用系统开发而言(不涉及到集群,分布式数据同步问题等),因为只在中间关联表执行批量删除时调用了sql执行,同时中间关联表一般是执行条件查询不太可能执行按id查询。所以,此时可以直接执行sql删除,甚至不需要调用缓存的清除方法。这样做不会导致以后配置了二级缓存引起数据有效性的问题。

 

退一步说,即使以后真的调用了按id查询中间表对象的方法,也可以通过调用清除缓存的方法来解决。

 

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值