Hibernate缓存机制剖析

Hibernate是基于缓存机制实现的。Hibernate的缓存包括:一级缓存、二级缓存和查询缓存。

Hibernate中支持懒加载load,也支持及时加载get。Hibernate采用CGlib的动态代理实现延迟加载。延迟加载采用CGlib的Enhancer类动态生成类。


比较

下面对Hibernate中一级缓存、二级缓存、查询缓存机制做一个横向比较:


相同点:

1、均为缓存,均可在一定的条件下缓存数据;

2、Hibernate的查询实现,是基于缓存机制;

3、三种缓存方式的内部实现方式类似,均使用key-value的map键值对方式实现;

4、一级缓存与二级缓存,军师对实体对象的缓存。内部实现均是:key里面放的是对象主键Id,value里面放的是实体对象。所以它们是实体对象的缓存。


不同点:

1、一级缓存,又称Session缓存,或者事务级缓存,它是内置的,不能被卸载。由于Session对象的生命周期通常对应一个数据库事务或一个应用事务,所以它的缓存是事务范围的缓存;

2、二级缓存,又称为SessionFactory缓存,它是进程范围或者集群范围的缓存,有可能出现并发问题,因此需要采用适当的并发策略,该策略为被缓存的数据提供了事务隔离级别;

3、查询缓存,是普通属性的缓存。查询缓存的生命周期:当关联的表发生修改,查询缓存的生命周期结束;

4、一级缓存是进程级的,进程结束后,即Session关闭后,缓存也随之清空;二级缓存与查询缓存均与Session无关,二级缓存的生命周期可以根据策略手动配置;查询缓存的生命周期与相关联的表相关;

5、一级缓存的存活期比较短,所以命中率比较低;查询缓存当关联表数据变更时,缓存生命周期结束,故命中率也比较低;二级缓存存活期较长,实际项目中应用也比较多;

6、一级缓存并不是为了大幅度提高性能而设置的,Hibernate主要使用一级缓存进行数据同步;而二级缓存的使用,由于它是进程范围内的缓存,可以大幅度提高性能。而查询缓存是为了缓存普通属性设置的缓存。


一级缓存

各种举例

拿公司和员工来举例子:


一、同一个session中,发生两次Load查询

在同一个session中,发出两次load查询。第一次查询,locad时查询代理,不查询上来数据,执行方法时,执行语句;

第二次查询,将不会再发出sql语句,直接在一级缓存里面取得。


二、同一个session中,发生两次get查询

在同一个session中,发出两次get查询。第一次查询,get时查询,马上执行查询语句,查询上来数据,方法执行时不再发sql语句。同时,将该数据放入缓存中。

第二次查询时,不会再发出sql语句,直接从缓存里面取得,除非第二次查询时,数据发生了变化。它与load一样,缓存中有的话,将直接查询。


三、同一个session中,发出两次iterate查询,查询实体对象

同一个session中,发出两次iterate查询,查询实体对象

第一次查询,发生N+1问题。第一次是查询所有id,然后去缓存里面去找,因为是第一次查询,缓存里面没有数据;所以会根据id,去数据库里面再次查询n次。

第二次查询,它会发出查询id语句,然后直接去缓存里面取得。


四、同一个session中,发出两次iterate查询,查询普通属性

同一个session中,发出两次iterate查询,查询普通属性。

这时,它还是会把sql语句发出来。

这是因为,iterate查询普通属性,一级缓存不会缓存,一级缓存只缓存实体对象查询。普通属性查询,不会缓存。

一级缓存是缓存实体对象的


五、在两个session里面,发load查询

在两个session里面,发load查询

由于session是进程级缓存,一个线程对应一个session,所以第一次查询后,会关闭session

第二次再次开启session

session间不能缓存一级缓存数据。因为它会伴随着session的消亡,而消亡。

所以会发两条语句


六、在同一个session中,先调用save,在调用load查询刚刚save的数据

在同一个session中,先调用save,在调用load查询刚刚save的数据

save也是支持缓存的。save之后,它会先往缓存里面存一份,取得时候,直接在缓存里面取

查询时,不会发出sql语句,因为save支持缓存。


七、大批量数据添加

大批量数据添加

对于大批量的数据添加,我们采用下面的做法:

每二十条数据,清一下缓存,到数据库里面存一次。最后提交事务


二级缓存

二级缓存,是SessionFactory级别的,因为SessionFactory能够管理它。二级缓存可以被所有的session共享。二级缓存,默认不启动,一般需要使用第三方产品。

使用ehcache第三方产品支持Hibernate的缓存。

<ehcache>
    <diskStore path="java.io.tmpdir"/>  <!--超出部分保存的位置-->

    <defaultCache
        maxElementsInMemory="10000"   <!--缺省配置,可以防止一万个对象-->
        eternal="false"      <!--过不过期,true时,永远不过期-->
        timeToIdleSeconds="120"  <!--第一次访问后,间隔120秒没被访问,就清掉缓存-->
        timeToLiveSeconds="120"   <!--缓存能够存活的时间120秒-->
        overflowToDisk="true"  <!--溢出的问题,如果已经超出了1万个对象,设置为TRUE,就保存在磁盘上-->
        />

</ehcache>



一、开启二级缓存,开启两个session,执行两次load方法

第一次load使用时,执行sql语句;第二次执行load时,不发语句。

因为它先到一级缓存里面找,没有数据,然后再去二级缓存里面找,查找到数据。

session可以共享二级缓存中的数据;二级缓存是进程级的。


二、开启二级缓存,开启两个session,执行两次get方法

与load基本类似,第一次查询发语句,第二次查询查询二级缓存。


三、开启二级缓存,在两个session中发load查询,采用SessionFactory管理二级缓存

//删除二级缓存

//HibernateUtils.getSessionFactory().evict(Student.class);

HibernateUtils.getSessionFactory().evict(Student.class, 1);

第二次查询时:会发出查询语句,因为二级缓存中的数据被清除了


四、开启二级缓存 ,一级缓存和二级缓存的交互

//禁止将一级缓存中的数据放到二级缓存中

session.setCacheMode(CacheMode.IGNORE);

第二次查询时:会发出查询语句,因为禁止了一级缓存和二级缓存的交互


五、 大批量的数据添加

大批量数据交互时,禁止一级缓存与二级缓存的交互,同时每二十条清一下一级缓存。


总结:二级缓存与一级缓存一模一样,只缓存实体对象,不缓存普通属性。


查询缓存

查询缓存是缓存普通属性的结果集

对实体对象的结果集缓存会缓存Id,生命周期不定。当表里的数据发生变化,查询缓存的生命周期结束。


查询缓存的配置和使用:

修改hibernate.cfg.xml文件,来开启查询缓存,默认是false,是不起用的
    <property name="hibernate.cache.use_query_cache">true</property>    
    //必须在程序启用
    query.setCacheable(true)


一、开启查询,关闭二级缓存,采用query.list()查询普通属性

在一个session中发query.list()查询

//执行两次如下代码

List names = session.createQuery("select s.name from Student s")
                                .setCacheable(true)
                                .list();

第一次将发出sql语句,第二次不发语句


二、 开启查询,关闭二级缓存,采用query.list()查询普通属性

在两个session中发query.list()查询

执行结果如上,跨session。查询缓存与session没关系


三、开启查询,关闭二级缓存,采用query.iterate()查询普通属性

在两个session中发query.iterate()查询

第二次查询会发出sql语句,

会发出查询语句,query.iterate()查询普通属性它不会使用查询缓存

查询缓存只对query.list()起作用


四、关闭查询,关闭二级缓存,采用query.list()查询实体

在两个session中发query.list()查询

会发出查询语句,默认query.list()每次执行都会发出查询语句


五、开启查询,关闭二级缓存,采用query.list()查询实体在两个session中发query.list()查询

会发出n条查询语句,因为开启了查询缓存,关闭了二级缓存,那么查询缓存就会缓存实体对象的id

第二次执行query.list(),将查询缓存中的id依次取出,分别到一级缓存和二级缓存中查询相应的实体

对象,如果存在就使用缓存中的实体对象,否则根据id发出查询学生的语句


六、开启查询,开启二级缓存,采用query.list()查询实体

在两个session中发query.list()查询

不再发出查询语句,因为配置了二级缓存和查询缓存


其他

1、加载方式

Hibernate有四种加载方式:即时加载、延迟加载、预先加载和批量加载。

a、即时加载

实体加载完成后,立即加载与实体先关连的数据,并填充到实体对应的属性中。这种加载方式通常会发多条sql语句;

b、延迟加载

实体加载时,其关联数据并不是立即读取,而是当关联数据第一次被访问再进行读取。这种加载方式在第一次访问关联数据时,必须在同一个session中,否则包session关闭的错误;

这种方式内部是采用动态代理实现的,具体内容请参见Spring AOP一文

c、预先加载

预先加载是发出“outer-join”语句。可以通过全局变量Hibernate.max_fetch_depth限定join的层次

d、批量加载

对于及时加载和延迟加载,可以采用批量价在进行优化。

批量价在通过批量提交多个限制条件,一次多个限定条件的数据读取。同时在实体映射文件中的class节点,通过配置batch-size参数打开批量加载机制,并限定每次批量加载数据的数量。


2、缓存机制导致的问题

Hibernate的缓存机制,导致了一些问题:a、n+1问题;b、OpenSessionInView


a、n+1问题

n+1问题就是在迭代查询时,发出了n+1条语句,如:

Iterator iter = session.createQuery("from User").list();

执行上面这段代码,直接返回一个迭代器。这时,假设一共有n条数据,那么它会执行n+1条sql语句。

这是因为:首先,它会把所有的主键Id查询上来,然后根据Id,去缓存里面找,也就是session里面找。如果缓存中已经有的数据的话,则它会直接从缓存里面取;如果缓存里面没有数据,则它需要根据Id,一条一条去数据库里面查询。所以会发出n+1条语句。


如何避免n+1问题?

可以先返回List:

List users = session.createQuery("from User").list();

该查询,会将查询上来的数据保存在缓存里面,也就是一级缓存里面。这是我们不关闭session,使用同一个session,进行迭代查询。

Iterator iter = session.createQuery("from User").list();

这时,它会发送一条查询Id的sql语句,然后再去一级缓存里面找,因为缓存里面都有数据,所以会直接查询上来。


注意:List查询时,不查询缓存;Iterator查询,会查询缓存。


b、OpenSessionInView

Hibernate查询中采用懒加载时,即设置了lazy=true,那么读取数据的时候,当读取了父数据后,进程结束。Hibernate会自动关系Session,这样,当要使用子数据的时候,系统会找不到当前session,所以,这就需要保持session一直开着,直到调用调用结束为止。

这个好办,只需要在发出请求之初,开启session时,将开启的session放入Threadlocal中,结束后再在Threadlocal中结束该进程。这个过程就是OpensessionInView的过程。


总结

1、一级缓存与二级缓存的最大区别就是:一级缓存是Session级缓存;二级缓存是跨Session的缓存。其他机制完全类似。

2、二级缓存是Session间的,能够跨Session;

3、查询缓存是Session间的,能够跨Session;

4、query.iterate()查询普通属性不会使用查询缓存,查询缓存只对query.list()起作用;

5、一级缓存生命周期很短暂,故命中率不高;查询缓存的生命周期与相关联表有关。关联表数据有变动,查询缓存的生命周期结束;二级缓存的生命周期可以认为控制,利用率较大。



郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。