Hibernate二级缓存问题
Posted ng-xixi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hibernate二级缓存问题相关的知识,希望对你有一定的参考价值。
相关概念和定义
1、缓存的意义
把一些不常修改,但是又经常用的数据存放到内存中,这样能减少与数据库的交互,提升程序的性能
2、Hibernate中提供了两级缓存:
第一级别的缓存是Session级别的缓存(比如说在调用get方法的时候,如果已经查询过一次了,第二次就不会查了,而是直接返回session缓存中已经存在的那个对象给你,不过这个只对当前Session有效,一旦又开一个新的Session的话,调用get方法获取数据时还是会再次去查询数据库的)。这一级别的缓存由hibernate 管理的,一般情况下无需进行干预
第二级别的缓存是SessionFactory 级别的缓存,也就是hibernate二级缓存,它是属于进程范围的缓存
3、SessionFactory的缓存:
内置缓存: Hibernate 自带的, 不可卸载. 通常在 Hibernate 的初始化阶段, Hibernate 会把映射元数据和预定义的 SQL 语句放到 SessionFactory 的缓存中, 映射元数据是映射文件中数据的复制, 而预定义 SQL 语句是 Hibernate 根据映射元数据推倒出来的. 该内置缓存是只读的.
外置缓存(二级缓存): 一个可配置的缓存插件. 默认情况下 SessionFactory 不会启动二级缓存,需要用户自己导入第三方插件,在hibernate.cfg.xml文件中通过配置开启二级缓存。外置缓存中的数据是数据库数据的复制, 外置缓存的物理介质可以是内存或硬盘
4、二级缓存的内存结构
二级缓存由缓存提供者提供(CacheProvider),包含四部分:类缓存区、集合缓存区、查询缓存区、更新时间戳
5、二级缓存的并发访问策略
6、缓存中存放的数据
适合放入二级缓存中的数据:
很少被修改
不是很重要的数据, 允许出现偶尔的并发问题
不适合放入二级缓存中的数据:
经常被修改
财务数据, 绝对不允许出现并发问题
与其他应用数据共享的数据
7、缓存提供的供应商
Hibernate 的二级缓存是进程或集群范围内的缓存, 缓存中存放的是对象的散装数据
二级缓存是可配置的的插件,Hibernate 允许选用以下类型的缓存插件:
EHCache: 可作为进程范围内的缓存, 存放数据的物理介质可以是内存或硬盘, 对 Hibernate 的查询缓存提供了支持
OpenSymphony `:可作为进程范围内的缓存, 存放数据的物理介质可以是内存或硬盘, 提供了丰富的缓存数据过期策略, 对 Hibernate 的查询缓存提供了支持
SwarmCache: 可作为集群范围内的缓存, 但不支持 Hibernate的查询缓存
JBossCache:可作为集群范围内的缓存, 支持 Hibernate 的查询缓存
4 种缓存插件支持的并发访问策略
15.2EHCache配置步骤
1、导入jar包及依赖jar包:
ehcache-1.5.0.jar依赖backport-util-concurrent 和 commons-logging
2、在hibernate.cfg.xml中开启二级缓存
<property name="hibernate.cache.use_second_level_cache">true</property>
3、在hibernate.cfg.xml指定缓存供应商
<property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
4、配置哪些数据使用二级缓存,不配置的话二级缓存不会缓存任何数据(在hibernate.cfg.xml文件里和映射文件里配置2选1,需要注意的是这些配置必须配置在映射文件的后面,具体参考hibernate-configuration-3.0.dtd)
第一种方式:
在hibernate.cfg.xml中配置
<!-- 配置类级别的二级缓存:这个必须放在映射文件引入的后面 -->
<class-cache usage="read-write" class="com.itheima.domain.Department"/>
<class-cache usage="read-write" class="com.itheima.domain.Employee"/>
<!-- 配置集合级别的二级缓存 -->
<collection-cache usage="read-write" collection="com.itheima.domain.Department.emps"/>
第二种方式:
Department.hbm.xml
<class name="Department" table="DEPARTMENT">
<!-- 类级别的二级缓存:必须放在class节点的第一个位置 -->
<cache usage="read-write"/>
<id name="id">
<generator class="native"/>
</id>
<property name="name" type="string" column="NAME" />
<set name="emps" table="Employee">
<!-- 集合级别的二级缓存 -->
<cache usage="read-write"/>
<key column="DEPT_ID"></key>
<one-to-many class="Employee" />
</set>
</class>
Employee.hbm.xml
<class name="Employee" table="EMPLOYEE">
<!-- 配置类级别的二级缓存 -->
<cache usage="read-write"/>
<id name="id">
<generator class="native"/>
</id>
<property name="name" type="string" column="NAME" />
<many-to-one name="depart">
<column name="DEPT_ID"></column>
</many-to-one>
</class>
5、配置ehcache默认的配置文件ehcache.xml(名字固定)(放在类路径下)
可以拷贝ehcache-1.5.0.jar包中的ehcache-failsafe.xml,然后改名为ehcache.xml
ehcache.xml内容:除了硬盘存储目录,其他内容基本上不需要更改
<diskStore path="java.io.tmpdir"/>
<!--硬盘默认缓存目录(C:UserswzhtingAppDataLocalTemp)-->
<!--我们一般都用下面的默认数据过期策略-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
也可以自己设置过期策略,下面详细解释一下缓存策略的配置中每个属性的意思:
15.3类级别的二级缓存(Class Cache)
所谓类级别的二级缓存,就是查询出的一个实体类对象会放入二级缓存
例一、编写测试用例证明数据存入了二级缓存
public void testClassCache(){
//第一次从数据库中查询一个部门的数据,会发送一条sql语句
Session s1 = HibernateUtil.getCurrentSession();
Transaction tx1 = s1.beginTransaction();
Department d1 = (Department)s1.get(Department.class, 1);
System.out.println(d1.getName());
tx1.commit();
//第二次查询一个部门的数据,由于s1已经关闭,而且已经配置了二级缓存,所以不会再发送sql
//语句,而是直接从二级缓存中去取,如果s1每关闭呢?那么会优先从一级缓存中取这个部门的数据
Session s2 = HibernateUtil.getCurrentSession();
Transaction tx2 = s2.beginTransaction();
Department d2 = (Department)s2.get(Department.class, 1);
System.out.println(d2.getName());
tx2.commit();
}
分析:
第一次查询时,把数据存放到一级缓存和二级缓存中,但存放形式不一样。一级缓存存放的是实体对象的引用(即内存地址),而二级缓存的类缓存区存放的是对象中的数据(散列数据id:1 name:d1name)。
一级缓存没有关闭的情况下,再次查询同样的实体记录,返回的是对象的引用,因此两次从一级缓存中取出的对象内存地址一致。而一级缓存关闭后,从二级缓存中取出的数据因为是散列数据,需要重新封装到新对象中,所以,内存地址会不同。
以下代码可以再次证明:
public void testClassCache(){
//第一次从数据库中查询一个部门的数据,会发送一条sql语句
Session s1 = HibernateUtil.getCurrentSession();
Transaction tx1 = s1.beginTransaction();
Department d1 = (Department)s1.get(Department.class, 1);
System.out.println(d1);
//第二次查询一个部门的数据,由于s1已经关闭,而且已经配置了二级缓存,所以不会再发送sql
//语句,而是直接从二级缓存中去取,如果s1每关闭呢?那么会优先从一级缓存中取这个部门的数据
Session s2 = HibernateUtil.getCurrentSession();
Department d2 = (Department)s2.get(Department.class, 1);
System.out.println(d2);
//这里才开始提交事务哦亲:s1和s2用的是同一个事务,同一个事务内,相同的数据是会从一级缓存里拿的,
//所以控制台看到的两个Department对象的内存地址是一样的
tx1.commit();
//还是不会从数据库里去查询,证明了散列数据的存在
Session s3 = HibernateUtil.getCurrentSession();
Transaction tx3 = s3.beginTransaction();
Department d3 = (Department)s3.get(Department.class, 1);
System.out.println(d3);
tx3.commit();
}
结果:
Hibernate:
select
department0_.id as id0_0_,
department0_.NAME as NAME0_0_
from
DEPARTMENT department0_
where
department0_.id=?
<!--以下是通过s1获取到的Department对象的内存地址-->
<!--以下是通过s2获取到的Department对象的内存地址-->
<!--以下是通过s3获取到的Department对象的内存地址-->
例二、get和load 可以读取类级别二级缓存,但是从数据库里查询出的一个集合的数据就只能存,不能取了,这个集合(直接用session.createQuery、session.createCriteria、session.createSQLQuery查出来的集合)要跟集合级别的二级缓存区分开来,集合级别的二级缓存说的是一个实体类中有一个集合属性(比如说部门的实体类中的员工的集合属性),这个集合查出来后会存入集合级别的二级缓存,两者概念不能混淆了
/**
* 测试查询出来的集合是否会存入二级缓存
* 结论:不管是Query、Criteria、还是原生态的sql查出来的集合数据不会存入二级缓
* 存,但是集合中的每个对象会存入二级缓存
*/
@SuppressWarnings("unchecked")
@Test
public void testCollCache(){
//第一次获取集合数据
Session s1 = HibernateUtil.getCurrentSession();
Transaction tx1 = s1.beginTransaction();
//List<Employee> emps1 = s1.createQuery("from Employee").list();
//List<Employee> emps1 = s1.createCriteria(Employee.class).list();
List<Employee> emps1 = s1.createSQLQuery("select * from employee").list();
System.out.println(emps1.size());
tx1.commit();
//第二次获取集合数据:又从数据库中取了一次
Session s2 = HibernateUtil.getCurrentSession();
Transaction tx2 = s2.beginTransaction();
//List<Employee> emps2 = s2.createQuery("from Employee").list();
//List<Employee> emps2 = s2.createCriteria(Employee.class).list();
List<Employee> emps2 = s2.createSQLQuery("select * from employee").list();
System.out.println(emps2.size());
tx2.commit();
//获取集合中的一个对象的数据:没有从数据库去取,说明集合中的对象已经都存入二级缓存了
Session s3 = HibernateUtil.getCurrentSession();
Transaction tx3 = s3.beginTransaction();
Employee e = (Employee)s3.get(Employee.class, 1);
System.out.println(e.getName());
tx3.commit();
}
问题:如果我确实有缓存集合这样的需求呢?就是说我用session.createQuery、session.createCriteria、session.createSQLQuery从数据库里查出来的集合想要进行缓存,怎么办?
答:请参看15.6查询缓存,这里面详细解释了如何进行集合的缓存
15.4集合级别的二级缓存(Collection Cache)
例一、集合级别二级缓存测试
我要测试的东西很明确,就是说当我用一个session查询出一个部门后(这个部门实体类中有一个员工的集合属性Set<Employee>),用第二个session再次去查询这同一个部门的话,还会不会再去数据库里查一次,当我获取这个部门中的员工集合的时候,会不会再去数据库里查一次?下面我们准备好测试环境和代码看演示效果:
准备测试环境:配置hibernate.cfg.xml(在实体类.hbm.xml中配置也行,详情参见15.2EHCache配置步骤)
<!-- 配置类级别的二级缓存 -->
<class-cache usage="read-write" class="com.itheima.domain.Department"/>
<class-cache usage="read-write" class="com.itheima.domain.Employee"/>
<!-- 配置集合级别的二级缓存 -->
<collection-cache usage="read-write" collection="com.itheima.domain.Department.emps"/>
/**
* 测试实体类中的集合是否会存入二级缓存:也就是集合级别的数据是否会存入二级缓存
* 结论:实体类中的集合属性的数据会存入集合级别的二级缓存
*/
@Test
public void testCollInEntity(){
Session s1 = HibernateUtil.getCurrentSession();
Transaction tx1 = s1.beginTransaction();
//存入类级别的二级缓存
Department dept1 = (Department)s1.get(Department.class, 1);
//存入集合级别的二级缓存
Set<Employee> emps1 = dept1.getEmps();
for(Employee e:emps1){
System.out.println(e.getName());
}
System.out.println("---------------------------");
tx1.commit();
Session s2 = HibernateUtil.getCurrentSession();
Transaction tx2 = s2.beginTransaction();
//从类级别的二级缓存中取出数据
Department dept2 = (Department)s2.get(Department.class, 1);
//从集合级别的二级缓存中取出数据
Set<Employee> emps2 = dept2.getEmps();
for(Employee e:emps2){
System.out.println(e.getName());
}
tx2.commit();
}
集合区的数据存放原理
集合区的数据存放原理结论:
由图可知,实体类中的集合属性的数据在存储时分为两部分,集合中每个对象的oid存储在集合缓存区,每个对象的具体的属性值数据存储在类级别的缓存区,当需要用的时候根据oid再次从类级别的缓存区中获取数据进行封装
小疑问
hibernate.cfg.xml:注释掉
<class-cache usage="read-write" class="com.itheima.domain.Employee"/>
请问会发生什么?
答:当第二次用这个部门中的员工集合的数据的时候会再次从数据库中去查询
例二、一级缓存的更新会自动同步到二级缓存中
当一级缓存中的数据更新后,是会同步到二级缓存中去的,下面我们测试一下这个结论:
/**
* 测试一级缓存中的数据更新后,会不会同步更新到二级缓存
* 结论:会
*/
@Test
public void cacheLevel1ToLevel2(){
Session s1 = HibernateUtil.getCurrentSession();
Transaction tx1 = s1.beginTransaction();
//存入一级缓存和类级别的二级缓存
Department dept1 = (Department)s1.get(Department.class, 1);
//这时候我们更新以下dept1中的数据,其实这个操作就是更新一级缓存中的数据,因为我们操作的是持久化对象
dept1.setName("hhhhh");
//当事务提交的时候,会同步将数据更新到二级缓存
tx1.commit();
Session s2 = HibernateUtil.getCurrentSession();
Transaction tx2 = s2.beginTransaction();
//从类级别的二级缓存中取出部门数据
Department dept2 = (Department)s2.get(Department.class, 1);
//如果打印结果是hhhhhh,那说明一级缓存中的更新确实会同步到二级缓存中去,结果是肯定的
System.out.println(dept2.getName());
tx2.commit();
}
示例:超过内存缓存数量会缓存到硬盘
maxElementsInMemory=5,查询所有员工(通过加断点来观察硬盘中的缓存数据大小)
15.5更新时间戳区(update timestamps)
代码示例:
15.6查询缓存
对于经常使用的查询语句, 如果启用了查询缓存, 当第一次执行查询语句时, Hibernate 会把查询结果存放在查询缓存中. 以后再次执行该查询语句时, 只需从缓存中获得查询结果, 从而提高查询性能
查询缓存使用于如下场合:
1.> 应用程序运行时经常使用查询语句
2.> 很少对与查询语句检索到的数据进行插入, 删除和更新操作
l 查询缓存的特点
1、查询缓存基于二级缓存的。
2、HQL的from Department的数据保存在类缓存区的,查询缓存区存放的是对象的ID
3、如果配置了查询缓存:将以SQL语句为key,查询结果为value存放
l 查询缓存的使用步骤:
a、启动查询缓存
hibernate.cfg.xml:
<property name=“hibernate.cache.use_query_cache">true</property>
b、程序中使用:query.setCacheable(true);
示例:
public void testQueryCache(){
Session s1 = HibernateUtil.getCurrentSession();
Transaction tx1 = s1.beginTransaction();
Query query1 = s1.createQuery("from Employee where id<10");
//启用查询缓存,将整个list都存储到查询缓存区,第一次查询,
//会从数据库去查询出这9条数据(为啥是9条呢?我数据库里id是从1开始的)
query1.setCacheable(true);
List<Employee> emps1 = query1.list();
System.out.println(emps1.size());
tx1.commit();
Session s2 = HibernateUtil.getCurrentSession();
Transaction tx2 = s2.beginTransaction();
Query query2 = s2.createQuery("from Employee where id<9");
//启用查询缓存,取数据
query2.setCacheable(true);
//这时候就不会从数据库里去查询了,但是如果第二次查询的时候sql语句变化了,那么这时候第二次取的时候
//就又会再一次从数据库里去查询了,因为查询缓存区维护的是一个map,key是sql语句,
//值是该sql语句查询出来的对象
List<Employee> emps2 = query2.list();
System.out.println(emps2.size());
tx2.commit();
}
l Query接口的list VSiterate
list():只能放数据到二级缓存,不能取,每次拿出来的数据是实体对象的所有的属性
iterate():每次拿出来的集合数据是集合的ID属性,当对集合中的数据进行遍历的时候优先从二级缓存中取每一个对象的数据,如果二级缓存中存在则直接拿出来用,不存在,则到数据库里去取,有几个对象不存在就会去查几次,以下代码验证了我们的结论:
public void testIterate(){
Session s1 = HibernateUtil.getCurrentSession();
Transaction tx1 = s1.beginTransaction();
//先查询出10条数据的所有字段放到二级缓存中
List<Employee> emps1 = s1.createQuery("from Employee where id<10").list();
System.out.println(emps1.size());
tx1.commit();
Session s2 = HibernateUtil.getCurrentSession();
Transaction tx2 = s2.beginTransaction();
//查询出13条数据的所有id字段,其他字段不查询
Iterator<Employee> emps2 = s2.createQuery("from Employee where id < 13").iterate();
//前面9条数据都会从二级缓存中去取,所以不会产生sql语句,第10、11、12三条数据每次迭代都会从数据库里查一次
while(emps2.hasNext())
System.out.println(emps2.next().getName());
tx2.commit();
}
---------------------
作者:zhoulenihao
来源:CSDN
原文:https://blog.csdn.net/zhoulenihao/article/details/25070575
版权声明:本文为博主原创文章,转载请附上博文链接!
以上是关于Hibernate二级缓存问题的主要内容,如果未能解决你的问题,请参考以下文章
[原创]java WEB学习笔记93:Hibernate学习之路---Hibernate 缓存介绍,缓存级别,使用二级缓存的情况,二级缓存的架构集合缓存,二级缓存的并发策略,实现步骤,集合缓存,查询缓