Hibernate缓存原理:
对于Hibernate这类ORM而言,缓存显的尤为重要,它是持久层性能提升的关键.简单来讲Hibernate就是对JDBC进行封装,以实现内部状态的管理,OR关系的映射等,但随之带来的就是数据访问效率的降低,和性能的下降,而缓存就是弥补这一缺点的重要方法.
缓存就是数据库数据在内存中的临时容器,包括数据库数据在内存中的临时拷贝,它位于数据库与数据库访问层中间.ORM在查询数据时首先会根据自身的缓存管 理策略,在缓存中查找相关数据,如发现所需的数据,则直接将此数据作为结果加以利用,从而避免了数据库调用性能的开销.而相对内存操作而言,数据库调用是 一个代价高昂的过程.
一般来讲ORM中的缓存分为以下几类:
1:事务级缓存:即在当前事务范围内的数据缓存.就Hibernate来讲,事务级缓存是基于 Session的生命周期实现的,每个Session内部会存在一个数据缓存,它随着 Session的创建而存在,随着Session的销毁而灭亡,因此也称为Session Level Cache.
2:应用级缓存:即在某个应用中或应用中某个独立数据库访问子集中的共享缓存,此缓存可由多个事务 共享(数据库事务或应用事务),事务之间的缓存共享策略与应用的事务隔离机制密切相关.在Hibernate中,应用级缓存由 SessionFactory实现,所有由一个SessionFactory创建的 Session实例共享此缓存,因此也称为SessionFactory Level Cache.
3:分布式缓存:即在多个应用实例,多个JVM间共享的缓存策略.分布式缓存由多个应用级缓存实例组成,通过某种远程机制(RMI,JMS)实现各个缓存实例间的数据同步,任何一个实例的数据修改,将导致整个集群间的数据状态同步.
Hibernate的一,二级缓存策略:
Hibernate中提供了两级Cache,第一级别的缓存是Session级别的缓存,它是属于事务范围的缓存。这一级别的缓存由hibernate管理的,一般情况下无需进行干预;第二级别的缓存是SessionFactory级别的缓存,它是属于进程范围或群集范围的缓存。这一级别的缓存可以进行配置和更改,并且可以动态加载和卸载,属于多事务级别,要防止事务并发性。
缓存是以map的形式进行存储的(key-id,value-object)
一级缓存(Session):
事务范围,每个事务(Session)都有单独的第一级缓存.
一级缓存的管理:当应用程序调用Session的save()、update()、saveOrUpdate()、get()或load(),以及调用查 询接口的 list()、iterate()--(用的是n+1次查询,先查id)或filter()方法时,如果在Session缓存中还不存在相应的对 象,Hibernate就会把该对象加入到第一级缓存中。当清理缓存时,Hibernate会根据缓存中对象的状态变化来同步更新数据库。 Session为应用程序提供了两个管理缓存的方法: evict(Object obj):从缓存中清除参数指定的持久化对象。 clear():清空缓存中所有持久化对象,flush():使缓存与数据库同步。
当查询相应的字段如(name),而不是对象时,不支持缓存。
二级缓存(SessionFactory):
Hibernate的二级缓存策略的一般过程如下:
1:条件查询的时候,总是发出一条select * from table_name where …. (选择所有字段)这样的SQL句查询数据库,一次获得所有的数据对象(这个问题要考虑,如果你查询十万条数据时,内存不是被占用)。
2:把获得的所有数据对象根据ID放入到第二级缓存中。
3: 当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;查不到,再查询数据库,把结果按照ID放入到缓存。
4:删除、更新、增加数据的时候,同时更新缓存。
Hibernate的二级缓存策略,是针对于ID查询的缓存策略,对于条件查询则毫无作用。为此,Hibernate提供了针对条件查询的Query Cache。
Q:什么样的数据适合存放到第二级缓存中?
1.很少被修改的数据
2.不是很重要的数据,允许出现偶尔并发的数据
3.不会被并发访问的数据
4.参考数据,指的是供应用参考的常量数据,它的实例数目有限,它的实例会被许多其他类的实例引用,实例极少或者从来不会被修改。
不适合存放到第二级缓存的数据?
1 经常被修改的数据
2 财务数据,绝对不允许出现并发
3 与其他应用共享的数据。
常用的缓存插件 Hibernater 的二级缓存是一个插件,下面是几种常用的缓存插件:
EhCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬盘,对Hibernate的查询缓存提供了支持。
OSCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬盘,提供了丰富的缓存数据过期策略,对Hibernate的查询缓存提供了支持。
SwarmCache:可作为群集范围内的缓存,但不支持Hibernate的查询缓存。
JBossCache:可作为群集范围内的缓存,支持事务型并发访问策略,对Hibernate的查询缓存提供了支持。
配置二级缓存的主要步骤:
1 选择需要使用二级缓存的持久化类,设置它的命名缓存的并发访问策略。这是最值得认真考虑的步骤。
2 选择合适的缓存插件,然后编辑该插件的配置文件。
Hibernate 所有缓存机制详解
hibernate提供的一级缓存
hibernate是一个线程对应一个session,一个线程可以看成一个用户。也就是说session级缓存(一级缓存)只能给一个线程用,别的线程用不了,一级缓存就是和线程绑定了。
hibernate一级缓存生命周期很短,和session生命周期一样,一级缓存也称session级的缓存或事务级缓存。如果tb事务提交或回滚了,我们称session就关闭了,生命周期结束了。
缓存和连接池的区别:缓 存和池都是放在内存里,实现是一样的,都是为了提高性能的。但有细微的差别,池是重量级的,里面的数据是一样的,比如一个池里放100个 Connection连接对象,这个100个都是一样的。缓存里的数据,每个都不一样。比如读取100条数据库记录放到缓存里,这100条记录都不一样。
缓存主要是用于查询
//同一个session中,发出两次load方法查询 Student student = (Student)session.load(Student.class, 1); System.out.println("student.name=" + student.getName()); //不会发出查询语句,load使用缓存 student = (Student)session.load(Student.class, 1); System.out.println("student.name=" + student.getName()); |
第二次查询第一次相同的数据,第二次load方法就是从缓存里取数据,不会发出sql语句到数据库里查询。
//同一个session,发出两次get方法查询 Student student = (Student)session.get(Student.class, 1); System.out.println("student.name=" + student.getName()); //不会发出查询语句,get使用缓存 student = (Student)session.get(Student.class, 1); System.out.println("student.name=" + student.getName()); |
第二次查询第一次相同的数据,第二次不会发出sql语句查询数据库,而是到缓存里取数据。
//同一个session,发出两次iterate查询实体对象 Iterator iter = session.createQuery ("from Student s where s.id<5").iterate(); while (iter.hasNext()) { Student student = (Student)iter.next(); System.out.println(student.getName()); } System.out.println("--------------------------------------"); //它会发出查询id的语句,但不会发出根据id查询学生的语句,因为iterate使用缓存 iter = session.createQuery("from Student s where s.id<5").iterate(); while (iter.hasNext()) { Student student = (Student)iter.next(); System.out.println(student.getName()); } |
一说到iterater查询就要立刻想起:iterater查询在没有缓存的情况下会有N+1的问题。
执行上面代码查看控制台的sql语句,第一次iterate查询会发出N+1条sql语句,第一条sql语句查询所有的id,然后根据id查询实体对象,有N个id就发出N条语句查询实体。
第二次iterate查询,却只发一条sql语句,查询所有的id,然后根据id到缓存里取实体对象,不再发sql语句到数据库里查询了。
//同一个session,发出两次iterate查询,查询普通属性 Iterator iter = session.createQuery( "select s.name from Student s where s.id<5").iterate(); while (iter.hasNext()) { String name = (String)iter.next(); System.out.println(name); } System.out.println("--------------------------------------"); //iterate查询普通属性,一级缓存不会缓存,所以发出查询语句 //一级缓存是缓存实体对象的 iter = session.createQuery ("select s.name from Student s where s.id<5").iterate(); while (iter.hasNext()) { String name = (String)iter.next(); System.out.println(name); } |
执行代码看控制台sql语句,第一次发出N+1条sql语句,第二次还是发出了N+1条sql语句。因为一级缓存只缓存实体对象,tb不会缓存普通属性,所以第二次还是发出sql查询语句。
//两个session,每个session发出一个load方法查询实体对象 try { session = HibernateUtils.getSession(); session.beginTransaction(); Student student = (Student)session.load(Student.class, 1); System.out.println("student.name=" + student.getName()); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } 第二个session调用load方法 try { session = HibernateUtils.getSession(); session.beginTransaction(); Student student = (Student)session.load(Student.class, 1); //会发出查询语句,session间不能共享一级缓存数据 //因为他会伴随着session的消亡而消亡 System.out.println("student.name=" + student.getName()); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } |
第一个session的 load方法会发出sql语句查询实体对象,第二个session的load方法也会发出sql语句查询实体对象。因为session间不能共享一级缓存 的数据,所以第二个session的load方法查询相同的数据还是要到数据库中查询,因为它找不到第一个session里缓存的数据。
//同一个session,先调用save方法再调用load方法查询刚刚save的数据 Student student = new Student(); student.setName("张三"); //save方法返回实体对象的id Serializable id = session.save(student); student = (Student)session.load(Student.class, id); //不会发出查询语句,因为save支持缓存 System.out.println("student.name=" + student.getName()); |
先save保存实体对象,再用load方法查询刚刚save的实体对象,则load方法不会发出sql语句到数据库查询的,而是到缓存里取数据,因为save方法也支持缓存。当然前提是同一个session。
//大批量的数据添加 for (int i=0; i<100; i++) { Student student = new Student(); student.setName("张三" + i); session.save(student); //每20条更新一次 if (i % 20 == 0) { session.flush(); //清除缓存的内容 session.clear(); } } |
大批量数据添加时,会造成内存溢出的,因为save方法支持缓存,每save一个对象就往缓存里放,如果对象足够多内存肯定要溢出。一般的做法是先判断一下save了多少个对象,如果save了20个对象就对缓存手动的清理缓存,这样就不会造成内存溢出。
注意:清理缓存前,要手动调用flush方法同步到数据库,否则save的对象就没有保存到数据库里。
注意:大批量数据的添加还是不要使用hibernate,这是hibernate弱项。可以使用jdbc(速度也不会太快,只是比hibernate好一点),或者使用工具产品来实现,比如oracle的Oracle SQL Loader,导入数据特别快。
Hibernate 二级缓存
二级缓存需要sessionFactory来管理,它是进初级的缓存,所有人都可以使用,它是共享的。
二级缓存比较复杂,一般用第三方产品。hibernate提供了一个简单实现,用Hashtable做的,只能作为我们的测试使用,商用还是需要第三方产品。
使用缓存,肯定是长时间不改变的数据,如果经常变化的数据放到缓存里就没有太大意义了。因为经常变化,还是需要经常到数据库里查询,那就没有必要用缓存了。
hibernate做了一些优化,和一些第三方的缓存产品做了集成。老师采用EHCache缓存产品。
和EHCache二级缓存产品集成:EHCache的jar文件在hibernate的lib里,我们还需要设置一系列的缓存使用策略,需要一个配置文件ehcache.xml来配置。这个文件放在类路径下。
//默认配置,所有的类都遵循这个配置 <defaultCache //缓存里可以放10000个对象 maxElementsInMemory="10000" //过不过期,如果是true就是永远不过期 eternal="false" //一个对象被访问后多长时间还没有访问就失效(120秒还没有再次访问就失效) timeToIdleSeconds="120" //对象存活时间(120秒),如果设置永不过期,这个就没有必要设了 timeToLiveSeconds="120" //溢出的问题,如果设成true,缓存里超过10000个对象就保存到磁盘里 overflowToDisk="true" /> |
我们也可以对某个对象单独配置:
<cache name="com.bjpowernode.hibernate.Student" maxElementsInMemory="100" eternal="false" timeToIdleSeconds="10000" timeToLiveSeconds="10000" overflowToDisk="true" /> |
还需要在hibernate.cfg.xml配置文件配置缓存,让hibernate知道我们使用的是那个二级缓存。
<!-- 配置缓存提供商 --> <property name="hibernate.cache.provider_class"> org.hibernate.cache.EhCacheProvider</property> <!-- 启用二级缓存,这也是它的默认配置 --> <property name="hibernate.cache.use_second_level_cache"> true</property> |
启用二级缓存的配置可以不写的,因为默认就是true开启二级缓存。
必须还手动指定那些实体类的对象放到缓存里在hibernate.cfg.xml里:
//在<sessionfactory>标签里,在<mapping>标签后配置 <class-cache class="com.bjpowernode.hibernate.Student" usage="read-only"/> |
或者在实体类映射文件里:
//在<class>标签里,<id>标签前配置 <cache usage="read-only"/> |
usage属性表示使用缓存的策略,一般优先使用read-only,表示如果这个数据放到缓存里了,则不允许修改,如果修改就会报错。这就要注意我们放入缓存的数据不允许修改。因为放缓存里的数据经常修改,也就没有必要放到缓存里。
使用read-only 策略效率好,因为不能改缓存。但是可能会出现脏数据的问题,这个问题解决方法只能依赖缓存的超时,比如上面我们设置了超时为120秒,120后就可以对缓 存里对象进行修改,而在120秒之内访问这个对象可能会查询脏数据的问题,因为我们修改对象后数据库里改变了,而缓存却不能改变,这样造成数据不同步,也 就是脏数据的问题。
第二种缓存策略read-write,当持久对象发生变化,缓存里就会跟着变化,数据库中也改变了。这种方式需要加解锁,效率要比第一种慢。
还有两种策略,请看hibernate文档,最常用还是第一二种策略。
二级缓存测试代码演示:注意上面我们讲的两个session分别调用load方法查询相同的数据,第二个session的load方法还是发了sql语句到数据库查询数据,这是因为一级缓存只在当前session中共享,也就是说一级缓存不能跨session访问。
//开启二级缓存,二级缓存是进程级的缓存,可以共享 //两个session分别调用load方法查询相同的实体对象 try { session = HibernateUtils.getSession(); session.beginTransaction(); Student student = (Student)session.load(Student.class, 1); System.out.println("student.name=" + student.getName()); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } try { session = HibernateUtils.getSession(); session.beginTransaction(); Student student = (Student)session.load(Student.class, 1); //不会发出查询语句,因为配置二级缓存,session可以共享二级缓存中的数据 //二级缓存是进程级的缓存 System.out.println("student.name=" + student.getName()); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } |
如果开启了二级缓存,那么第二个session调用的load方法查询第一次查询的数据,是不会发出sql语句查询数据库的,而是去二级缓存中取数据。
//开启二级缓存 //两个session分别调用get方法查询相同的实体对象 try { session = HibernateUtils.getSession(); session.beginTransaction(); Student student = (Student)session.get(Student.class, 1); System.out.println("student.name=" + student.getName()); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } try { session = HibernateUtils.getSession(); session.beginTransaction(); Student student = (Student)session.get(Student.class, 1); //不会发出查询语句,因为配置二级缓存,session可以共享二级缓存中的数据 //二级缓存是进程级的缓存 System.out.println("student.name=" + student.getName()); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } |
注意:二级缓存必须让sessionfactory管理,让sessionfactory来清除二级缓存。sessionFactory.evict(Student.class);//清除二级缓存中所有student对象,sessionFactory.evict(Student.class,1);//清除二级缓存中id为1的student对象。
如果在第一个session调用load或get方法查询数据后,把二级缓存清除了,那么第二个session调用load或get方法查询相同的数据时,还是会发出sql语句查询数据库的,因为缓存里没有数据只能到数据库里查询。
我们查询数据后会默认自动的放到二级和一级缓存里,如果我们想查询的数据不放到缓存里,也是可以的。也就是说我们可以控制一级缓存和二级缓存的交换。
session.setCacheMode(CacheMode.IGNORE);禁止将一级缓存中的数据往二级缓存里放。
还是用上面代码测试,在 第一个session调用load方法前,执行session.setCacheMode(CacheMode.IGNORE);这样load方法查询的 数据不会放到二级缓存里。那么第二个session执行load方法查询相同的数据,会发出sql语句到数据库中查询,因为二级缓存里没有数据,一级缓存 因为不同的session不能共享,所以只能到数据库里查询。
上面我们讲过大批量的数 据添加时可能会出现溢出,解决办法是每当天就20个对象后就清理一次一级缓存。如果我们使用了二级缓存,光清理一级缓存是不够的,还要禁止一二级缓存交 互,在save方法前调用session.setCacheMode(CacheMode.IGNORE)。
二级缓存也不会存放普通属性的查询数据,这和一级缓存是一样的,只存放实体对象。session级的缓存对性能的提高没有太大的意义,因为生命周期太短了。
Hibernate 查询缓存
一级缓存和二级缓存都只是存放实体对象的,如果查询实体对象的普通属性的数据,只能放到查询缓存里,查询缓存还存放查询实体对象的id。
查询缓存的生命周期不确定,当它关联的表发生修改,查询缓存的生命周期就结束。这里表的修改指的是通过hibernate修改,并不是通过数据库客户端软件登陆到数据库上修改。
hibernate的查询缓存默认是关闭的,如果要使用就要到hibernate.cfg.xml文件里配置:
<property name="hibernate.cache.use_query_cache">true</property> |
并且必须在程序中手动启用查询缓存,在query接口中的setCacheable(true)方法来启用。
//关闭二级缓存,没有开启查询缓存,采用list方法查询普通属性 //同一个sessin,查询两次 List names = session.createQuery("select s.name from Student s") .list(); for (int i=0; i<names.size(); i++) { String name = (String)names.get(i); System.out.println(name); } System.out.println("-----------------------------------------"); //会发出sql语句 names = session.createQuery("select s.name from Student s") .setCacheable(true) .list(); for (int i=0; i<names.size(); i++) { String name = (String)names.get(i); System.out.println(name); } |
上面代码运行,由于没有使用查询缓存,而一、二级缓存不会缓存普通属性,所以第二次查询还是会发出sql语句到数据库中查询。
现在开启查询缓存,关闭二级缓存,并且在第一次的list方法前调用setCacheable(true),并且第二次list查询前也调用这句代码,可以写出下面这样:
List names = session.createQuery("select s.name from Student s") .setCacheable(true) .list(); |
其它代码不变,运行代码后发现第二次list查询普通属性没有发出sql语句,也就是说没有到数据库中查询,而是到查询缓存中取数据。
//开启查询缓存,关闭二级缓存,采用list方法查询普通属性 //在两个session中调用list方法 try { session = HibernateUtils.getSession(); session.beginTransaction(); List names = session.createQuery("select s.name from Student s") .setCacheable(true) .list(); for (int i=0; i<names.size(); i++) { String name = (String)names.get(i); System.out.println(name); } session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } System.out.println("----------------------------------------"); try { session = HibernateUtils.getSession(); session.beginTransaction(); //不会发出查询语句,因为查询缓存和session的生命周期没有关系 List names = session.createQuery("select s.name from Student s") .setCacheable(true) .list(); for (int i=0; i<names.size(); i++) { String name = (String)names.get(i); System.out.println(name); } session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } |
运行结果是第二个session发出的list方法查询普通属性,没有发出sql语句到数据库中查询,而是到查询缓存里取数据,这说明查询缓存和session生命周期没有关系。
//开启缓存,关闭二级缓存,采用iterate方法查询普通属性 //在两个session中调用iterate方法查询 |
运行结果是第二个session的iterate方法还是发出了sql语句查询数据库,这说明iterate迭代查询普通属性不支持查询缓存。
//关闭查询缓存,关闭二级缓存,采用list方法查询实体对象 //在两个session中调用list方法查询 |
运行结果第一个session调用list方法查询实体对象会发出sql语句查询数据,因为关闭了二级缓存,所以第二个session调用list方法查询实体对象,还是会发出sql语句到数据库中查询。
//开启查询缓存,关闭二级缓存 //在两个session中调用list方法查询实体对象 |
运行结果第一个 session调用list方法查询实体对象会发出sql语句查询数据库的。第二个session调用list方法查询实体对象,却发出了很多sql语句 查询数据库,这跟N+1的问题是一样的,发出了N+1条sql语句。为什么会出现这样的情况呢?这是因为我们现在查询的是实体对象,查询缓存会把第一次查询的实体对象的id放到缓存里,当第二个session再次调用list方法时,它会到查询缓存里把id一个一个的拿出来,然后到相应的缓存里找(先找一级缓存找不到再找二级缓存),如果找到了就返回,如果还是没有找到,则会根据一个一个的id到数据库中查询,所以一个id就会有一条sql语句。
注意:如果配置了二级缓存,则第一次查询实体对象后,会往一级缓存和二级缓存里都存放。如果没有二级缓存,则只在一级缓存里存放。(一级缓存不能跨session共享)
//开启查询缓存,开启二级缓存 //在两个session中调用list方法查询实体对象 |
运行结果是第一个 session调用list方法会发出sql语句到数据库里查询实体对象,因为配置了二级缓存,则实体对象会放到二级缓存里,因为配置了查询缓存,则实体 对象所有的id放到了查询缓存里。第二个session调用list方法不会发出sql语句,而是到二级缓存里取数据。
查询缓存意义不大,查询 缓存说白了就是存放由list方法或iterate方法查询的数据。我们在查询时很少出现完全相同条件的查询,这也就是命中率低,这样缓存里的数据总是变 化的,所以说意义不大。除非是多次查询都是查询相同条件的数据,也就是说返回的结果总是一样,这样配置查询缓存才有意义。
posted on 2012-06-05 12:44 chen11-1 阅读(14054) 评论(9) 编辑 收藏 所属分类: java
Feedback
# re: Hibernate 所有缓存机制详解 2013-11-01 13:56 叮叮当当
# re: Hibernate 所有缓存机制详解 2013-12-12 18:34 hubery
不错,写得很详细,很好,是很好的学习榜样!!! 回复 更多评论
# re: Hibernate 所有缓存机制详解 2014-01-14 16:30 我来了
# re: Hibernate 所有缓存机制详解 2014-02-14 10:05 很快解决
# re: Hibernate 所有缓存机制详解 2014-02-28 09:49 hiber
# re: Hibernate 所有缓存机制详解 2014-03-05 13:21 rlf
# re: Hibernate 所有缓存机制详解 2014-04-03 18:47 无名氏
# re: Hibernate 所有缓存机制详解 2014-04-07 20:50 wanjinxiong
是不是写错了,上面所说的一级缓存一个说第二次查询是到缓存中取数据!下面怎么又说第二次查询还是要到数据库中取。因为session间不能共享一级缓存 的数据,所以第二个session的load方法查询相同的数据还是要到数据库中查询,因为它找不到第一个session里缓存的数据