Hibernate - 整合Ehcache二级缓存使用详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/J080624/article/details/83242225

SessionFactory与Session详解博文中讲述了基于Session的一级缓存,本篇博文讲述基于SessionFactory的二级缓存。

缓存(Cache)

计算机领域非常通用的概念。它介于应用程序和永久性数据存储源(如硬盘上的文件或者数据库)之间,其作用是降低应用程序直接读写永久性数据存储源的频率,从而提高应用的运行性能。缓存中的数据是数据存储源中数据的拷贝。缓存的物理介质通常是内存。

Hibernate中提供了两个级别的缓存

  • 第一级别的缓存是 Session 级别的缓存,它是属于事务范围的缓存。这一级别的缓存由 hibernate 管理的。
  • 第二级别的缓存是 SessionFactory 级别的缓存,它是属于进程范围的缓存。

一级缓存测试如下:

	@Test
	public void testHibernateSecondLevelCache(){
		Employee employee = (Employee) session.get(Employee.class, 1);
		System.out.println(employee.getName()); 
	
		Employee employee2 = (Employee) session.get(Employee.class, 1);
		System.out.println(employee2.getName()); 
	}

测试结果如下:

Hibernate: 
    select
        employee0_.ID as ID1_1_0_,
        employee0_.NAME as NAME2_1_0_,
        employee0_.SALARY as SALARY3_1_0_,
        employee0_.EMAIL as EMAIL4_1_0_,
        employee0_.DEPT_ID as DEPT_ID5_1_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.ID=?
AA
AA

可以看到只发送了一次查询SQL,第一次查询后将结果放到了session的缓存中,供第二次session.get()使用。


【1】SessionFactory级别的缓存

① SessionFactory 的缓存可以分为两类:

  • 内置缓存: Hibernate 自带的, 不可卸载。 通常在 Hibernate 的初始化阶段, Hibernate 会把映射元数据和预定义的 SQL 语句放到 SessionFactory 的缓存中, 映射元数据是映射文件中数据(hibernate.cfg.xml和*.hbm.xml 文件中的数据)的复制。 该内置缓存是只读的。
  • 外置缓存(二级缓存): 一个可配置的缓存插件。 在默认情况下, SessionFactory 不会启用这个缓存插件。外置缓存中的数据是数据库数据的复制, 外置缓存的物理介质可以是内存或硬盘。

适合放入二级缓存中的数据:

  • 很少被修改
  • 不是很重要的数据, 允许出现偶尔的并发问题

不适合放入二级缓存中的数据:

  • 经常被修改
  • 财务数据, 绝对不允许出现并发问题
  • 与其他应用程序共享的数据

② Hibernate二级缓存架构

在这里插入图片描述


③ 二级缓存的并发访问策略

两个并发的事务同时访问持久层的缓存的相同数据时, 也有可能出现各类并发问题。

二级缓存可以设定以下 4 种类型的并发访问策略, 每一种访问策略对应一种事务隔离级别。

  • 非严格读写(Nonstrict-read-write): 不保证缓存与数据库中数据的一致性。提供 Read Uncommited 事务隔离级别, 对于极少被修改, 而且允许脏读的数据, 可以采用这种策略。
  • 读写型(Read-write): 提供 Read Commited 数据隔离级别.对于经常读但是很少被修改的数据, 可以采用这种隔离类型, 因为它可以防止脏读。
  • 事务型(Transactional): 仅在受管理环境下适用. 它提供了 Repeatable Read 事务隔离级别。对于经常读但是很少被修改的数据, 可以采用这种隔离类型, 因为它可以防止脏读和不可重复读。
  • 只读型(Read-Only):提供 Serializable 数据隔离级别, 对于从来不会被修改的数据, 可以采用这种访问策略。

参考博文:一文读懂Spring事务和MySQL事务


④ 二级缓存的提供者

Hibernate 的二级缓存是进程或集群范围内的缓存。

二级缓存是可配置的的插件, Hibernate 允许选用以下类型的缓存插件:

  • EHCache: 可作为进程范围内的缓存, 存放数据的物理介质可以使内存或硬盘, 对 Hibernate 的查询缓存提供了支持。
  • OpenSymphony OSCache:可作为进程范围内的缓存, 存放数据的物理介质可以使内存或硬盘, 提供了丰富的缓存数据过期策略, 对 Hibernate 的查询缓存提供了支持。
  • SwarmCache: 可作为集群范围内的缓存, 但不支持 Hibernate 的查询缓存。
  • JBossCache:可作为集群范围内的缓存, 支持 Hibernate 的查询缓存。

4 种缓存插件支持的并发访问策略(√ 代表支持, 空白代表不支持)

Cache provider Read-Only Nonstrict-read-write Read-write Transactional
EHCache
OSCache
SwarmCache
JBossCache

【2】二级缓存配置与使用

① 配置进程范围内的二级缓存的步骤:

  • 选择合适的缓存插件: EHCache(jar 包和 配置文件), 并编辑配置文件;
	<!-- hibernate+ehcache -->
		<dependency>
		    <groupId>org.hibernate</groupId>
		    <artifactId>hibernate-ehcache</artifactId>
		     <version>${hibernate.version}</version>
		</dependency>
		
		<!-- EHcache -->
		<dependency>
		    <groupId>net.sf.ehcache</groupId>
		    <artifactId>ehcache</artifactId>
		    <version>${ehcache.version}</version>
		</dependency>
  • 在 Hibernate 的配置文件(hibernate.cfg.xml)中启用二级缓存并指定和 EHCache 对应的缓存适配器;
<!-- 启用二级缓存 -->
<property name="cache.use_second_level_cache">true</property>
 	
<!-- 配置使用的二级缓存的产品 -->
<property name="hibernate.cache.region.factory_class">
	org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>
  • 选择需要使用二级缓存的持久化类, 设置它的二级缓存的并发访问策略
    • <class>元素的 cache 子元素表明 Hibernate 会缓存对象的简单属性, 但不会缓存集合属性, 若希望缓存集合属性中的元素, 必须在<set> 元素中加入 <cache>子元素;
    • 在 hibernate 配置文件(hibernate.cfg.xml)中通过 <class-cache/><collection-cache节点配置使用缓存。

Department.hbm.xml如下:

<hibernate-mapping package="com.jane.model">

    <class name="Department" table="GG_DEPARTMENT">
    	<cache usage="read-write"/>
        
        <id name="id" type="java.lang.Integer">
            <column name="ID" />
            <generator class="native" />
        </id>
    
        <property name="name" type="java.lang.String">
            <column name="NAME" />
        </property>
        
        <set name="emps" table="GG_EMPLOYEE" inverse="true" lazy="true">
            <key>
                <column name="DEPT_ID" />
            </key>
            <one-to-many class="Employee" />
<!--             <cache usage="read-write"/> -->
        </set>
        
    </class>
</hibernate-mapping>

hibernate.cfg.xml配置如下:

<class-cache usage="read-write" class="com.jane.model.Employee"/>
<class-cache usage="read-write" class="com.jane.model.Department"/>
<collection-cache usage="read-write" collection="com.jane.model.Department.emps"/>

② 测试简单属性代码如下:

	@Test
	public void testHibernateSecondLevelCache(){
		Employee employee = (Employee) session.get(Employee.class, 1);
		System.out.println(employee.getName()); 
		
		transaction.commit();
		session.close();
		
		session = sessionFactory.openSession();
		transaction = session.beginTransaction();
		
		Employee employee2 = (Employee) session.get(Employee.class, 1);
		System.out.println(employee2.getName()); 
	}

测试结果如下:

Hibernate: 
    select
        employee0_.ID as ID1_1_0_,
        employee0_.NAME as NAME2_1_0_,
        employee0_.SALARY as SALARY3_1_0_,
        employee0_.EMAIL as EMAIL4_1_0_,
        employee0_.DEPT_ID as DEPT_ID5_1_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.ID=?
AA
AA

③ 测试集合属性

默认情况下,集合属性不会被缓存,如下所示:

	@Test
	public void testCollectionSecondLevelCache(){
		Department dept = (Department) session.get(Department.class, 1);
		System.out.println(dept.getName());
		System.out.println(dept.getEmps().size()); 
		
		transaction.commit();
		session.close();
		
		session = sessionFactory.openSession();
		transaction = session.beginTransaction();
		
		Department dept2 = (Department) session.get(Department.class, 1);
		System.out.println(dept2.getName());
		System.out.println(dept2.getEmps().size()); 
	}

结果如下:

Hibernate: 
    select
        department0_.ID as ID1_0_0_,
        department0_.NAME as NAME2_0_0_ 
    from
        GG_DEPARTMENT department0_ 
    where
        department0_.ID=?
AA
Hibernate: 
    select
        emps0_.DEPT_ID as DEPT_ID5_1_0_,
        emps0_.ID as ID1_1_0_,
        emps0_.ID as ID1_1_1_,
        emps0_.NAME as NAME2_1_1_,
        emps0_.SALARY as SALARY3_1_1_,
        emps0_.EMAIL as EMAIL4_1_1_,
        emps0_.DEPT_ID as DEPT_ID5_1_1_ 
    from
        GG_EMPLOYEE emps0_ 
    where
        emps0_.DEPT_ID=?
2
AA
Hibernate: 
    select
        emps0_.DEPT_ID as DEPT_ID5_1_0_,
        emps0_.ID as ID1_1_0_,
        emps0_.ID as ID1_1_1_,
        emps0_.NAME as NAME2_1_1_,
        emps0_.SALARY as SALARY3_1_1_,
        emps0_.EMAIL as EMAIL4_1_1_,
        emps0_.DEPT_ID as DEPT_ID5_1_1_ 
    from
        GG_EMPLOYEE emps0_ 
    where
        emps0_.DEPT_ID=?
2

每次需要使用对象的集合属性时,都需要发送SQL语句进行查询。

如果需要缓存集合属性,则有两个解决办法:

  • <set> 元素中加入 <cache>子元素;
 <set name="emps" table="GG_EMPLOYEE" inverse="true" lazy="true">
   <key>
        <column name="DEPT_ID" />
    </key>
    <one-to-many class="Employee" />
     <cache usage="read-write"/> 
</set>
  • 在hibernate.cfg.xml中添加元素。
<collection-cache usage="read-write" collection="com.jane.model.Department.emps"/>

再次测试结果如下:

Hibernate: 
    select
        department0_.ID as ID1_0_0_,
        department0_.NAME as NAME2_0_0_ 
    from
        GG_DEPARTMENT department0_ 
    where
        department0_.ID=?
AA
Hibernate: 
    select
        emps0_.DEPT_ID as DEPT_ID5_1_0_,
        emps0_.ID as ID1_1_0_,
        emps0_.ID as ID1_1_1_,
        emps0_.NAME as NAME2_1_1_,
        emps0_.SALARY as SALARY3_1_1_,
        emps0_.EMAIL as EMAIL4_1_1_,
        emps0_.DEPT_ID as DEPT_ID5_1_1_ 
    from
        GG_EMPLOYEE emps0_ 
    where
        emps0_.DEPT_ID=?
2
AA
2

只查询一次Department和其关联的Employees。需要注意的是集合属性缓存使用时,不光要缓存集合属性,而且还要缓存集合里面的对象。

如下所示,不缓存集合里面的对象:

<!-- 二级缓存作用的持久化类 -->
<!-- 		<class-cache usage="read-write" class="com.jane.model.Employee"/> -->
		<class-cache usage="read-write" class="com.jane.model.Department"/>
		<collection-cache usage="read-write" collection="com.jane.model.Department.emps"/>

再次测试结果如下:

Hibernate: 
    select
        department0_.ID as ID1_0_0_,
        department0_.NAME as NAME2_0_0_ 
    from
        GG_DEPARTMENT department0_ 
    where
        department0_.ID=?
AA
Hibernate: 
    select
        emps0_.DEPT_ID as DEPT_ID5_1_0_,
        emps0_.ID as ID1_1_0_,
        emps0_.ID as ID1_1_1_,
        emps0_.NAME as NAME2_1_1_,
        emps0_.SALARY as SALARY3_1_1_,
        emps0_.EMAIL as EMAIL4_1_1_,
        emps0_.DEPT_ID as DEPT_ID5_1_1_ 
    from
        GG_EMPLOYEE emps0_ 
    where
        emps0_.DEPT_ID=?
2
AA
Hibernate: 
    select
        employee0_.ID as ID1_1_0_,
        employee0_.NAME as NAME2_1_0_,
        employee0_.SALARY as SALARY3_1_0_,
        employee0_.EMAIL as EMAIL4_1_0_,
        employee0_.DEPT_ID as DEPT_ID5_1_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.ID=?
Hibernate: 
    select
        employee0_.ID as ID1_1_0_,
        employee0_.NAME as NAME2_1_0_,
        employee0_.SALARY as SALARY3_1_0_,
        employee0_.EMAIL as EMAIL4_1_0_,
        employee0_.DEPT_ID as DEPT_ID5_1_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.ID=?
2

这种情况下,实际效果更差了。


【3】ehcache.xml详解

① 实例如下

<ehcache>

    <!-- Sets the path to the directory where cache .data files are created.

         If the path is a Java System Property it is replaced by
         its value in the running VM.

         The following properties are translated:
         user.home - User's home directory
         user.dir - User's current working directory
         java.io.tmpdir - Default temp file path -->
    <!--  
    	指定一个目录:当 EHCache 需要把数据写到硬盘上时, 将把数据写到这个目录下.
    -->     
    <diskStore path="d:\\tempDirectory"/>


<!--Default Cache configuration. These will applied to caches programmatically created 
    through   the CacheManager.

The following attributes are required for defaultCache:

maxInMemory       - Sets the maximum number of objects that will be created in memory
eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element   is never expired.
timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
              if the element is not eternal. Idle time is now - last accessed time
timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
              if the element is not eternal. TTL is now - creation time
overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
              has reached the maxInMemory limit.

-->
    <!--  
    	设置缓存的默认数据过期策略 
    -->    
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />

 
    <cache name="com.jane.model.Employee"
        maxElementsInMemory="1"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="true"
        />

    <cache name="com.jane.model.Department.emps"
        maxElementsInMemory="1000"
        eternal="true"
        timeToIdleSeconds="0"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        />

</ehcache>

② 详细说明如下

<diskStore>: 指定一个目录:当 EHCache 需要把数据写到硬盘上时, 将把数据写到这个目录下。
<defaultCache>: 设置缓存的默认数据过期策略 。

<cache>设定具体的命名缓存的数据过期策略,每个命名缓存代表一个缓存区域。

缓存区域(region):一个具有名称的缓存块,可以给每一个缓存块设置不同的缓存策略。如果没有设置任何的缓存区域,则所有被缓存的对象,都将使用默认的缓存策略。即:<defaultCache.../>

Hibernate 在不同的缓存区域保存不同的类/集合:

  • 对于类而言,区域的名称是类名。如:com.jane.model.Customer
  • 对于集合而言,区域的名称是类名加属性名。如com.jane.model.Customer.orders

defaultCache属性详解解释如下:

  • name: 设置缓存的名字,它的取值为类的全限定名或类的集合的名字 。

  • maxElementsInMemory: 设置基于内存的缓存中可存放的对象最大数目。

  • eternal: 设置对象是否为永久的, true表示永不过期。此时将忽略timeToIdleSeconds 和 timeToLiveSeconds属性; 默认值是false

  • timeToIdleSeconds:设置对象空闲最长时间,以秒为单位, 超过这个时间,对象过期。当对象过期时,EHCache会把它从缓存中清除。如果此值为0,表示对象可以无限期地处于空闲状态。

  • timeToLiveSeconds:设置对象生存最长时间,超过这个时间,对象过期。如果此值为0,表示对象可以无限期地存在于缓存中. 该属性值必须大于或等于 timeToIdleSeconds 属性值 。

  • overflowToDisk:设置基于内存的缓存中的对象数目达到上限后,是否把溢出的对象写到基于硬盘的缓存中


【4】Query 接口的 iterate() 方法

Query 接口的 iterator() 方法说明如下:

  • 同 list() 一样也能执行查询操作;
  • list() 方法执行的 SQL 语句包含实体类对应的数据表的所有字段;
  • Iterator() 方法执行的SQL 语句中仅包含实体类对应的数据表的 ID 字段。
    当遍历访问结果集时, 该方法先到 Session 缓存及二级缓存中查看是否存在特定 OID 的对象, 如果存在, 就直接返回该对象, 如果不存在该对象就通过相应的 SQL Select 语句到数据库中加载特定的实体对象

大多数情况下, 应考虑使用 list() 方法执行查询操作. iterator() 方法仅在满足以下条件的场合, 可以稍微提高查询性能:

  • 要查询的数据表中包含大量字段
  • 启用了二级缓存, 且二级缓存中可能已经包含了待查询的对象

测试代码一query.list():

	@Test
	public void testQueryIterate(){
		Department dept = (Department) session.get(Department.class, 1);
		System.out.println(dept.getName());
		System.out.println(dept.getEmps().size()); 
		
		Query query = session.createQuery("FROM Employee e WHERE e.dept.id = 1");
		//list方法查询出来一系列Employee对象并封装为list<employee>
		List<Employee> emps = query.list();
		System.out.println(emps.size()); 
	}

查询结果如下:

Hibernate: 
    select
        department0_.ID as ID1_0_0_,
        department0_.NAME as NAME2_0_0_ 
    from
        GG_DEPARTMENT department0_ 
    where
        department0_.ID=?
AA
Hibernate: 
    select
        emps0_.DEPT_ID as DEPT_ID5_1_0_,
        emps0_.ID as ID1_1_0_,
        emps0_.ID as ID1_1_1_,
        emps0_.NAME as NAME2_1_1_,
        emps0_.SALARY as SALARY3_1_1_,
        emps0_.EMAIL as EMAIL4_1_1_,
        emps0_.DEPT_ID as DEPT_ID5_1_1_ 
    from
        GG_EMPLOYEE emps0_ 
    where
        emps0_.DEPT_ID=?
2
Hibernate: 
    select
        employee0_.ID as ID1_1_,
        employee0_.NAME as NAME2_1_,
        employee0_.SALARY as SALARY3_1_,
        employee0_.EMAIL as EMAIL4_1_,
        employee0_.DEPT_ID as DEPT_ID5_1_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.DEPT_ID=1
2

测试代码二query.iterate()方法

	@Test
	public void testQueryIterate(){
		Department dept = (Department) session.get(Department.class, 1);
		System.out.println(dept.getName());
		System.out.println(dept.getEmps().size()); 
		
		Query query = session.createQuery("FROM Employee e WHERE e.dept.id = 1");
//		List<Employee> emps = query.list();
//		System.out.println(emps.size()); 
		//将OID查询出来
		Iterator<Employee> empIt = query.iterate();
		while(empIt.hasNext()){
			System.out.println(empIt.next().getName()); 
		}
	}

注意,测试二是在测试一的基础上进行的测试,此时query.list()的结果在二级缓存中。

测试二结果如下:

Hibernate: 
    select
        department0_.ID as ID1_0_0_,
        department0_.NAME as NAME2_0_0_ 
    from
        GG_DEPARTMENT department0_ 
    where
        department0_.ID=?
AA
Hibernate: 
    select
        emps0_.DEPT_ID as DEPT_ID5_1_0_,
        emps0_.ID as ID1_1_0_,
        emps0_.ID as ID1_1_1_,
        emps0_.NAME as NAME2_1_1_,
        emps0_.SALARY as SALARY3_1_1_,
        emps0_.EMAIL as EMAIL4_1_1_,
        emps0_.DEPT_ID as DEPT_ID5_1_1_ 
    from
        GG_EMPLOYEE emps0_ 
    where
        emps0_.DEPT_ID=?
2
Hibernate: 
    select
        employee0_.ID as col_0_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.DEPT_ID=1
AA
BB

可以看到,在测试一的基础上进行的测试二,查询性能比测试一稍微提高。


测试三,不在测试一的基础上,如下所示:

	@Test
	public void testQueryIterate(){
		Department dept = (Department) session.get(Department.class, 1);
		System.out.println(dept.getName());
		System.out.println(dept.getEmps().size()); 
		
		//这里修改了部门id与上面不同
		Query query = session.createQuery("FROM Employee e WHERE e.dept.id = 2");
//		List<Employee> emps = query.list();
//		System.out.println(emps.size()); 
		
		Iterator<Employee> empIt = query.iterate();
		while(empIt.hasNext()){
			System.out.println(empIt.next().getName()); 
		}
	}

测试结果如下:

Hibernate: 
    select
        department0_.ID as ID1_0_0_,
        department0_.NAME as NAME2_0_0_ 
    from
        GG_DEPARTMENT department0_ 
    where
        department0_.ID=?
AA
Hibernate: 
    select
        emps0_.DEPT_ID as DEPT_ID5_1_0_,
        emps0_.ID as ID1_1_0_,
        emps0_.ID as ID1_1_1_,
        emps0_.NAME as NAME2_1_1_,
        emps0_.SALARY as SALARY3_1_1_,
        emps0_.EMAIL as EMAIL4_1_1_,
        emps0_.DEPT_ID as DEPT_ID5_1_1_ 
    from
        GG_EMPLOYEE emps0_ 
    where
        emps0_.DEPT_ID=?
2
Hibernate: 
    select
        employee0_.ID as col_0_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.DEPT_ID=2
Hibernate: 
    select
        employee0_.ID as ID1_1_0_,
        employee0_.NAME as NAME2_1_0_,
        employee0_.SALARY as SALARY3_1_0_,
        employee0_.EMAIL as EMAIL4_1_0_,
        employee0_.DEPT_ID as DEPT_ID5_1_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.ID=?
CC
Hibernate: 
    select
        employee0_.ID as ID1_1_0_,
        employee0_.NAME as NAME2_1_0_,
        employee0_.SALARY as SALARY3_1_0_,
        employee0_.EMAIL as EMAIL4_1_0_,
        employee0_.DEPT_ID as DEPT_ID5_1_0_ 
    from
        GG_EMPLOYEE employee0_ 
    where
        employee0_.ID=?
DD

iterate()方法将OID查询出来,在需要的时候根据OID去数据库查询对象出来,性能降低!


猜你喜欢

转载自blog.csdn.net/J080624/article/details/83242225