hibernate缓存详解

hibernate 提供的一级缓存

hibernate是一个线程对应一个session,一个线程能够当作一个用户。也就是说session级缓存(一级缓存)只能给一个线程用,别的线程用不了,一级缓存就是和线程绑定了。 node

hibernate一级缓存生命周期很短,和session生命周期同样,一级缓存也称session级的缓存或事务级缓存。若是tb事务提交或回滚了,咱们称session就关闭了,生命周期结束了。 sql

缓存和链接池的区别缓存和池都是放在内存里,实现是同样的,都是为了提升性能的。但有细微的差异,池是重量级的,里面的数据是同样的,好比一个池里放100个Connection链接对象,这个100个都是同样的。缓存里的数据,每一个都不同。好比读取100条数据库记录放到缓存里,这100条记录都不同。 数据库

缓存主要是用于查询 缓存

//同一个session中,发出两次load方法查询 session

Student student = (Student)session.load(Student.class, 1); oracle

System.out.println("student.name=" + student.getName()); app

//不会发出查询语句,load使用缓存 ide

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);

}

上面代码运行,因为没有使用查询缓存,而1、二级缓存不会缓存普通属性,因此第二次查询仍是会发出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方法查询的数据。咱们在查询时不多出现彻底相同条件的查询,这也就是命中率低,这样缓存里的数据老是变化的,因此说意义不大。除非是屡次查询都是查询相同条件的数据,也就是说返回的结果老是同样,这样配置查询缓存才有意义。

相关文章
相关标签/搜索