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 数据隔离级别, 对于从来不会被修改的数据, 可以采用这种访问策略。
④ 二级缓存的提供者
Hibernate 的二级缓存是进程或集群范围内的缓存。
二级缓存是可配置的的插件, Hibernate 允许选用以下类型的缓存插件:
- EHCache: 可作为进程范围内的缓存, 存放数据的物理介质可以使内存或硬盘, 对 Hibernate 的查询缓存提供了支持。
- OpenSymphony OSCache:可作为进程范围内的缓存, 存放数据的物理介质可以使内存或硬盘, 提供了丰富的缓存数据过期策略, 对 Hibernate 的查询缓存提供了支持。
- SwarmCache: 可作为集群范围内的缓存, 但不支持 Hibernate 的查询缓存。
- JBossCache:可作为集群范围内的缓存, 支持 Hibernate 的查询缓存。
4 种缓存插件支持的并发访问策略(√ 代表支持, 空白代表不支持)
【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
可以看到,在测试一的基础上进行的测试二,性能比测试一稍微提高(只查询了OID,根据OID去缓存里面找对应对象,不用再向内存中添加一批Employee对象)。
测试三,不在测试一的基础上,如下所示:
@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去数据库查询对象出来,性能降低!