我这里使用的是Hibernate5.2.0版本
Hibernate缓存分为一级缓存(有的也叫Session缓存)和二级缓存。
一级缓存(Session)
一级缓存的生命周期和session的生命周期一致,当前sessioin一旦关闭,一级缓存就消失,因此一级缓存也叫session级的缓存或事务级缓存。一级缓存只存实体对象的 ,它不会缓存一般的对象属性(查询缓存可以),即当获得对象后,就将该对象的缓存起来,如果在同一session中如果再去获取这个对象 时,它会先判断缓存中有没有该对象的ID,如果有就直接从缓存中取出,反之则去数据库中取,取的同时将该对象的缓存起来,有以下方法可以 支持一级缓存:
get()
load()
iterate(查询实体对象)
其 中 Query 和Criteria的list() 只会缓存,但不会使用缓存(除非结合查询缓存)。
下面我们进行代码测试:
测试1:
1 Session session=factory.openSession(); 2 Transaction tran=session.beginTransaction(); 3 4 User user=session.get(User.class, 2); 5 System.out.println(user); 6 7 User user2=session.get(User.class, 2); 8 System.out.println(user2); 9 10 tran.commit(); 11 session.close();
输出结果:
1 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 2 User [id=2, name=姓名2] 3 User [id=2, name=姓名2]
可以很清楚的看到,这里只执行了一条SQL语句,当使用get方法时,获取到一个对象,并将这个对象缓存到session中,当下次再从这个session中获取Id=2的User对象,那么session首先从缓存中查询是否有一个id=2的User对象,如果就返回,这里刚好有一个,所有就不会执行第二条SQL语句。
测试2:
1 Session session=factory.openSession(); 2 Transaction tran=session.beginTransaction(); 3 4 User user=session.get(User.class, 200); 5 System.out.println(user); 6 7 User user2=session.get(User.class, 200); 8 System.out.println(user2); 9 10 tran.commit(); 11 session.close();
输出结果
1 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 2 null 3 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 4 null
因为ID=200的User对象在数据库中并不存在,所有第一条语句执行后,输出该对象为null,当再次从使用get方法时,Hibernate先判断session是否有id=200的User对象,这里很明显并没有。所有再次从数据库中查找,执行了第二条语句
测试3
1 Session session=factory.openSession(); 2 Transaction tran=session.beginTransaction(); 3 4 User user=session.get(User.class, 2); 5 System.out.println(user); 6 7 tran.commit(); 8 session.close(); 9 10 System.out.println("---------------------"); 11 12 session=factory.openSession(); 13 tran=session.beginTransaction(); 14 15 user=session.get(User.class, 2); 16 System.out.println(user); 17 18 tran.commit(); 19 session.close();
输出结果:
1 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 2 User [id=2, name=姓名2] 3 --------------------- 4 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 5 User [id=2, name=姓名2]
这里我们可以看出,执行了两条语句。我们知道一级缓存是基于Session,每个Session缓存的数据并不能共享,当关闭Session时,这个Session中的缓存也会被清楚。上面我们可以看到两个Session并不是同一个Session对象,所有会读取两条操作。那么怎么才可以夸Session读取缓存信息了。这里就要用到我们下面要讲解的二级缓存。
二级缓存
二级缓存也称进程级的缓存或SessionFactory级的缓存,二级缓存可以被所有的session共享,二级缓存的生命周二级缓存的生命周期和 SessionFactory的生命周期一致。
二级缓存的工具包也很多,我们这里讲解EhCache
hibernate.cfg.xml配置
<!-- 启用二级缓存 --> <property name="cache.use_second_level_cache">true</property> <!-- 选择二级缓存的工具类 --> <property name="cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
第一句代码,是启用二级缓存,要想使用二级缓存,我们必须先启用二级缓存。第二句代码是选择第三方缓存提供类,我们这里使用EhCache。以前版本的Hibernate可能会用cache.provider_class。
并不是所有的类都需要使用缓存机制,比如财务上的数据,变更比较大,就不能使用缓存。而基本上没有变更或变更比较少的话就可以使用缓存。要想使某个类型的对象使用使用缓存,我们也必须给这个类定义缓存声明
1 <class-cache usage="read-write" class="com.myproc.domain.User"/>
属性:class用来指定对那个类使用缓存,这里必须是全限命名(包括包名)
usage:指定缓存策略
☆实体缓存
测试代码:
1 Session session=factory.openSession(); 2 Transaction tran=session.beginTransaction(); 3 4 User user=session.get(User.class, 2); 5 System.out.println(user); 6 7 tran.commit(); 8 session.close(); 9 10 System.out.println("---------------------"); 11 12 session=factory.openSession(); 13 tran=session.beginTransaction(); 14 15 user=session.get(User.class, 2); 16 System.out.println(user); 17 18 tran.commit(); 19 session.close();
输出结果
1 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 2 User [id=2, name=姓名2] 3 --------------------- 4 User [id=2, name=姓名2]
可以看到,虽然是两个不同的Session,但是还是只执行了一条SQL语句,说明二级缓存起了作用。
【总结】:通过Session.get()方法和Session.load()方法都可以将一个实体对象缓存到一级缓存中。如果开启了二级缓存,那么数据也会缓存到二级缓存中
☆集合属性的缓存
代码测试:
1 Dept dept=session.get(Dept.class, 5); 2 System.out.println(dept); 3 System.out.println(dept.getUsers()) 4 tran.commit(); 5 session.close(); 6 7 System.out.println("---------------------"); 8 9 session=factory.openSession(); 10 tran=session.beginTransaction(); 11 12 dept=session.get(Dept.class, 2); 13 System.out.println(dept); 14 System.out.println(dept.getUsers());
15 tran.commit();
16 session.close();
输出结果:
1 Hibernate: select dept0_.Id as Id1_0_0_, dept0_.name as name2_0_0_ from dept dept0_ where dept0_.Id=? 2 Dept [id=5, name=部门5] 3 Hibernate: select users0_.deptid as deptid3_1_0_, users0_.Id as Id1_1_0_, users0_.Id as Id1_1_1_, users0_.name as name2_1_1_, users0_.deptid as deptid3_1_1_ from user users0_ where users0_.deptid=? 4 [User [id=49, name=姓名49], User [id=26, name=姓名26], User [id=35, name=姓名35], User [id=14, name=姓名14]] 5 --------------------- 6 Dept [id=5, name=部门5] 7 Hibernate: select users0_.deptid as deptid3_1_0_, users0_.Id as Id1_1_0_, users0_.Id as Id1_1_1_, users0_.name as name2_1_1_, users0_.deptid as deptid3_1_1_ from user users0_ where users0_.deptid=? 8 [User [id=26, name=姓名26], User [id=49, name=姓名49], User [id=14, name=姓名14], User [id=35, name=姓名35]]
这里我们可以看到select .... from dept 语句只有一句,说明对应Dept对象的二级缓存是成功了的。但是当我们查看该对象下users集合中的值时,在第一次查询的时候从数据库中读取了一次,第二次查询的时候又读取了一次,显然,对应对象的集合属性并没有缓存到二级缓存中,那么怎么才能够缓存集合属性了?
方法:在hibernate中添加collection-cache
<collection-cache usage="read-only" collection="com.myproc.domain.Dept.users"/>
collection属性为类的全限命名+集合属性名称
输出结果
1 Hibernate: select dept0_.Id as Id1_0_0_, dept0_.name as name2_0_0_ from dept dept0_ where dept0_.Id=? 2 Dept [id=5, name=部门5] 3 Hibernate: select users0_.deptid as deptid3_1_0_, users0_.Id as Id1_1_0_, users0_.Id as Id1_1_1_, users0_.name as name2_1_1_, users0_.deptid as deptid3_1_1_ from user users0_ where users0_.deptid=? 4 [User [id=35, name=姓名35], User [id=26, name=姓名26], User [id=49, name=姓名49], User [id=14, name=姓名14]] 5 --------------------- 6 Dept [id=5, name=部门5] 7 [User [id=35, name=姓名35], User [id=14, name=姓名14], User [id=26, name=姓名26], User [id=49, name=姓名49]]
☆查询缓存
首先看一下下面的代码:
1 Session session=factory.openSession(); 2 Transaction tran=session.beginTransaction(); 3 4 List list= session.createQuery("FROM User WHERE id<5").list(); 5 System.out.println(list); 6 7 tran.commit(); 8 session.close(); 9 10 System.out.println("---------------------"); 11 12 session=factory.openSession(); 13 tran=session.beginTransaction(); 14 15 User user=session.get(User.class, 1); 16 System.out.println(user); 17 18 List list2= session.createQuery("FROM User WHERE id<5").list(); 19 System.out.println(list2); 20 21 tran.commit(); 22 session.close(); 23 24 tran.commit(); 25 session.close();
输出结果
1 Hibernate: select user0_.Id as Id1_1_, user0_.name as name2_1_, user0_.deptid as deptid3_1_ from user user0_ where user0_.Id<5 2 [User [id=1, name=姓名1], User [id=2, name=姓名2], User [id=3, name=姓名3], User [id=4, name=姓名4]] 3 --------------------- 4 User [id=1, name=姓名1] 5 Hibernate: select user0_.Id as Id1_1_, user0_.name as name2_1_, user0_.deptid as deptid3_1_ from user user0_ where user0_.Id<5 6 [User [id=1, name=姓名1], User [id=2, name=姓名2], User [id=3, name=姓名3], User [id=4, name=姓名4]]
当我们第一次使用HQL查询出一个id<5的集合。在第一次使用HQL同样查询id<5的集合,还是在数据库中再查询了异常,结论查询语句不会使用缓存。
当在第二次中我们使用session.get()方式,我们查询id=1的user时,结果没有想数据库中查询,说明缓存中存在对应的数据。
【综上所诉】:查询语句不会使用缓存,但是会将查询的结果放在缓存中
Question:对于查询语句难道我们就没有办法进行缓存了吗?
Answer:答案是我们是有办法的。看看下面的代码
1 Session session=factory.openSession(); 2 Transaction tran=session.beginTransaction(); 3 4 Iterator iterator1= session.createQuery("FROM User WHERE id<5").iterate(); 5 while(iterator1.hasNext()){ 6 System.out.println(iterator1.next()); 7 } 8 9 tran.commit(); 10 session.close(); 11 12 System.out.println("---------------------"); 13 14 session=factory.openSession(); 15 tran=session.beginTransaction(); 16 17 User user=session.get(User.class, 1); 18 System.out.println(user); 19 20 Iterator iterator2= session.createQuery("FROM User WHERE id<5").iterate(); 21 while(iterator2.hasNext()){ 22 System.out.println(iterator2.next()); 23 } 24 25 tran.commit(); 26 session.close();
输出结果
1 Hibernate: select user0_.Id as col_0_0_ from user user0_ where user0_.Id<5 2 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 3 User [id=1, name=姓名1] 4 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 5 User [id=2, name=姓名2] 6 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 7 User [id=3, name=姓名3] 8 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 9 User [id=4, name=姓名4] 10 --------------------- 11 User [id=1, name=姓名1] 12 Hibernate: select user0_.Id as col_0_0_ from user user0_ where user0_.Id<5 13 User [id=1, name=姓名1] 14 User [id=2, name=姓名2] 15 User [id=3, name=姓名3] 16 User [id=4, name=姓名4]
结果说明:在第一次查询的时候,首先查询了满足条件对象的所有id值,然后在通过迭代方式,没迭代一次就查询一次对应id的对象,并将该对象放入缓存中。这种就是N+1的查询。N表示有多少个对象
在第二次中,使用session.get能够读取到之前缓存的数据,如果在使用HQL语句查询,那么还是先从数据库中查询出满足条件的所有ID,如果对应ID在缓存中存在,则直接从缓存中读取,否则需要再次从数据库中查询出来
上面的例子,刚好第二次查询所有数据都在缓存中存在,如果我们第二次查询id<8,那么我们看到结果将会是怎样
1 Hibernate: select user0_.Id as col_0_0_ from user user0_ where user0_.Id<8 2 User [id=1, name=姓名1] 3 User [id=2, name=姓名2] 4 User [id=3, name=姓名3] 5 User [id=4, name=姓名4] 6 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 7 User [id=5, name=姓名5] 8 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 9 User [id=6, name=姓名6] 10 Hibernate: select user0_.Id as Id1_1_0_, user0_.name as name2_1_0_, user0_.deptid as deptid3_1_0_ from user user0_ where user0_.Id=? 11 User [id=7, name=姓名7]
虽然使用迭代的方式可以HQL语句缓存问题,但是对于N+1条查询,的确非常影响性能,如果数据非常多,反而结果并不理想,所有我们推荐使用下面一种方式:
1 List list1=session.createQuery("FROM User WHERE id<5") 2 .setCacheable(true) 3 .list(); 4 System.out.println(list1); 5 tran.commit(); 6 session.close(); 7 8 System.out.println("---------------------"); 9 10 session=factory.openSession(); 11 tran=session.beginTransaction(); 12 13 List list2=session.createQuery("FROM User WHERE id<5") 14 .setCacheable(true) 15 .list(); 16 System.out.println(list2);
对Query设置能够使用缓存setCacheable(true),同时还需要启用查询缓存
<!-- 启用查询缓存 --> <property name="cache.use_query_cache">true</property>
输出结果
1 Hibernate: select user0_.Id as Id1_1_, user0_.name as name2_1_, user0_.deptid as deptid3_1_ from user user0_ where user0_.Id<5 2 [User [id=1, name=姓名1], User [id=2, name=姓名2], User [id=3, name=姓名3], User [id=4, name=姓名4]] 3 --------------------- 4 [User [id=1, name=姓名1], User [id=2, name=姓名2], User [id=3, name=姓名3], User [id=4, name=姓名4]]
【注意】:使用这种方式,要求条件不行完全一致,不存在包含关系
☆缓存策略
a、只读缓存(read-only):只读策略,表示数据不能够被更改
b、不严格的读/写缓存(nonstrict-read-write):需要更新数据,但是两个事务更新同一条记录的可能性很小,性能比读写缓存好
c、读/写缓存(read-write):允许少量的修改数据
d、事务缓存(transactional):缓存支持事务,发生异常的时候,缓存也能够回滚,只支持jta环境
读写缓存和不严格读写缓存在实现上的区别在于,读写缓存更新缓存的时候会把缓存里面的数据换成一个锁,其他事务如果去取相应的缓存数据,发现被锁住了,然后就直接取数据库查询。
各策略的性能从下往上越来越好
☆使用ehcache.xml配置文件
1 <!-- 2 ~ Hibernate, Relational Persistence for Idiomatic Java 3 ~ 4 ~ License: GNU Lesser General Public License (LGPL), version 2.1 or later. 5 ~ See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. 6 --> 7 <ehcache> 8 9 <!-- Sets the path to the directory where cache .data files are created. 10 11 If the path is a Java System Property it is replaced by 12 its value in the running VM. 13 14 The following properties are translated: 15 user.home - User's home directory 16 user.dir - User's current working directory 17 java.io.tmpdir - Default temp file path --> 18 <diskStore path="D:/cache/"/> 19 20 21 <!--Default Cache configuration. These will applied to caches programmatically created through 22 the CacheManager. 23 24 The following attributes are required for defaultCache: 25 26 maxInMemory - Sets the maximum number of objects that will be created in memory 27 eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the element 28 is never expired. 29 timeToIdleSeconds - Sets the time to idle for an element beforeQuery it expires. Is only used 30 if the element is not eternal. Idle time is now - last accessed time 31 timeToLiveSeconds - Sets the time to live for an element beforeQuery it expires. Is only used 32 if the element is not eternal. TTL is now - creation time 33 overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache 34 has reached the maxInMemory limit. 35 36 --> 37 <defaultCache 38 maxElementsInMemory="10" 39 eternal="false" 40 timeToIdleSeconds="120" 41 timeToLiveSeconds="120" 42 overflowToDisk="true" 43 /> 44 </ehcache>
<diskStore path="D:/cache/"/>:表示当缓存超出了maxElementsInMemory是,存到到硬盘的什么位置
在hibernate.cfg.xml文件中我们还需要指定ehcache的位置
1 <!-- 设置ehcache配置文件 --> 2 <property name="net.sf.ehcache.configurationResourceName">ehcache.xml</property>
下面我看一看在硬盘中缓存的文件
【最后提醒】
如果对于指定了可读性的策略是,通过HQL语句update或delete操作修改了对象后,hibernate会通知二级缓存,删除对于该对象的缓存信息。当下一次访问该对象时,首先访问二级缓存,此时二级缓存中该对象根本就不存在了,所有会查询数据库,然后将查询来的对象再次保存到二级缓存中。所有我们直观的看到,二级缓存中的信息是被更改了的。
而对于一级缓存,对于update的操作,不会通知session,所有对象属性还是在没有保存之前的数据,如果需要查看对象的新状态,则使用session.refresh(object);