Hibernate二级缓存——SessionFactory

Hibernate二级缓存简介

  在《Hibernate一级缓存——Session》中介绍了Session级别的一级缓存,例如,当如下的代码执行时,由于一级缓存的作用,只会发送一条select语句:

1
2
3
4
5
6
7
8
9
@Test
public void testCache(){
Employee employee1 = (Employee) session.get(Employee.class, 1);
System.out.println(employee1);
Employee employee2 = (Employee) session.get(Employee.class, 1);
System.out.println(employee2);
}

image_1b3e0uaih1m5o1oqu1q3g18r1ksc13.png-19kB

  现在,我们在两次加载代码之间,先关闭当前的session和事务,再重新开启一个session和事务,那么不难理解,由于开启了新的session,所以会打印两条select语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testCache(){
Employee employee1 = (Employee) session.get(Employee.class, 1);
System.out.println(employee1);
//提交事务,关session
transaction.commit();
session.close();
//开启一个新的session和事务
session = sessionFactory.openSession();
transaction = session.beginTransaction();
Employee employee2 = (Employee) session.get(Employee.class, 1);
System.out.println(employee2);
}

image_1b3e116cat3g1gk1fv4brlje1g.png-34.8kB
  那么,有没有办法使就算session关闭,也能缓存employee对象的办法呢?这就是我们现在要介绍的,SessionFactory级别的,Hibernate二级缓存。
  
  缓存(Cache )是计算机领域非常通用的概念。它介于应用程序和永久性数据存储源(如硬盘上的文件或者数据库)之间,其作用是降低应用程序直接读写永久性数据存储源的频率,从而提高应用的运行性能。缓存中的数据是数据存储源中数据的拷贝,应用程序在运行时直接读写缓存中的数据,只在某些特定时刻按照缓存中的数据来同步更新数据存储源。缓存的物理介质通常是内存。
  
  Hibernate中提供了两个级别的缓存:
  1.第一级别的缓存是Session级别的缓存,它是属于事务范围的缓存,即缓存只能被当前事务访问,每个事务都有独自的缓存。缓存的生命周期依赖于事务的生命周期,当事务结束时,缓存也就结束生命周期。在此范围下,缓存的介质是内存。这一级别的缓存是由hibernate管理的,一般情况下无须进行干预。
  2.第二级别的缓存是SessionFactory级别的缓存,它是属于进程范围的缓存。缓存被进程内的所有事务共享。这些事务有可能是并发访问缓存,因此必须对缓存采取必要的事务隔离机制。缓存的生命周期依赖于进程的生命周期,进程结束时,缓存也就结束了生命周期。进程范围的缓存可能会存放大量的数据,所以存放的介质可以是内存或硬盘。这一级别的缓存可以进行配置和更改,并且可以动态加载和卸载。
  
  SessionFactory 的缓存可以分为两类:
  内置缓存: Hibernate自带的,不可卸载。通常在Hibernate的初始化阶段,Hibernate会把映射元数据和预定义的SQL语句放到SessionFactory的缓存中,映射元数据是映射文件中数据的复制,而预定义SQL语句是Hibernate根据映射元数据推到出来的。该内置缓存是只读的。
  外置缓存(二级缓存):一个可配置的缓存插件。在默认情况下,SessionFactory不会启用这个缓存插件。外置缓存中的数据是数据库中数据的复制,外置缓存的物理介质可以是内存,也可以是硬盘。
  
  缓存的物理介质通常是内存,而永久性数据存储源的物理介质通常是硬盘或磁盘,应用程序读写内在的速度显然比读写硬盘的速度快,如果缓存中存放的数据量非常大,也会用硬盘作为缓存的物理介质。缓存的实现不仅需要作为物理介质的硬件,同时还需要用于管理缓存的并发访问和过期等策略的软件。因此,缓存是通过软件和硬件共同实现的。
  
  适合放入二级缓存中的数据:
  1.很少被修改;
  2.不是很重要的数据,允许出现偶尔的并发问题。
  
  不适合放入二级缓存中的数据:
  1.经常被修改;
  2.财务数据,绝对不允许出现并发问题;
  3.与其他应用程序共享的数据。
  
  二级缓存的并发访问策略:
  两个并发的事务同时访问持久层的缓存的相同数据时,也有可能出现各类并发问题。
  二级缓存可以设定以下4种类型的并发访问策略,每一种访问策略对应一种事务隔离级别。
  非严格读写(Nonstrict-read-write):不保证缓存与数据库种数据的一致性。提供read uncommitted事务隔离级别。对于极少被修改,而且允许脏读的数据,可以采用这种策略。
  读写型(Read-write):提供read committed事务隔离级别。对于经常读但是很少被修改的数据,可以采用这种隔离类型,它可以防止脏读。(通常选用的策略)
  事务型(Transactional):仅在受管理环境下适用。它提供了repeatable read的事务隔离级别。对于经常读,但是很少被修改的数据,可以采用这种隔离类型,它可以防止脏读和不可重复读。
  只读型(Read-Only):提供serializable事务隔离级别,对于从来不会修改的数据,可以采用这种访问策略,可以避免脏读,不可重复读和幻读。
  
  管理Hibernate的二级缓存:
  Hibernate的二级缓存是进程或者集群范围内的缓存。
  二级缓存是可配置的插件,Hibernate允许选用以下类型的缓存插件:
  EhCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬盘,对Hibernate的查询缓存提供了支持。
  OSCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬盘,提供了丰富的缓存数据过期策略,对Hibernate的查询缓存提供了支持。
  SwarmCache:可作为群集范围内的缓存,但不支持Hibernate的查询缓存。   JBossCache:可作为群集范围内的缓存,支持事务型并发访问策略,对Hibernate的查询缓存提供了支持。
  
image_1b3dvptab23t1vc1v1g26c1cei9.png-49.7kB

使用Hibernate二级缓存的步骤

  
一、加入二级缓存的jar包及配置文件
  
1.加入jar包
  image_1b3dvuhed16r4ci0ghv1aj3f4m.png-5.1kB
  
2.添加配置文件ehcache.xml到src目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<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 -->
<diskStore path="java.io.tmpdir"/>
<!--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"
/>
<!--Predefined caches. Add your cache configuration settings here.
If you do not have a configuration for your cache a WARNING will be issued when the
CacheManager starts
The following attributes are required for defaultCache:
name - Sets the name of the cache. This is used to identify the cache. It must be unique.
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.
-->
<!-- Sample cache named sampleCache1
This cache contains a maximum in memory of 10000 elements, and will expire
an element if it is idle for more than 5 minutes and lives for more than
10 minutes.
If there are more than 10000 elements it will overflow to the
disk cache, which in this configuration will go to wherever java.io.tmp is
defined on your system. On a standard Linux system this will be /tmp"
-->
<cache name="sampleCache1"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<!-- Sample cache named sampleCache2
This cache contains 1000 elements. Elements will always be held in memory.
They are not expired. -->
<cache name="sampleCache2"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/> -->
<!-- Place configuration for your caches following -->
</ehcache>

二、配置hibernate.cfg.xml
 
1.配置启用hibernate的二级缓存

1
<property name="cache.use_second_level_cache">true</property>

2.配置hibernate二级缓存使用的产品

1
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>

3.配置对哪些类(或属性)使用 hibernate 的二级缓存

第一种情况,类级别的二级缓存:(配置对Employee类使用二级缓存)

1
<class-cache usage="read-write" class="com.atguigu.hibernate.entities.Employee"/>

第二种情况,集合级别的二级缓存:(配置对Department类中的Employee集合属性emps使用二级缓存)

1
<collection-cache usage="read-write" collection="com.atguigu.hibernate.entities.Department.emps"/>

注意,当配置对集合属性使用二级缓存时,还需要对集合所属的类,集合中元素的类型使用二级缓存,例如,对于上述集合属性,则还需配置对Department类和Employee类使用二级缓存:(如果不对Employee类配置二级缓存,则会多出n条SQL语句,得不偿失。因为这种情况下缓存的是一个一个的employee的id,当要使用到employee对象时,需要再根据id一条一条地去数据库查询记录)

1
2
<class-cache usage="read-write" class="com.atguigu.hibernate.entities.Department"/>
<class-cache usage="read-write" class="com.atguigu.hibernate.entities.Employee"/>

另外,配置对哪些类(或属性)使用二级缓存,还可以在映射文件中配置,例如:
类级别(在class节点下):

1
<cache usage="read-write"/>

集合级别:(在department映射文件的set节点下)

1
<cache usage="read-write"/>

  现在,测试上面的testCache方法,只会打印一条select语句:
  image_1b3e2cv2vad01ntl1rt71oac7o41t.png-18kB
  对于集合的测试也是一样,只会打印两条select语句,一条用于查询department,一条用于查询employee:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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());
}

image_1b3e2epr8r731smq4t0scfca62a.png-30.6kB

二级缓存配置文件

  下面使用一个修改过的二级缓存配置文件介绍其中各个属性的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<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"
/>
<!--
设定具体的命名缓存的数据过期策略。每个命名缓存代表一个缓存区域
缓存区域(region):一个具有名称的缓存块,可以给每一个缓存块设置不同的缓存策略。
如果没有设置任何的缓存区域,则所有被缓存的对象,都将使用默认的缓存策略。即:<defaultCache.../>
Hibernate 在不同的缓存区域保存不同的类/集合。
对于类而言,区域的名称是类名。如:com.atguigu.domain.Customer
对于集合而言,区域的名称是类名加属性名。如com.atguigu.domain.Customer.orders
-->
<!--
name: 设置缓存的名字,它的取值为类的全限定名或类的集合的名字
maxElementsInMemory: 设置基于内存的缓存中可存放的对象最大数目
eternal: 设置对象是否为永久的, true表示永不过期,
此时将忽略timeToIdleSeconds 和 timeToLiveSeconds属性; 默认值是false
timeToIdleSeconds:设置对象空闲最长时间,以秒为单位, 超过这个时间,对象过期。
当对象过期时,EHCache会把它从缓存中清除。如果此值为0,表示对象可以无限期地处于空闲状态。
timeToLiveSeconds:设置对象生存最长时间,超过这个时间,对象过期。
如果此值为0,表示对象可以无限期地存在于缓存中. 该属性值必须大于或等于 timeToIdleSeconds 属性值
overflowToDisk:设置基于内存的缓存中的对象数目达到上限后,是否把溢出的对象写到基于硬盘的缓存中
-->
<cache name="com.atguigu.hibernate.entities.Employee"
maxElementsInMemory="1"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<cache name="com.atguigu.hibernate.entities.Department.emps"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/>
</ehcache>

查询缓存

  对于经常使用的查询语句,如果启用了查询缓存,当第一次执行查询语句时,Hibernate会把查询结果存放在查询缓存中,以后再次执行该查询语句时,只需从缓存中获得查询结果,从而提高查询性能。
  默认情况下,Hibernate设置的缓存对HQL和QBC查询是无效的,但可以通过以下步骤使其有效:
  1.配置二级缓存,因为查询缓存依赖于二级缓存.
  2.在hibernate配置文件中声明开启查询缓存。

1
<property name="cache.use_query_cache">true</property>

  3.调用Query或者Criteria的setCachable(true)。
  
例如,在没有配置查询缓存的情况下,下面的代码会打印两条select语句:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testQueryCache(){
Query query = session.createQuery("FROM Employee");
List<Employee> emps = query.list();
System.out.println(emps.size());
emps = query.list();
System.out.println(emps.size());
}

image_1b3e39bve1vg91r2t12i65c91n782n.png-30.6kB
  
  进行了相关配置之后,并且在方法中设置query.setCacheable(true);则只会打印一条select语句:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testQueryCache(){
Query query = session.createQuery("FROM Employee");
query.setCacheable(true);
List<Employee> emps = query.list();
System.out.println(emps.size());
emps = query.list();
System.out.println(emps.size());
}

image_1b3e3bndj184be4f1mga1gi81baq34.png-18.5kB

  查询缓存适用于如下场合:
  1.应用程序运行时经常使用查询语句
  2.很少对与查询语句检索到的数据进行插入,删除或更新操作

时间戳缓存区域

  时间戳缓存区域存放了对于查询结果相关的表进行插入,更新或者删除操作的时间戳。Hibernate通过时间戳缓存区域来判断被缓存的查询结果是否过期,其运行过程如下:
  T1时刻执行查询操作,把结果存放在QueryCache区域,记录该区域的时间戳为T1;
  T2时刻(可能在T1之前,也可能在T1之后)对查询结果相关的表进行更新操作,Hibernate把T2时刻存放在UpdateTimestampCache区域。
  T3时刻(在T1,T2之后)执行查询结果前,先比较QueryCache区域的时间戳和UpdateTimestampCache区域的时间戳。若T2>T1,则丢弃原先存放在QueryCache区域的查询结果,重新到数据库中查询数据并放入QueryCache区域;若T2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testUpdateTimeStampCache(){
Query query = session.createQuery("FROM Employee");
query.setCacheable(true);
List<Employee> emps = query.list();
System.out.println(emps.size());
Employee employee = (Employee) session.get(Employee.class, 1);
employee.setSalary(30000);
emps = query.list();
System.out.println(emps.size());
}

  在第二次查询的时候,会重新用select去数据库中查找最新的记录。
  image_1b3e56hbmjqu3n719ono39k8m3h.png-40.9kB

Query接口的iterate()方法

  Query的list方法返回实体类对应表的所有字段,而Query的iterate方法仅返回数据包的ID字段。当使用了iterate方法,然后遍历访问结果集时,先到Session缓存及二级缓存中查看是否存在特定OID的对象,如果存在,直接返回,否则就通过相应的SQL SELECT语句到数据库中查找特定的记录。
  在大多数情况下,应该考虑使用list方法执行查询操作,iterate方法仅在下述情况下可以稍微提高查询性能:
  1.要查询的数据表中包含大量字段;
  2.启用了二级缓存,且二级缓存中可能已经包含了待查询的对象。