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