你的位置:首页 > Java教程

[Java教程]hibernate的缓存问题


       又到了写总结的时候了,今天来扒拉扒拉hibernate的缓存问题,顺便做了几个小测试,算是学习加上复习吧,下面开始。

1.综述

  首先为什么要有缓存这项技术,详细原因呢我也不知道,唯一知道的一点就是web应用在和数据库打交道时,查询数据库的次数越少越好,可能说的也不太准确吧,什么意思呢,就是尽量减少查询数据库的次数,但是有时候一样的数据我们又需要查询很多次,怎么办呢?这时候就是缓存大展身手的时候了,可以把查询的数据先缓存下来,需要的时候从缓存里面来拿,如果缓存里面没有需要的数据再从数据库去查,这样就可以减少查询数据库的次数,提高应用的查询速度的同时减轻数据库的压力。

2.hibernate缓存

  hibernate的缓存机制大概可以分为一级缓存和二级缓存,有时候还会用到查询缓存,一级缓存是默认开启的,一级缓存是session共享的,因此可以叫做session缓存或者叫做事务缓存,save,update,saveOrUpdate,load,get,list,iterate这些方法都会把对象放在一级缓存中,但是一级缓存不能控制缓存的数量,所以在操作大批量数据时有可能导致内存溢出,可以使用clear,evict方法来清空一级缓存,一级缓存依赖于session,session的生命周期结束了,一级缓存也会跟着结束。

  hibernate的二级缓存是可插拔的,要启用二级缓存需要第三方支持,hibernate内置了对EhCache,OSCache,TreeCache,SwarmCache的支持,可以通过实现CacheProvider和Cache接口加入。

session的save(不适合native方式生成的主键),update,saveOrUpdate,list,iterator,get,load,以及Query, Criteria都会填充二级缓存,但查询缓存时,只有Session的iterator,get,load会从二级缓存中取数据。Query,Criteria由于命中率较低,所以hibernate缺省是关闭的。Query查询命中低的一方面原因就是条件很难保证一致,且数据量大,无法保证数据的真实性。

  hibernate的二级缓存是sessionFactory级别,也就是跨session的,由于SessionFactory对象的生命周期和应用程序的整个过程对应,因此Hibernate二级缓存是进程范围或者集群范围的缓存,有可能出现并发问题,因此需要采用适当的并发访问策略,该策略为被缓存的数据提供了事务隔离级别。

(上述两段引自:http://www.cnblogs.com/shiyangxt/archive/2008/12/30/1365407.html,我自己可能说的没有那么清晰,所以就引用一下大神们的话)

  

3.ehcache实现hibernate二级缓存

  ehcache是用的比较多的一个二级缓存框架,当然还有OSCache等很多二级缓存框架,我暂时就会用ehcache,所以就用ehcache做了个demo,下面详细说一下:

3.1 新建项目

  新建一个java项目,当然建web项目也可以,只是用不到页面而已,测试都是在junit中进行的。给项目添加hibernate支持,然后添加eacache的jar包,因为要和数据库打交道,数据库驱动包也是少不了的,junit的包也是需要的,下面是项目结构截图:

  

关于二级缓存的提供类还有另外一种写法,<property name="hibernate.cache.provider_class"> org.hibernate.cache.EhCacheProvider </property>,这种方式是hibernate3的版本使用的,hibernate4版本不推荐使用,但是如果使用的是hibernate3版本的话也可以使用这个。

  开启二级缓存并且加入二级缓存提供类之后就需要ehcache的配置文件了,配置文件详细如下:

 

<diskStore path="java.io.tmpdir"/>  <defaultCache      maxElementsInMemory="10000"      eternal="true"      overflowToDisk="true"      maxElementsOnDisk="10000000"      diskPersistent="true"      diskExpiryThreadIntervalSeconds="120"      />

 

  diskStore中的path路径可以设置为硬盘路径,也可以使用如上的设置方式,表示默认存储路径。

  defaultCache为默认的缓存设置,maxElementsInMemory : 在內存中最大緩存的对象数量。

                  eternal : 缓存的对象是否永远不变。

                   timeToIdleSeconds :可以操作对象的时间。

                  timeToLiveSeconds :缓存中对象的生命周期,时间到后查询数据会从数据库中读取。

                  overflowToDisk :内存满了,是否要缓存到硬盘。

其他的属性可以在ehcache的core包中的chcache-failsafe.

  上面的配置是默人配置,如果不指定缓存的对象,那么所有的二级缓存都是使用的默认配置,如果设置了缓存对象,那么则使用置顶的缓存对象配置,置顶缓存对象中没有配置的信息继承默认配置的。

 <cache  name="modal.User"  maxElementsInMemory="200" eternal="false"   timeToIdleSeconds="50"   timeToLiveSeconds="60"   overflowToDisk="true"  />

  上面的就是指定缓存对象的配置,注意name中要把类的包名带上。

上面的工作做完之后就是开启二级缓存了,如果是注解形式的,那么使用如下的方式开启:

@Entity@Table(name="t_class")@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)@Cacheablepublic class User {  @Id  @GeneratedValue(strategy=GenerationType.IDENTITY)  private int id;  private String name;

  可以设置二级缓存的类型,READ_ONLY表示只读,二级缓存一般设置为只读形式的,因为效率最高,READ_WRITE表示读写,效率低但是可以保证并发正确,nonstrict-read-write:非严格的读写,效率较高,不用加锁,不能保证并发正确性。例如帖子浏览量。transactional:事务性缓存,可回滚缓存数据,一般缓存框架不带有此功能,实现很复杂。

  如果是hbm.

    <cache usage="read-only"/>

3.3 测试

  配置完成了我们来做下测试:

先看下数据库数据,数据库就三条数据:


sql语句:

Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=?------------301301--------

  通过上面的代码和sql语句以及结果可以看到,在一个sessionFactory的两个session中查询一条记录时,只发出了一条sql语句,说明此时二级缓存是好使的。

下面看一个报错的情况:

@Test  public void test() {    Session session = null;    try {      session = HibernateSessionFactory.getSession();      User u1 = (User)session.load(User.class, 1);      System.out.println("------------"+u1.getName());    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      User u1 = (User)session.load(User.class, 1);      System.out.println(u1.getName()+"--------");            session.beginTransaction();      u1.setName("222");      session.beginTransaction().commit();    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

  看控制台:

 

 

  这时候控制台报错了,什么情况呢?因为我们前面设置的二级缓存类型为只读类型,但是我们这里却修改了查询出来的数据,这是不允许的,所以报错了。

3.3.2

  上面是查询一个对象 ,但是有时候我们查询的并不是对象,而是使用HQL查询对象中的一个或者几个属性,这时候二级缓存好使吗?拭目以待吧:

@Test  public void testQueryHql2() {    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User u where u.name like ? ").setParameter(0, "%"+3+"%").list();      System.out.println("---------------"+list.size());    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User user where user.name like ?").setParameter(0, "%"+1+"%").list();      System.out.println(list.size()+"---------------");    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

  上面是测试代码,HQL语句是一样的,只是参数不一样,测试一下结果如下:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?---------------3Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?1---------------

  可以看到,发出了两条sql语句,说明二级缓存没有起作用,那么当参数一样的时候二级缓存能起作用吗?看测试代码:

@Test  public void testQueryHql2() {    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User u where u.name like ? ").setParameter(0, "%"+3+"%").list();      System.out.println("---------------"+list.size());    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User user where user.name like ?").setParameter(0, "%"+3+"%").list();      System.out.println(list.size()+"---------------");    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

直接上结果:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?---------------3Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?3---------------

  可以看到即使条件一样也是发出了两条sql语句,此时二级缓存也没有起作用。

结论:通过上面两个测试,我们可以知道二级缓存缓存的仅仅是对象,对于属性是不会进行缓存的。

  再来个测试,如果要查询的属性在实体类中的构造方法中呢,此时能不能进行缓存呢?看测试:

public class User {  @Id  @GeneratedValue(strategy=GenerationType.IDENTITY)  private int id;  private String name;    public User() {      }  public User(String name) {    super();    this.name = name;  }

  将User添加两个构造方法之后发现还是发出两条sql语句,说明添加构造方法也不能进行二级缓存。

 

3.3.3

  关于上面的hql查询属性不能进行二级缓存的问题,有人说我就要进行二级缓存怎么办呢?其实还是有办法的,什么办法呢?查询缓存,此时需要再配置一些东西:

 首先是hibernate的配置文件:

<!-- 启用查询缓存 -->    <property name="hibernate.cache.use_query_cache">true</property>

然后再查询的实体类上加上@Cacheable注解即可,下面测试重新配置之后的代码:

@Test  public void testQueryHql2() {    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User u where u.name like ? ").setCacheable(true).setParameter(0, "%"+3+"%").list();      System.out.println("---------------"+list.size());    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User user where user.name like ?").setCacheable(true).setParameter(0, "%"+3+"%").list();      System.out.println(list.size()+"---------------");    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

  还是这段代码,测试结果如下:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?---------------33---------------

  可以看到此时只有一条sql语句发出。

如果两次查询条件不一样呢?看测试:

@Test  public void testQueryHql2() {    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User u where u.name like ? ").setCacheable(true).setParameter(0, "%"+3+"%").list();      System.out.println("---------------"+list.size());    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User user where user.name like ?").setCacheable(true).setParameter(0, "%"+1+"%").list();      System.out.println(list.size()+"---------------");    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

测试结果:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?---------------3Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?1---------------

  发出两条sql语句。

结论:通过上面的两个测试我们得出如下结论,第一,在没有配置查询缓存的情况下,二级缓存不会对hql进行的属性查询进行缓存,只会对查询出的对象进行缓存;第二,已经配置查询缓存的情况下,二级缓存只会缓存HQL语句和传入参数一模一样的查询结果进行缓存,如果不一样则不进行缓存。

3.3.4

  写到这突然想到一个问题,上面的3.3.3的结论有点问题,但是不想改上面的了,就在这补充一下,并不是说只有在查询属性的时候二级缓存才不会缓存数据,而是说在使用hql的时候,及时查询出来的是对象,二级缓存也只会对对象进行缓存,但是对于hql语句是不会缓存的,如果要想缓存,那么久需要开启查询缓存,方法已经给出了,下面补充一个测试,我先把查询缓存给注掉,然后看测试:

@Test  public void testQueryHql() {    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").list();      System.out.println("---------------"+list.size());    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").list();      System.out.println(list.size()+"---------------");    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

  看此时的查询结果:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_---------------3Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_3---------------

可以看到此时查询的并不是属性,并且两条hql语句也一样,查询时还是发出了两条sql语句,此时再开启查询缓存,看下结果,两次代码是基本一样的,开启查询缓存后只需要加上.setCacheable(true)即可,直接给结果:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_---------------33---------------

查询结果是只发出了一条sql语句,跟上面的结论符合。

4、N+1问题

4.1

  首先,什么是N+1问题,N+1问题是执行条件查询时,在第一次查询时iterate方法会执行满足条件的查询结果数再加一次(n+1)的查询,但是此问题只存在于第一次查询时,在后面执行相同查询时性能会得到极大的改善。

  我们先举个例子来说明什么是N+1问题:

 

@Test  public void iteraTest(){    Session session = null;    try {      session = HibernateSessionFactory.getSession();      Iterator<User> it = session.createQuery("from User").iterate();       for (; it.hasNext();)        {         User u = (User) it.next();          System.out.println(u.getName());        }          } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }  }  

 

上面代码的sql语句如下:

Hibernate: select user0_.id as col_0_0_ from t_class user0_Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=?301Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=?302Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=?303

上面的sql语句可以看到,先查询出全部数据的id,再根据id查询其他数据,有N条数据就发出几条sql,再加上查询全部id的一条sql语句,总共就发出了N+1条sql语句。

  其实N+1问题也是很容易解决的,只需要使用list()的方式查询就行了,如下所示:

    try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").list();          } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }    

  这也是我们平常经常用的一个方式,这样就不会出现N+1问题了,使用二级缓存也可以解决N+1问题,怎么办呢?如下所示:

@Test  public void N1Test(){    Session session = null;    try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").list();          } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }         /*     * 由于user的对象已经缓存在二级缓存中了,此时再使用iterate来获取对象的时候,首先会通过一条     * 取id的语句,然后在获取对象时去二级缓存中,如果发现就不会再发SQL,这样也就解决了N+1问题     * 而且内存占用也不多     */        try {      session = HibernateSessionFactory.getSession();      Iterator<User> it = session.createQuery("from User").iterate();       for (; it.hasNext();)        {         User u = (User) it.next();          System.out.println("it---"+u.getName());        }          } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

  测试结果:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_Hibernate: select user0_.id as col_0_0_ from t_class user0_it---301it---302it---303

  可以看到就发出了两条语句,并没有出现N+1的情况。

 

 

4.2

  查询缓存也是会引起N+1问题的,我们先去掉User对象上的@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)注解,然后看测试代码:

@Test  public void testQueryHql3() {    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User u where u.name like ? ").setCacheable(true).setParameter(0, "%"+3+"%").list();      System.out.println("---------------"+list.size());      for(User u :list){        System.out.println(u.getName());      }    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User user where user.name like ?").setCacheable(true).setParameter(0, "%"+3+"%").list();      System.out.println(list.size()+"---------------");      for(User u :list){        System.out.println(u.getName());      }    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

测试结果:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?---------------3301302303Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=?Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=?Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=?3---------------301302303

  可以看到,在去掉二级缓存之后,进行查询缓存时又出现了N+1问题,这是由于查询缓存缓存的是id,虽然此时缓存中已经存在了这样一组数据,但是只有id,那么需要user的其他属性时候就需要根据id去查,这也是导致N+1问题的一个原因。所以在使用查询缓存的时候还是必须开启二级缓存的。

  

5.read-write

  前面一直是使用read-only测试的,下面来一个read-write测试的,read-write表示可以更改,更改之后就需要重新就行查询了,二级缓存是不好使的,下面上测试:

首先把二级缓存的类型改为read-write

@Entity@Table(name="t_class")@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)@Cacheablepublic class User {  @Id  @GeneratedValue(strategy=GenerationType.IDENTITY)  private int id;  private String name;

 下面上第一个测试:

@Test  public void testAdd(){    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").setCacheable(true).list();      for(User u : list){        System.out.println("111111111111111"+u.getName());      }    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }          try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").setCacheable(true).list();      for(User u : list){        System.out.println("222222222222"+u.getName());      }    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

看下查询结果:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_111111111111111301111111111111111302111111111111111303111111111111111304111111111111111305222222222222301222222222222302222222222222303222222222222304222222222222305

此时只发出了一条sql语句,表示二级缓存成功,注意红色部分。

下面做第二个测试,一样的代码,只不过需要在中间加上一部分代码:

@Test  public void testAdd(){    Session session = null;        try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").setCacheable(true).list();      for(User u : list){        System.out.println("111111111111111"+u.getName());      }    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }        try {      session = HibernateSessionFactory.getSession();      User u = new User();      u.setName("306");      session.save(u);    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      try {      session = HibernateSessionFactory.getSession();      List<User> list = session.createQuery("from User").setCacheable(true).list();      for(User u : list){        System.out.println("222222222222"+u.getName());      }    } catch (HibernateException e) {      e.printStackTrace();    } finally {      session.close();    }      }

  此时的测试结果为:

Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_111111111111111301111111111111111302111111111111111303111111111111111304111111111111111305Hibernate: insert into t_class (name) values (?)Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_222222222222301222222222222302222222222222303222222222222304222222222222305222222222222306

  中间进行过新增操作之后,重新发出了查询的sql语句,这是因为read-write是允许写入操作的,在发生写入操作之后,需要重新发出sql语句进行查询,将查询结果放入二级缓存,如果中间没有发生其他写入操作,那么下次查询的时候就会从二级缓存里面读取数据了。

 

  6.总结

关于hibernate的缓存就暂时写到这里了,主要说的是二级缓存,一级缓存其实也没什么说的,也可能是我的理解达不到吧,有问题的地方欢迎大家指正,谢谢。