hibernate缓存详解

为什么要用hibernate缓存?

hibernate是一个持久层框架,经常访问物理数据库。为了降低应用程序对物理数据源访问的次数,从而提高应用程序的运行性能,我们想到使用hibernate缓存机制。缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或事件会同步缓存和物理数据源的数据。

hibernate缓存的原理

缓存的主要作用是查询

hibernate缓存包括三大类:hibernate一级缓存、hibernate二级缓存和hibernate查询缓存

一级缓存

一级缓存是hibernate自带的,不受用户干预。

hibernate是一个线程对应一个session,一个线程可以看成一个用户。也就是说session级缓存只能给一个线程用,别的线程用不了,一级缓存就是和线程绑定的。其生命周期和session的生命周期一致,当前session一旦关闭,一级缓存就会消失,因此,一级缓存也叫session缓存或者事务级缓存,一级缓存只存储实体对象,不会缓存一般的对象属性,即:当获得对象后,就将该对象缓存起来,如果在同一个session中再去获取这个对象时,它会先判断缓存中有没有这个对象的ID,如果有,就直接从缓存中取出,否则,则去访问数据库,取了以后同时会将这个对象缓存起来。

Session内置不能被卸载,Session的缓存是事务范围的缓存(Session对象的生命周期通常对应一个数据库事务或者一个应用事务)。一级缓存中,持久化类的每个实例都具有唯一的OID。

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

以下我们结合具体示例来学习一级缓存:

import java.io.Serializable;
import java.util.Iterator;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class CacheDemo {

    private static SessionFactory sessionFactory;
    static{
        sessionFactory = new Configuration().configure().buildSessionFactory();
    }

    /**
     * 同一个Session发出两次load方法查询
     * 
     * 第二次查询的数据和第一次相同,第二次load方法是从缓存里取数据,而不会再发出sql语句到数据库中进行查询。
     */
    public void cacheTest1(){

        Session session = sessionFactory.openSession();
        session.beginTransaction();
        //第一次load查询
        Student student = session.load(Student.class, 1);
        System.out.println("student.name : " + student.getName());
        System.out.println("*************************************");
        //第二次load查询(使用缓存)
        student = session.load(Student.class, 1);
        System.out.println("student.name : " + student.getName());

    }


    /**
     * 同一个Session发出两次get方法查询
     * 
     * 第二次查询的数据和第一次相同,第二次get方法是从缓存里取数据,而不会再发出sql语句到数据库中进行查询。
     */
    public void cacheTest2(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        //第一次get查询
        Student student = session.get(Student.class, 1);
        System.out.println("student.name : " + student.getName());
        System.out.println("*************************************");
        //第二次get查询(使用缓存)
        student = session.get(Student.class, 1);
        System.out.println("student.name : " + student.getName());
    }


    /**
     * 同一个Session发出两次iterator查询对象
     * 
     * 说起iterator查询,我们会想到,iterator查询在没有缓存的情况下会有N+1的问题。
     * 执行上面的代码,我们可以看到,第一次iterator查询会发出N+1条语句,第一条sql语句查询所有的ID,
     * 然后根据ID查询实体对象,有N个ID就出N条语句查询实体。第二次iterator查询,却只发一条sql语句,
     * 查询所有的ID,然后根据ID到缓存里取实体对象,不再发sql语句到数据库里查询了。
     */
    public void cacheTest3(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        //第一次iterator查询
        Iterator iter = session.createQuery("from student s where s.id < 5").iterate();
        while (iter.hasNext()) {
            Student student = (Student)iter.next();
            System.out.println("student.name : " + student.getName());          
        }
        System.out.println("*************************************");
        //第二次iterator查询(使用缓存)
        iter = session.createQuery("from student s where s.id < 5").iterate();
        while (iter.hasNext()) {
            Student student = (Student)iter.next();
            System.out.println("student.name : " + student.getName());          
        }
    }


    /**
     * 同一个Session发出两次iterator查询对象的普通属性
     * 
     * 执行上面的代码,我们可以看到,第一次iterator查询会发出N+1条语句,第二次iterator查询还是发了N+1条sql语句,
     * 因为一级缓存只是缓存实体对象,而不缓存对象属性,所以在iterate第二次查询普通属性时,无法从缓存中获取,
     * 从而跟第一次查询发出相同条数的sql语句。
     */
    public void cacheTest4(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        //第一次iterator查询
        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("student.name : " + name);           
        }
        System.out.println("*************************************");
        //第二次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("student.name : " + name);           
        }

    }


    /**
     * 两个session,每个session发出一个load方法查询实体对象
     * 
     * 第一个session的load方法会发出sql语句查询实体对象,第二个session的load方法也会发出sql语句查询实体对象。
     * session间不能共享一级缓存的数据,第一个session中的数据在其被关闭的时候就已经不存在了,
     * 所以第二个session的load方法查询相同的数据还是要到数据库中查询。
     */
    public void cacheTest5(){
        Session session = null;
        //第一个session的load查询
        try {
            session = sessionFactory.openSession();
            session.beginTransaction();
            Student student = session.load(Student.class, 1);
            System.out.println("student.name : " + student.getName());
            session.getTransaction().commit();
        } catch (Exception e) {
            session.getTransaction().rollback();
        }finally{
            session.close();//关闭session
        }
        System.out.println("***********************************");
        //第二个session的load查询
        try {
            session = sessionFactory.openSession();
            session.beginTransaction();
            Student student = session.load(Student.class, 1);
            System.out.println("student.class : " + student.getName());
            session.getTransaction().commit();
        } catch (Exception e) {
            session.getTransaction().rollback();
        }finally{
            session.close();
        }

    }


    /**
     * 同一个session,先调用save方法再调用load方法查询刚刚save的数据
     * 
     * 先save保存实体对象,再用load方法查询刚刚save的实体对象,则load方法不会发出sql语句到数据库查询的,
     * 而是到缓存里取数据,因为save方法也支持缓存。当然前提是同一个session。
     */
    public void cacheTest6(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        Student student = new Student().setName("Tom");
        //save方法返回实体对象的ID
        Serializable id = session.save(student);
        //load查询刚刚save的数据
        student = session.load(Student.class, 1);
        System.out.println("student.name : " + student.getName());
    }


    /**
     * 大批量的数据添加
     * 
     * 大批量数据添加时,会造成内存溢出的,因为save方法支持缓存,每save一个对象就往缓存里放,如果对象足够多内存肯定要溢出。
     * 一般的做法是先判断一下save了多少个对象,如果save了40个对象就对缓存手动的清理缓存,这样就不会造成内存溢出。
     * 
     * 注意:清理缓存前,要手动调用flush方法同步到数据库,否则save的对象就没有保存到数据库里。
     * 
     * 建议:大批量数据的添加还是不要使用hibernate,这是hibernate弱项。
     *      可以使用jdbc(速度也不会太快,只是比hibernate好一点),或者使用工具产品来实现,
     *      比如oracle的Oracle SQL Loader,导入数据特别快。
     */
    public void cacheTest7(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        for (int i = 0; i < 100; i++) {
            Student student = new Student().setName("Tom"+i);
            session.save(student);
            //每40条数据更新一次
            if (i % 40 == 0) {
                session.flush();
                session.clear();//清除缓存数据
            }
        }
    }

}

二级缓存

二级缓存也称为进程缓存或者sessionFactory级的缓存,它可以被所有的session共享,二级缓存的生命周期和sessionFactory的生命周期一致,二级缓存也是只存储实体对象。

二级缓存的一般过程如下

①:条件查询的时候,获取查询到的实体对象
②:把获得到的所有数据对象根据ID放到二级缓存中
③:当Hibernate根据ID访问数据对象时,首先从sesison的一级缓存中查,查不到的时候如果配置了二级缓存,会从二级缓存中查找,如果还查不到,再查询数据库,把结果按照ID放入到缓存中
④:进行delete、update、add操作时会同时更新缓存

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

二级缓存是可选的,是一个可配置的插件,默认下SessionFactory不会启用这个插件。Hibernate提供了org.hibernate.cache.CacheProvider接口,它充当缓存插件与Hibernate之间的适配器。 二级缓存比较复杂,hibernate做了一些优化,和一些第三方的缓存产品做了集成。。hibernate提供了一个简单实现,用Hashtable做的,只能作为我们的测试使用,商用还是需要第三方产品。

使用缓存,肯定是长时间不改变的数据,如果经常变化的数据放到缓存里就没有太大意义了。因为经常变化,还是需要经常到数据库里查询,那就没有必要用缓存了。

什么样的数据适合存放到第二级缓存中?   
1) 很少被修改的数据   
2) 不是很重要的数据,允许出现偶尔并发的数据   
3) 不会被并发访问的数据   
4) 常量数据   

什么样的数据不适合存放到第二级缓存中?   
1) 经常被修改的数据   
2) 绝对不允许出现并发访问的数据,如财务数据,绝对不允许出现并发
3) 与其他应用共享的数据。

我们以一个和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"
/>
<!-- 配置缓存提供商 -->
<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,当持久对象发生变化,缓存里就会跟着变化,数据库中也改变了。这种方式需要加解锁,效率要比第一种慢。

下面我们结合具体示例来学习二级缓存:


      /**
       * 开启二级缓存,两个session,每个session发出一个load/get方法查询实体对象
       *
       * 第二次查询的数据和第一次相同,不会发出查询语句,因为配置二级缓存,session可以共享二级缓存中的数据。
       * 所以第二次load方法是从缓存里取数据,而不会再发出sql语句到数据库中进行查询。
       */
      public void cacheTest1(){

            Session session = null;
            //第一次load查询
            try {
                  session = sessionFactory.openSession();
                  session.beginTransaction();
                  Student student = session.load(Student.class, 1);
                  System.out.println("student.name : " + student.getName());
                  session.getTransaction().commit();
            } catch (Exception e) {
                  session.getTransaction().rollback();
            }finally{
                  session.close();//关闭session
            }
            System.out.println("***********************************");
            //第二次load查询
            try {
                  session = sessionFactory.openSession();
                  session.beginTransaction();
                  Student student = session.load(Student.class, 1);
                  System.out.println("student.class : " + student.getName());
                  session.getTransaction().commit();
            } catch (Exception e) {
                  session.getTransaction().rollback();
            }finally{
                  session.close();
            }

      }

}

注意:二级缓存必须让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不能共享,所以只能到数据库里查询。

上面我们讲过大批量的数据添加时可能会出现溢出,解决办法是每当天就40个对象后就清理一次一级缓存。如果我们使用了二级缓存,光清理一级缓存是不够的,还要禁止一二级缓存交互,在save方法前调用

session.setCacheMode(CacheMode.IGNORE)

二级缓存也不会存放普通属性的查询数据,这和一级缓存是一样的,只存放实体对象。session级的缓存对性能的提高没有太大的意义,因为生命周期太短了。

查询缓存

查询缓存意义不大,查询缓存说白了就是存放由list方法或iterate方法查询的数据。我们在查询时很少出现完全相同条件的查询,这也就是命中率低,这样缓存里的数据总是变化的,所以说意义不大。除非是多次查询都是查询相同条件的数据,也就是说返回的结果总是一样,这样配置查询缓存才有意义。我们这里不做过多解释。

Hibernate查找对象如何应用缓存?

当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;如果都查不到,再查询数据库,把结果按照ID放入到缓存删除、更新、增加数据的时候,同时更新缓存。

最后贴出一个一级缓存和二级缓存的对比图:

这里写图片描述
这里写图片描述

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。

MAT使用的几张图例 - 2016-07-22 18:07:51

下面三个是内存泄漏可能性比较大的地方 problem suspect 1 problem suspect 2 problem suspect 3 点击detail 可以看详细 在dominator_tree 可以对象按照group by package 分类 便于查看那部分代码出问题 选中一个节点 右键查看with incoming reference 可以看  ps :( ListObjects with incoming references  表示的是 当前查看的对象,被外部应用 ListObjec

Hibernate的核心接口和类 - 2016-07-22 18:07:25

Hibernate的核心类和接口一共有6个,分别为: Session、SessionFactory、 Transaction、Query、Criteria和Configuration 。这6个核心和类接口在任何开发中都会用到。通过这些接口,不仅可以对持久化对象进行存取,还能够进行事务控制。下面对这6个核心接口和类分别加以介绍。 Configuration Configuration类的作用是对Hibernate进行配置,以及对它进行启动。在Hibernate的启动过程中,Configuration 类的实
1、概述 在我开始构思这几篇关于“自己动手设计ESB中间件”的文章时,曾有好几次动过放弃的念头。原因倒不是因为对冗长的文章产生了惰性,而是ESB中所涉及到的技术知识和需要突破的设计难点实在是比较多,再冗长的几篇博文甚至无法对它们全部进行概述,另外如果在思路上稍微有一点差池就会误导读者。 一个可以稳定使用的ESB中间件凝聚了一个团队很多参与者的心血,一个人肯定是无法完成这些工作的 。但是笔者思索再三,还是下决心将这这即便文章完成,因为这是对本专题从第19篇文章到第39篇文章中所介绍的知识点的最好的总结。我们
1、Exactly_once简介 Exactly_once语义是Flink的特性之一,那么Flink到底提供了什么层次的Excactly_once?有人说是是每个算子保证只处理一次,有人说是每条数据保证只处理一次。其实理解这个语义并不难,直接在官方文档中就可以看出: 从图中可以看出:Exactly_once是为有状态的计算准备的! 换句话说,没有状态的算子操作(operator),Flink无法也无需保证其只被处理Exactly_once!为什么无需呢?因为即使失败的情况下,无状态的operator(ma
简单工厂模式是指专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 从图中我们可以清楚的看到,该模式中主要包含下面3种 角色: 工厂(Creator)角色 它是工厂模式的核心,负责实现创建所有势力的内部逻辑。工厂类可以被外界直接调用,创建所需的产品的对象。 抽象(Product)角色 简单工厂模式所创建的所有对象的父类,负责描述所有实例所共有的公共接口。 具体产品(Concrete Product)角色 是该模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。一般来讲是

Mybatis学习第一天 - 2016-07-22 17:07:07

  Mybatis第一天 2      MyBatis介绍 MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。 MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集

IP数据报的分片和组装过程 - 2016-07-22 17:07:07

        一份数据从一个主机通过以太网发送到里一个主机时,是要经过很多层路由转发的。其中过程相对比较的复杂,在这里我们要讨论的是IP在路由中转发时是以怎样的形式转发的和目的主机在接受到这写数据报时又是怎样处理的。        首先我们需要了解的是整个 IP数据报的格式 : IP的转发控制都是由IP数据报的头部决定的。在这里我们就不详细的讨论首部的所有字段,我们就讨论一下个分片有关的总长度字段。        在IP数据报中,总长度是16位的字段,依次数据报的最大长度为2^16-1=65535字节,

Java的纤程库 - Quasar - 2016-07-22 17:07:06

最近遇到的一个问题大概是微服务架构中经常会遇到的一个问题: 服务  A  是我们开发的系统,它的业务需要调用  B  、  C  、  D  等多个服务,这些服务是通过http的访问提供的。 问题是  B  、  C  、  D  这些服务都是第三方提供的,不能保证它们的响应时间,快的话十几毫秒,慢的话甚至1秒多,所以这些服务的Latency比较长。幸运地是这些服务都是集群部署的,容错率和并发支持都比较高,所以不担心它们的并发性能,唯一不爽的就是就是它们的Latency太高了。 系统A会从Client接收

操作系统知识点整理 - 2016-07-22 17:07:03

作业 用户在一次解题或一个事务处理过程中要求计算机系统所做工作的集合。它包括用户程序、所需要的数据及控制命令等。作业是由一系列有序的步骤组成的。 进程 一个程序在一个数据集合上的一次运行过程。所以一个程序在不同数据集合上运行,乃至一个程序在同样数据集合的多次运行都是不同的进程。 线程 线程是进程中的一个实体,是被系统独立调度和执行的基本单位。 进程和线程的区别 进程是程序的一次执行。线程可以理解为进程中执行的一段程序片段。 进程是独立的,这表现在内存空间、上下文环境上;线程运行在进程空间内。一般来讲(不适

Spring MVC Web简单入门实例 - 2016-07-22 17:07:52

本文通过一个简单的用户登录例子带你入门Spring MVC Web开发。 开发环境 1、STS 3.7.3(Spring Tool Suit), 下载 。STS其实是一个包装过的Eclipse,由Spring小组开发的,专门用于Spring项目的开发。老规矩,安装之前先要安装jdk,并配置好环境变量。 2、Tomcat 7, 下载Tomcat 7 。sts已经集成了一个叫Pivotal tc Server的web服务器,不过我们一般都使用Tomcat作为我们的Web服务器。 Tomcat配置 。 创建项目