又到了写总结的时候了,今天来扒拉扒拉hibernate的缓存问题,顺便做了几个小测试,算是学习加上复习吧,下面开始。
首先为什么要有缓存这项技术,详细原因呢我也不知道,唯一知道的一点就是web应用在和数据库打交道时,查询数据库的次数越少越好,可能说的也不太准确吧,什么意思呢,就是尽量减少查询数据库的次数,但是有时候一样的数据我们又需要查询很多次,怎么办呢?这时候就是缓存大展身手的时候了,可以把查询的数据先缓存下来,需要的时候从缓存里面来拿,如果缓存里面没有需要的数据再从数据库去查,这样就可以减少查询数据库的次数,提高应用的查询速度的同时减轻数据库的压力。
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,我自己可能说的没有那么清晰,所以就引用一下大神们的话)
ehcache是用的比较多的一个二级缓存框架,当然还有OSCache等很多二级缓存框架,我暂时就会用ehcache,所以就用ehcache做了个demo,下面详细说一下:
新建一个java项目,当然建web项目也可以,只是用不到页面而已,测试都是在junit中进行的。给项目添加hibernate支持,然后添加eacache的jar包,因为要和数据库打交道,数据库驱动包也是少不了的,junit的包也是需要的,下面是项目结构截图:
hibernate的版本是4.1版本,环境是myeclipse10.7,jdk是1.7版本,其他的就没有什么了,需要的两个配置文件分别是hibernate的配置文件和ehcache的配置文件,实体类使用了注解的方式。
如果要使用二级缓存,需要先配置二级缓存,首先在hibernate的配置文件中配置如下信息:
<!-- 开启二级缓存 --> <property name="hibernate.cache.use_second_level_cache">true</property> <!-- 二级缓存提供类 --> <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property> <!-- 二级缓存配置文件位置 --> <property name="hibernate.cache.provider_configuration_file_resource_path">ehcache.xml</property>
关于二级缓存的提供类还有另外一种写法,<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.xml文件中找到,我就不一一列举出来了。
上面的配置是默人配置,如果不指定缓存的对象,那么所有的二级缓存都是使用的默认配置,如果设置了缓存对象,那么则使用置顶的缓存对象配置,置顶缓存对象中没有配置的信息继承默认配置的。
<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) @Cacheable public class User { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id; private String name;
可以设置二级缓存的类型,READ_ONLY表示只读,二级缓存一般设置为只读形式的,因为效率最高,READ_WRITE表示读写,效率低但是可以保证并发正确,nonstrict-read-write:非严格的读写,效率较高,不用加锁,不能保证并发正确性。例如帖子浏览量。transactional:事务性缓存,可回滚缓存数据,一般缓存框架不带有此功能,实现很复杂。
如果是hbm.xml形式的,那么使用如下的方式开启:
<cache usage="read-only"/>
配置完成了我们来做下测试:
先看下数据库数据,数据库就三条数据:
先做第一个测试,在两个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()+"--------"); } catch (HibernateException e) { e.printStackTrace(); } finally { session.close(); } }
sql语句:
Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=? ------------301 301--------
通过上面的代码和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(); } }
看控制台:
这时候控制台报错了,什么情况呢?因为我们前面设置的二级缓存类型为只读类型,但是我们这里却修改了查询出来的数据,这是不允许的,所以报错了。
上面是查询一个对象 ,但是有时候我们查询的并不是对象,而是使用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 ?
---------------3
Hibernate: 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 ?
---------------3
Hibernate: 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语句,说明添加构造方法也不能进行二级缓存。
关于上面的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 ? ---------------3 3---------------
可以看到此时只有一条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 ?
---------------3
Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ where user0_.name like ?
1---------------
发出两条sql语句。
结论:通过上面的两个测试我们得出如下结论,第一,在没有配置查询缓存的情况下,二级缓存不会对hql进行的属性查询进行缓存,只会对查询出的对象进行缓存;第二,已经配置查询缓存的情况下,二级缓存只会缓存HQL语句和传入参数一模一样的查询结果进行缓存,如果不一样则不进行缓存。
写到这突然想到一个问题,上面的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_ ---------------3 Hibernate: 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_
---------------3
3---------------
查询结果是只发出了一条sql语句,跟上面的结论符合。
首先,什么是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=? 301 Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=? 302 Hibernate: 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---301 it---302 it---303
可以看到就发出了两条语句,并没有出现N+1的情况。
查询缓存也是会引起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 ? ---------------3 301 302 303 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=? Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_ from t_class user0_ where user0_.id=? 3--------------- 301 302 303
可以看到,在去掉二级缓存之后,进行查询缓存时又出现了N+1问题,这是由于查询缓存缓存的是id,虽然此时缓存中已经存在了这样一组数据,但是只有id,那么需要user的其他属性时候就需要根据id去查,这也是导致N+1问题的一个原因。所以在使用查询缓存的时候还是必须开启二级缓存的。
前面一直是使用read-only测试的,下面来一个read-write测试的,read-write表示可以更改,更改之后就需要重新就行查询了,二级缓存是不好使的,下面上测试:
首先把二级缓存的类型改为read-write
@Entity @Table(name="t_class") @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) @Cacheable public 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_
111111111111111301
111111111111111302
111111111111111303
111111111111111304
111111111111111305
222222222222301
222222222222302
222222222222303
222222222222304
222222222222305
此时只发出了一条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_ 111111111111111301 111111111111111302 111111111111111303 111111111111111304 111111111111111305 Hibernate: insert into t_class (name) values (?) Hibernate: select user0_.id as id0_, user0_.name as name0_ from t_class user0_ 222222222222301 222222222222302 222222222222303 222222222222304 222222222222305 222222222222306
中间进行过新增操作之后,重新发出了查询的sql语句,这是因为read-write是允许写入操作的,在发生写入操作之后,需要重新发出sql语句进行查询,将查询结果放入二级缓存,如果中间没有发生其他写入操作,那么下次查询的时候就会从二级缓存里面读取数据了。
关于hibernate的缓存就暂时写到这里了,主要说的是二级缓存,一级缓存其实也没什么说的,也可能是我的理解达不到吧,有问题的地方欢迎大家指正,谢谢。