Hibernate的状态以及缓存问题
Hibernate的状态
临时状态/瞬时状态(transient):刚刚用new语句创建,没有被持久化
不处于session中(没有使用session的方法去操作临时对象)。该对象成为临时对象
持久化状态/托管状态(persistent):已经被持久化,加入到session的缓存中。session是没有关闭该状态的对象为持久化对象。
游离状态/脱管状态(detached):已经被持久化,但不处于session中。
该状态的对象为游离对象。
删除状态(removed):对象有关联的ID,并且在Session管理下,但是已经被计划(事务提交的时候,commit())删除。如果没有事务就不能删除
缓存:
为什么使用缓存:为了提供访问速度,把磁盘或数据库访问变成内存访问。
Hibernate缓存包括两大类:Hibernate一级缓存和Hibernate二级缓存。
1.Hibernate一级缓存又称为“Session的缓存”。
Session缓存内置不能被卸载,Session的缓存是事务范围的缓存(Session对象的生命周期通常对应一个数据库事务或者一个应用事务)。
一级缓存中,持久化类的每个实例都具有唯一的OID。
2.Hibernate二级缓存又称为“SessionFactory的缓存”。
由于SessionFactory对象的生命周期和应用程序的整个过程对应,因此Hibernate二级缓存是进程范围或者集群范围的缓存,有可能出现并发问题,因此需要采用适当的并发访问策略,该策略为被缓存的数据提供了事务隔离级别。
第二级缓存是可选的,是一个可配置的插件,默认下SessionFactory不会启用这个插件。
Hibernate提供了org.hibernate.cache.CacheProvider接口,它充当缓存插件与Hibernate之间的适配器。
一级缓存就是Session级别的缓存,在事务范围内有效是,内置的不能被卸载。二级缓存是SesionFactory级别的缓存,从应用启动到应用结束有效。是可选的,默认没有二级缓存,需要手动开启。
保存数据库后,在内存中保存一份,如果更新了数据库就要同步更新。
什么样的数据适合存放到第二级缓存中?
1) 很少被修改的数据 帖子的最后回复时间
2) 经常被查询的数据 电商的地点
2) 不是很重要的数据,允许出现偶尔并发的数据
3) 不会被并发访问的数据
4) 常量数据
扩展:hibernate的二级缓存默认是不支持分布式缓存的。使用memcahe,redis等中央缓存来代替二级缓存。
如何开启二级缓存:
1.添加二级缓存包,常用的二级缓存包是EHcache。这个我们在下载好的hibernate的lib->optional->ehcache下可以找到(我这里使用的hibernate4.1.7版本),然后将里面的几个jar包导入即可
2.在hibernate.cfg.xml配置文件中配置我们二级缓存的一些属性
<!-- 开启二级缓存 -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- 二级缓存的提供类 在hibernate4.0版本以后我们都是配置这个属性来指定二级缓存的提供类-->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<!-- 二级缓存配置文件的位置 -->
<property name="hibernate.cache.provider_configuration_file_resource_path">ehcache.xml</property>
3.配置hibernate的二级缓存是通过使用 ehcache的缓存包,所以我们需要创建一个 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 -->
<!--指定二级缓存存放在磁盘上的位置-->
<diskStore path="user.dir"/>
<!--我们可以给每个实体类指定一个对应的缓存,如果没有匹配到该类,则使用这个默认的缓存配置-->
<defaultCache
maxElementsInMemory="10000" //在内存中存放的最大对象数
eternal="false" //是否永久保存缓存,设置成false
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true" //如果对象数量超过内存中最大的数,是否将其保存到磁盘中,设置成true
/>
<!--
1、timeToLiveSeconds的定义是:以创建时间为基准开始计算的超时时长;
2、timeToIdleSeconds的定义是:在创建时间和最近访问时间中取出离现在最近的时间作为基准计算的超时时长;
3、如果仅设置了timeToLiveSeconds,则该对象的超时时间=创建时间+timeToLiveSeconds,假设为A;
4、如果没设置timeToLiveSeconds,则该对象的超时时间=max(创建时间,最近访问时间)+timeToIdleSeconds,假设为B;
5、如果两者都设置了,则取出A、B最少的值,即min(A,B),表示只要有一个超时成立即算超时。
-->
<!--可以给每个实体类指定一个配置文件,通过name属性指定,要使用类的全名-->
<cache name="com.xiaoluo.bean.Student"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<cache name="sampleCache2"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/> -->
</ehcache>
4.开启我们的二级缓存
①如果使用xml配置,我们需要在 Student.hbm.xml 中加上一下配置:
<hibernate-mapping package="com.xiaoluo.bean">
<class name="Student" table="t_student">
<!-- 二级缓存一般设置为只读的 -->
<cache usage="read-only"/>
<id name="id" type="int" column="id">
<generator class="native"/>
</id>
<property name="name" column="name" type="string"></property>
<property name="sex" column="sex" type="string"></property>
<many-to-one name="room" column="rid" fetch="join"></many-to-one>
</class>
</hibernate-mapping>
二级缓存的使用策略一般有这几种:read-only、nonstrict-read-write、read-write、transactional。注意:我们通常使用二级缓存都是将其配置成 read-only ,即我们应当在那些不需要进行修改的实体类上使用二级缓存,否则如果对缓存进行读写的话,性能会变差,这样设置缓存就失去了意义。
②如果使用annotation配置,我们需要在Student这个类上加上这样一个注解:
@Entity
@Table(name="t_student")
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY) // 表示开启二级缓存,并使用read-only策略
public class Student
{
private int id;
private String name;
private String sex;
private Classroom room;
.......
}
调用
TestCase1
public class TestSecondCache
{
@Test
public void testCache1()
{
Session session = null;
try
{
session = HibernateUtil.openSession();
Student stu = (Student) session.load(Student.class, 1);
System.out.println(stu.getName() + "-----------");
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
HibernateUtil.close(session);
}
try
{
/**
* 即使当session关闭以后,因为配置了二级缓存,而二级缓存是sessionFactory级别的,所以会从缓存中取出该数据
* 只会发出一条sql语句
*/
session = HibernateUtil.openSession();
Student stu = (Student) session.load(Student.class, 1);
System.out.println(stu.getName() + "-----------");
/**
* 因为设置了二级缓存为read-only,所以不能对其进行修改
*/
session.beginTransaction();
stu.setName("aaa");
session.getTransaction().commit();
}
catch (Exception e)
{
e.printStackTrace();
session.getTransaction().rollback();
}
finally
{
HibernateUtil.close(session);
}
}
因为二级缓存是sessionFactory级别的缓存,我们看到,在配置了二级缓存以后,当我们session关闭以后,我们再去查询对象的时候,此时hibernate首先会去二级缓存中查询是否有该对象,有就不会再发sql了
②二级缓存缓存的仅仅是对象,如果查询出来的是对象的一些属性,则不会被加到缓存中去
TestCase2:
@Test
public void testCache2()
{
Session session = null;
try
{
session = HibernateUtil.openSession();
/**
* 注意:二级缓存中缓存的仅仅是对象,而下面这里只保存了姓名和性别两个字段,所以 不会被加载到二级缓存里面
*/
List<Object[]> ls = (List<Object[]>) session
.createQuery("select stu.name, stu.sex from Student stu")
.setFirstResult(0).setMaxResults(30).list();
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
HibernateUtil.close(session);
}
try
{
/**
* 由于二级缓存缓存的是对象,所以此时会发出两条sql
*/
session = HibernateUtil.openSession();
Student stu = (Student) session.load(Student.class, 1);
System.out.println(stu);
}
catch (Exception e)
{
e.printStackTrace();
}
}
我们看到这个测试用例,如果我们只是取出对象的一些属性的话,则不会将其保存到二级缓存中去,因为二级缓存缓存的仅仅是对象。
③通过二级缓存来解决 N+1 的问题
TestCase3
@Test
public void testCache3()
{
Session session = null;
try
{
session = HibernateUtil.openSession();
/**
* 将查询出来的Student对象缓存到二级缓存中去
*/
List<Student> stus = (List<Student>) session.createQuery(
"select stu from Student stu").list();
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
HibernateUtil.close(session);
}
try
{
/**
* 由于学生的对象已经缓存在二级缓存中了,此时再使用iterate来获取对象的时候,首先会通过一条
* 取id的语句,然后在获取对象时去二级缓存中,如果发现就不会再发SQL,这样也就解决了N+1问题
* 而且内存占用也不多
*/
session = HibernateUtil.openSession();
Iterator<Student> iterator = session.createQuery("from Student")
.iterate();
for (; iterator.hasNext();)
{
Student stu = (Student) iterator.next();
System.out.println(stu.getName());
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
我们如果需要查询出两次对象的时候,可以使用二级缓存来解决N+1的问题。
④二级缓存会缓存 hql 语句吗?
TestCase4:
@Test
public void testCache4()
{
Session session = null;
try
{
session = HibernateUtil.openSession();
List<Student> ls = session.createQuery("from Student")
.setFirstResult(0).setMaxResults(50).list();
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
HibernateUtil.close(session);
}
try
{
/**
* 使用List会发出两条一模一样的sql,此时如果希望不发sql就需要使用查询缓存
*/
session = HibernateUtil.openSession();
List<Student> ls = session.createQuery("from Student")
.setFirstResult(0).setMaxResults(50).list();
Iterator<Student> stu = ls.iterator();
for(;stu.hasNext();)
{
Student student = stu.next();
System.out.println(student.getName());
}
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
HibernateUtil.close(session);
}
}
我们看到,当我们如果通过 list() 去查询两次对象时,二级缓存虽然会缓存查询出来的对象,但是我们看到发出了两条相同的查询语句,这是因为二级缓存不会缓存我们的hql查询语句,要想解决这个问题,我们就要配置我们的查询缓存了。
四、查询缓存(sessionFactory级别)
我们如果要配置查询缓存,只需要在hibernate.cfg.xml中加入一条配置即可:
<!-- 开启查询缓存 -->
<property name="hibernate.cache.use_query_cache">true</property>
然后我们如果在查询hql语句时要使用查询缓存,就需要在查询语句后面设置这样一个方法:
List<Student> ls = session.createQuery("from Student where name like ?")
.setCacheable(true) //开启查询缓存,查询缓存也是SessionFactory级别的缓存
.setParameter(0, "%王%")
.setFirstResult(0).setMaxResults(50).list();
如果是在annotation中,我们还需要在这个类上加上这样一个注解:@Cacheable
接下来我们来通过测试用例来看看我们的查询缓存
①查询缓存也是sessionFactory级别的缓存
TestCase1:
@Test
public void test2() {
Session session = null;
try {
/**
* 此时会发出一条sql取出所有的学生信息
*/
session = HibernateUtil.openSession();
List<Student> ls = session.createQuery("from Student")
.setCacheable(true) //开启查询缓存,查询缓存也是sessionFactory级别的缓存
.setFirstResult(0).setMaxResults(50).list();
Iterator<Student> stus = ls.iterator();
for(;stus.hasNext();) {
Student stu = stus.next();
System.out.println(stu.getName());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
HibernateUtil.close(session);
}
try {
/**
* 此时会发出一条sql取出所有的学生信息
*/
session = HibernateUtil.openSession();
List<Student> ls = session.createQuery("from Student")
.setCacheable(true) //开启查询缓存,查询缓存也是sessionFactory级别的缓存
.setFirstResult(0).setMaxResults(50).list();
Iterator<Student> stus = ls.iterator();
for(;stus.hasNext();) {
Student stu = stus.next();
System.out.println(stu.getName());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
HibernateUtil.close(session);
}
}
我们看到,此时如果我们发出两条相同的语句,hibernate也只会发出一条sql,因为已经开启了查询缓存了,并且查询缓存也是sessionFactory级别的
②只有当 HQL 查询语句完全相同时,连参数设置都要相同,此时查询缓存才有效
TestCase2:
@Test
public void test3() {
Session session = null;
try {
/**
* 此时会发出一条sql取出所有的学生信息
*/
session = HibernateUtil.openSession();
List<Student> ls = session.createQuery("from Student where name like ?")
.setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存
.setParameter(0, "%王%")
.setFirstResult(0).setMaxResults(50).list();
Iterator<Student> stus = ls.iterator();
for(;stus.hasNext();) {
Student stu = stus.next();
System.out.println(stu.getName());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
HibernateUtil.close(session);
}
session = null;
try {
/**
* 此时会发出一条sql取出所有的学生信息
*/
session = HibernateUtil.openSession();
/**
* 只有当HQL完全相同的时候,连参数都要相同,查询缓存才有效
*/
// List<Student> ls = session.createQuery("from Student where name like ?")
// .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存
// .setParameter(0, "%王%")
// .setFirstResult(0).setMaxResults(50).list();
List<Student> ls = session.createQuery("from Student where name like ?")
.setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存
.setParameter(0, "%张%")
.setFirstResult(0).setMaxResults(50).list();
Iterator<Student> stus = ls.iterator();
for(;stus.hasNext();) {
Student stu = stus.next();
System.out.println(stu.getName());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
HibernateUtil.close(session);
}
}
我们看到,如果我们的hql查询语句不同的话,我们的查询缓存也没有作用
③查询缓存也能引起 N+1 的问题
查询缓存也能引起 N+1 的问题,我们这里首先先将 Student 对象上的二级缓存先注释掉:
<!-- 二级缓存一般设置为只读的 --> <!-- <cache usage="read-only"/> -->
TestCase4:
@Test
public void test4() {
Session session = null;
try {
/**
* 查询缓存缓存的不是对象而是id
*/
session = HibernateUtil.openSession();
List<Student> ls = session.createQuery("from Student where name like ?")
.setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存
.setParameter(0, "%王%")
.setFirstResult(0).setMaxResults(50).list();
Iterator<Student> stus = ls.iterator();
for(;stus.hasNext();) {
Student stu = stus.next();
System.out.println(stu.getName());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
HibernateUtil.close(session);
}
session = null;
try {
/**
* 查询缓存缓存的是id,此时由于在缓存中已经存在了这样的一组学生数据,但是仅仅只是缓存了
* id,所以此处会发出大量的sql语句根据id取对象,这也是发现N+1问题的第二个原因
* 所以如果使用查询缓存必须开启二级缓存
*/
session = HibernateUtil.openSession();
List<Student> ls = session.createQuery("from Student where name like ?")
.setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存
.setParameter(0, "%王%")
.setFirstResult(0).setMaxResults(50).list();
Iterator<Student> stus = ls.iterator();
for(;stus.hasNext();) {
Student stu = stus.next();
System.out.println(stu.getName());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
HibernateUtil.close(session);
}
}
我们看到,当我们将二级缓存注释掉以后,在使用查询缓存时,也会出现 N+1 的问题,为什么呢?
因为查询缓存缓存的也仅仅是对象的id,所以第一条 sql 也是将对象的id都查询出来,但是当我们后面如果要得到每个对象的信息的时候,此时又会发sql语句去查询,所以,如果要使用查询缓存,我们一定也要开启我们的二级缓存,这样就不会出现 N+1 问题了