Hibernate的缓存
缓存是hibernate进行性能优化的一个很好的手段,上面我们学习了抓取策略也是hibernate的性能优化的方式,现在我们来看hibernate的另一个优化方式—缓存。
Hibernate中的缓存包括了一级缓存和二级缓存,一级缓存是session级别的缓存,就是说,只要这个session不关闭,之前查询出来的数据就会被缓存到session中,当我们再去查询该数据时,hibernate不会再次发生sql查询,而是会取出session中的数据使用。这就是一级缓存。
一级缓存和N+1事件
与一级缓存相关的就是N+1事件,什么是N+1事件呢?下面我们来看看,简单的演示一个N+1事件:
@Test publicvoid test02() { Session session = null; try { session = HibernateUtil.openSession(); /** * 在HQL语句中存在一个iterate方法,该方法可以将一个list列表 * 直接转换为一个迭代器,如果我们使用了这个方法,我们会发现一旦 * 我们使用了数据,一条数据会发送一条sql,这就是N+1事件 */ Iterator<Student> it = session.createQuery("from Student").iterate(); while (it.hasNext()) { System.out.println(it.next().getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
代码运行效果如下:
我们发现一条语句会发送一条sql,这样效率肯定很低,那么hibernate为什么提供这个方法呢?
如果大家仔细看的话,会发现,sql比数据多了一条,就是第一条sql,我们研究这条sql发现这条sql仅仅查询了一个所有数据的id的sql,那么为什么会这样了,这就说的hibernate的一级缓存了。
hibernate中的一级缓存就是如果数据已经在前面查询出来了,那么如果session此时没有被关闭,这数据会缓存到session,这样我们如果在后面使用到该数据,hibernate就不会发生sql了,这就是一级缓存,所以一级缓存也叫做session级别的缓存。而iterate方法只会去查询数据的id,hibernate是这样认为的,如果我们前面使用了list等方法查询出了数据,并且session没有关闭,数据缓存了此时使用iterate方法,我们只要再去查询一下数据的id就行了,id查询回来后就会和缓存的数据对比,找到需要的数据。
下面我们来在测试下:
@Test publicvoid test03() { Session session = null; try { session = HibernateUtil.openSession(); List<Student> list = session.createQuery("from Student").list(); Iterator<Student> it = list.iterator(); while (it.hasNext()) { System.out.println(it.next().getName()); } /** * 重新去查,看看会不会发送sql,我们发现发送了一条sql, * 但是这条sql仅仅查询了id而已,当我们在前面查询了数据, * 此时数据缓存了,我们再使用iterate方法查询的时候,只会发送一条 * 查询ID的sql,这样就减少了对内存的消耗 */ it = session.createQuery("from Student").iterate(); while (it.hasNext()) { System.out.println(it.next().getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
当然iterate方法建议大家慎重使用。我们一般不会使用它,只有我们保证数据之前查询过,并且session没有关闭,否则使用它很容易触发N+1事件的。其实iterate方法主要是在二级缓存中使用的,这个我们后面讲解二级缓存的时候再说。
其实一级缓存我们发现不是很常用,因为我们在查询数据之后都会关闭session的,这样来说一级缓存的用处就不多了,其次如果我们不使用iterate方法,再次使用list方法也行,只是内存占用比较多,但是现在服务器的数据瓶颈早已经不是内存这儿了,内存现在比较便宜,储量现在都比较大,不值得为了一点内存使用iterate方法,而且很容易触发N+1事件。
二级缓存
Hibernate的效率优化除了抓取策略外,主要就是二级缓存,一级缓存不建议大家使用。前面我们说过一级缓存是session级别的缓存,session关闭,则一级缓存失效,而二级缓存则是SessionFactory级别的缓存,所以只要SessionFactory存在,则二级缓存就有效。但是Hibernate本身没有提供二级缓存,所以我们要使用二级缓存的话,需要借助第三方的工具,常用的二级缓存工具有—EHCache。下面我们就以EHCache为例,讲解hibernate中的二级缓存的使用。
Hibernate配置二级缓存的步骤
1、 导入EHCache的jar包;
2、 在hibernate.cfg.xml文件中开启相应的配置
3、 在src目录下创建一个ehcache.xml文件(EHCache的配置文件)
4、 开启二级缓存
下面我们来详细的配置一遍二级缓存吧。
导入EHCache的jar包
首先需要一个hibernate项目,我们使用前面XML配置的项目,其次需要ehcache的jar包,注意:hibernate给我们提供了ehcache的jar包,在lib文件夹下的option文件夹下,有一个ehcache的文件夹,这个下面的三个jar文件都是EHCache所需的jar包:
将这三个jar文件拷入项目中。
在hibernate.cfg.xml文件中开启相应的配置
我们要在hibernate中开启二级缓存,需要配置如下信息:
<!-- 二级缓存部分 --> <!-- 表示该项目要使用二级缓存,开启二级缓存 --> <property name="hibernate.cache.use_second_level_cache">true</property> <!-- 告诉hibernate我们要使用的缓存提供类是哪个(我们使用的是EhCacheProvider) --> <property name="hibernate.cache.provider_class">net.sf.ehcache.hibernate.EhCacheProvider</property> <!--4.0之后需要使用这个配置,不能少,据说可以提高效率 --> <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property> |
在src目录下创建一个ehcache.xml文件(EHCache的配置文件)
这个文件其实hibernate给我们已经提供了,在hibernate的project/etc下就有,这个目录下hibernate为我们提供了好多我们可能用的配置。
复制到项目的src目录下。
下面我们详细的说明下ehcache.xml文件中的一些配置说明,其他的后面再说。
<!-- 这个说明我们的缓存保存在项目的临时文件夹下 --> <diskStore path="java.io.tmpdir"/> |
<!-- 这个是默认的配置 maxElementsInMemory="10000"表示最大的缓存对象是一万个 eternal="false" 表示是否永久缓存,一般不会,所以值一般是false timeToIdleSeconds="120" 表示不活动(空闲)的对象多久更新一次,单位是秒 timeToLiveSeconds="120" 表示活动的对象多久更新一次,单位是秒 overflowToDisk="true" 表示如果超过了缓存对象的数量,我们就放入硬盘中 --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> |
之后在hibernate.cfg.xml文件中再次配置一下,告诉hibernate配置的ehcache.xml文件的位置:
<!—告诉hibernate,我们的ehcache配置文件的位置 --> <property name="hibernate.cache.provider_configuration_file_resource_path"> ehcache.xml</property> |
这样我们就完成了ehcache.xml的配置了。
开启二级缓存
在项目中要开启二级缓存有两种方式,就是在实体类的配置文件中去写,如下我们在Student.hbm.xml文件中配置:
<hibernate-mapping package="com.lzcc.hibernate.entity"> <class name="Student" table="student"> <!-- 注意:我们一般二级缓存都只配置read-only,不会配置可以变化的对象的 --> <cache usage="read-only" /> <id name="id" > <generator class="native" /> </id> <property name="name" /> <property name="sex" /> <many-to-one name="classroom" column="cid" fetch="join"/> </class> </hibernate-mapping> |
我们在class节点下面配置cache节点,注意的是,usage一般都是只读。
这样我们就完成了二级缓存的配置了,Student类现在就是存在了二级缓存的。下面我们来测试是否配置成功。
@Test publicvoid test01() { Session session = null; try { session = HibernateUtil.openSession(); Student stu = (Student) session.load(Student.class, 2); System.out.println(stu.getName()); } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } /** * 没有再发送sql,说明hibernate的二级缓存生效了 */ session = HibernateUtil.openSession(); Student stu = (Student) session.load(Student.class, 2); System.out.println(stu.getName()); } |
我们发现第二次查询的时候,hibernate并没有发送sql,这就说明二级缓存已经生效了。
下面我们来详细的看看二级缓存的一些知识吧。
Hibernate二级缓存的细节
前面我们给实体类配置二级缓存是ehcache.xml中默认的配置,其实我们还可以为一个对象配置一个独立的缓存配置,就是在ehcache.xml文件中如下配置:
<!-- 我们也可以在这个给实体类配置二级缓存,如下 注意:指明类时要加包路径,这样Student这个对象 就不会使用上面默认的配置,而是使用下面的配置,其他 还是使用上面默认的配置--> <cache name="com.lzcc.hibernate.entity.Student" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> |
再次测试代码:
这样Student就是使用了我们上面配置的缓存配置,没有使用默认的配置。
下面我们来看其他的一些二级缓存的问题:
@Test publicvoid test02() { Session session = null; try { session = HibernateUtil.openSession(); Student stu = (Student) session.load(Student.class, 2); System.out.println(stu.getName()); } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } /** * 没有再发送sql,说明hibernate的二级缓存生效了 */ try { session = HibernateUtil.openSession(); session.beginTransaction(); Student stu = (Student) session.load(Student.class, 2); /** * 注意,代码会报错,因为这个的对象是二级缓存中的对象, * 而二级缓存中的对象是只读的对象,如果我们要修改,则会 * 报错 */ stu.setName("王老板"); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
注意:二级缓存中的对象我们一般配置成了只读的对象,不要去更新它。
@Test publicvoid test03() { Session session = null; try { session = HibernateUtil.openSession(); List<Student> stus = (List<Student>) session.createQuery("from Student").list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } try { session = HibernateUtil.openSession(); session.beginTransaction(); /** * 没有再发送sql,因为上面已经缓存了对象,所以就不会发生sql */ Student stu = (Student) session.load(Student.class, 2); System.out.println(stu.getName()+"***********"); } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
注意:二级缓存是缓存持久化对象,它是把所有的对象缓存到内存中了,一定要记得,hibernate的二级缓存是基于存持久化对象的缓存。
下面我们来看看为什么我们说hibernate的二级缓存是基于对象的缓存呢?
@Test publicvoid test04() { Session session = null; try { session = HibernateUtil.openSession(); List<Object[]> stus = (List<Object[]>) session.createQuery("select s.id, s.name from Student s").list(); } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } try { session = HibernateUtil.openSession(); session.beginTransaction(); /** * 会发送sql,因为hibernate的二级缓存是基于对象的缓存,上面的 * 查询不是Student对象,所以此处话发送一条sql完成查询 */ Student stu = (Student) session.load(Student.class, 2); System.out.println(stu.getName()+"***********"); } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
@Test publicvoid test05() { Session session = null; try { session = HibernateUtil.openSession(); List<Student> stus = (List<Student>) session.createQuery("from Student").list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } try { session = HibernateUtil.openSession(); /** * 我们发现没有了N+1事件,这是因为二级缓存生效了 * 所以iterate方法主要是在二级缓存中使用 */ Iterator<Student> stus = session.createQuery("from Student").iterate(); while (stus.hasNext()) { System.out.println(stus.next().getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
Iterate方法主要是在二级缓存中使用的,配合二级缓存,这样就没有了N+1事件了。但是二级缓存在HQL语句中除了iterate方法外,list则失效,如下代码:
@Test publicvoid test06() { Session session = null; try { session = HibernateUtil.openSession(); List<Student> stus = (List<Student>) session.createQuery("from Student").list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } try { session = HibernateUtil.openSession(); /** * 这里会发送两条一模一样的sql,因为二级缓存仅仅只是缓存对象, * hql查询如果使用了二级缓存,iterate方法可以使用, * list不能缓存,如果这里也想发送一条sql,则要使用查询缓存 */ List<Student> stus = session.createQuery("from Student").list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
如果这里也想发送一条sql,则要使用查询缓存。
查询缓存
Hibernate中的查询缓存是针对HQL语句的缓存,查询缓存只会缓存hql语句和id,而不会缓存对象(二级缓存缓存的是对象,这里再次强调)。这里注意:查询缓存也是SessionFactory级别的缓存。
我们要使用查询缓存,我们要在SessionFactory中配置开启查询缓存,如果配置呢?在hibernate.cfg.xml文件中配置如下的信息:
<property name="hibernate.cache.use_second_level_cache">true</property> <!-- 表示开启查询缓存 --> <property name="hibernate.cache.use_query_cache">true</property> |
这样就开启了查询缓存了,那么我们怎么使用查询缓存呢?比较简单,因为查询缓存是针对hql语句的,使用如下:
@Test publicvoid test07() { Session session = null; try { session = HibernateUtil.openSession(); List<Student> stus = (List<Student>) session .createQuery("from Student") .setCacheable(true)//设置setCacheable(true)表示使用查询缓存 .list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } try { session = HibernateUtil.openSession(); /** * 我们发现使用了查询缓存后,这段代码只是发送了一条sql */ List<Student> stus = session. createQuery("from Student") .setCacheable(true)//再次查询时必须也设置 .list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
但是查询缓存一定要慎重使用,如下代码:
@Test publicvoid test08() { Session session = null; try { session = HibernateUtil.openSession(); List<Student> stus = (List<Student>) session .createQuery("from Student where name like ?") .setCacheable(true)//设置setCacheable(true)表示使用查询缓存 .setParameter(0, "%王%") .list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } try { session = HibernateUtil.openSession(); /** * 我们发现查询缓存无效,这是因为查询缓存缓存的是hql,一旦hql语句不 * 一致,则查询缓存失效,即便只是参数不一致 * 所以查询缓存慎重使用,在项目设计之初就要考虑,一般只有一些对象如果 * 查询时语句一直不变,否则不建议使用查询缓存 */ List<Student> stus = session .createQuery("from Student where name like ?") .setCacheable(true)//再次查询时必须也设置 .setParameter(0, "%张%") .list(); for (Student student : stus) { System.out.println(student.getName()); } } catch (Exception e) { e.printStackTrace(); if(session != null) { session.getTransaction().rollback(); } } finally { HibernateUtil.closed(session); } } |
我们发现查询缓存只会缓存hql语句一致的,所以建议大家慎重使用,在项目设计之初就要考虑如果存在查询对象hql语句一直不变的才建议使用查询缓存,否则不建议使用,并且因为查询缓存缓存的是id,所以如果我们一旦某个对象没有使用二级缓存,则查询缓存因为缓存id,会发送大量的sql完成查询,因此更加不建议大家使用查询缓存(这个是真的新手,因为不熟悉查询缓存,可能关闭二级缓存,引起大量sql,这也是引起N+1事件的第二个原因)。
查询缓存的最佳使用建议
1、 只有在查询hql不发生变化的对象使用查询缓存
2、 使用查询缓存的对象时,该对象的二级缓存一定要开启,否则会触发N+1事件。
使用annotation配置二级缓存
前面我们使用XML来配置二级缓存,在hibernate.cfg.xml文件中先配置,再在需要二级缓存的对象的xxx.hbm.xml文件中开启二级缓存,那么annotation如何配置二级缓存呢?其实使用annotation配置二级缓存和XML差不多,hibernate.cfg.xml文件一样,只是因为annotation配置的hibernate没有实体类对应的xxx.hbm.xml文件,所以需要一个annotation来配置二级缓存,如下:
还是XML配置的几个步骤:
1、 添加三个jar文件
2、 在hibernate.cfg.xml文件中配置
3、 在src目录下创建一个ehcache.xml文件(EHCache的配置文件)
4、 使用@Cache 这个annotation来开启对象的二级缓存
前面的步骤就不写了,我们看第四步:
package com.lzcc.hibernate.entity;
import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import org.hibernate.annotations.Cache;//导入的是hibernate的路径 import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity @Table(name="student") @Cache(usage=CacheConcurrencyStrategy.READ_ONLY)//开启Student的二级缓存 public class Student { private int id; private String name; private String sex; private Classroom classroom;
@Id @GeneratedValue public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSex() { return sex; }
public void setSex(String sex) { this.sex = sex; }
//注意:annotation配置的二级缓存如果存在关联关系 //想要二级缓存生效,则注意: //如果关联关系没有使用二级缓存,则开启延迟加载,否则 //因为要查询关系关系对象,所以查询缓存失效 //如果关联关系已经都使用了二级缓存,则延迟加载可开启 //也可不开启,不会影响二级缓存的使用 @ManyToOne @JoinColumn(name="cid") public Classroom getClassroom() { return classroom; }
public void setClassroom(Classroom classroom) { this.classroom = classroom; }
public Student(String name, String sex, Classroom classroom) { super(); this.name = name; this.sex = sex; this.classroom = classroom; }
public Student() { }
@Override public String toString() { return "Student [id=" + id + ", name=" + name + ", sex=" + sex + ", classroom=" + classroom + "]"; }
} |
这样就完成了使用annotation配置hibernate的二级缓存了。