Hibernate 性能之缓存与缓存算法

1. 前言

本节课和大家一起聊聊查询缓存和缓存算法。

对于缓存的使用要有针对性,不能滥用缓存,因为缓存本身是需要占用系统资源的,缓存的维护也需要消耗系统性能。

所以,这个世界是平衡的!如何掌握平衡,多用心感悟!

通过本节课程的学习,你将了解到:

  • 什么是查询缓存,如何使用查询缓存;
  • 常用的缓存算法有哪些。

2. list()和 iterate()

在前面的课程里,咱们一起讲解过 Query 对象,它提供了 list() 方法,此方法能接受 HQL 语句,查询出开发者所需要的数据。

那么 list() 方法支持缓存吗?也就是说 list() 方法查询出来的数据会存储到缓存中吗?

本节课程中的缓存都是指二级缓存。

问题出来了,要找到答案很简单,编写一个实例,测试一下便知道结果 。创建 2 个 Session 对象,分别对同一个 HQL 语句进行查询:

Session session = sessionFactory.openSession();
Transaction transaction = null;
try {
	transaction = session.beginTransaction();
	String hql = "from Student s";
	Query query = session.createQuery(hql);
	System.out.println("------------------第一次查询-------------------");
	List<Student> stus = query.list();
	System.out.println(stus.size());
	transaction.commit();
	} catch (Exception e) {
		transaction.rollback();
	} finally {
		session.close();
	}
	
session = sessionFactory.openSession();
transaction = null;
try {
	transaction = session.beginTransaction();
	String hql = "from Student s";
	Query query = session.createQuery(hql);
	System.out.println("-----------------第二次查询--------------------");
	List<Student> stus = query.list();
	System.out.println(stus.size());
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}

查看控制台上的输出结果:

Hibernate: 
    select
        student0_.stuId as stuId1_1_,
        student0_.classRoomId as classRoo5_1_,
        student0_.stuName as stuName2_1_,
        student0_.stuPassword as stuPassw3_1_,
        student0_.stuSex as stuSex4_1_ 
    from
        Student student0_
4
-----------------第二次查询--------------------
Hibernate: 
    select
        student0_.stuId as stuId1_1_,
        student0_.classRoomId as classRoo5_1_,
        student0_.stuName as stuName2_1_,
        student0_.stuPassword as stuPassw3_1_,
        student0_.stuSex as stuSex4_1_ 
    from
        Student student0_
4

从结果上可以看出,两次查询的 HQL 请求是相同的,但每一次都会重新发送 SQL 语句,是不是就得出结论,list() 方法与缓存无缘分呢?

结论可不要提出来的太早。

Query 还提供了一个方法 iterate(),从功能上做比较,和 list() 没有多大区别,只是一个返回的是集合对象,一个返回的是迭代器对象,作用是一样的。

但是不是就没有其它的区别了?

不急,先了解一下 iterate() 方法的特点,用实例来说话:

Session session = sessionFactory.openSession();
Transaction transaction = null;
try {
	transaction = session.beginTransaction();
	String hql = "from Student s";
	Query query = session.createQuery(hql);
	System.out.println("------------------迭代查询-------------------");
	Iterator<Student> stus = query.iterate();
			
	while(stus.hasNext()) {
	    Student stu=	stus.next();
	    System.out.println("-------------------输出结果------------------");
	    System.out.println("学生姓名:"+stu.getStuName());
	}	
		transaction.commit();
	} catch (Exception e) {
		transaction.rollback();
	} finally {
		session.close();
	}

截取运行后的一部分控制台上的内容展示如下:

------------------迭代查询-------------------
Hibernate: 
    select
        student0_.stuId as col_0_0_ 
    from
        Student student0_
-------------------输出结果------------------
Hibernate: 
    select
        student0_.stuId as stuId1_1_0_,
        student0_.classRoomId as classRoo5_1_0_,
        student0_.stuName as stuName2_1_0_,
        student0_.stuPassword as stuPassw3_1_0_,
        student0_.stuSex as stuSex4_1_0_ 
    from
        Student student0_ 
    where
        student0_.stuId=?
学生姓名:Hibernate

当我们执行 iterate() 方法时,Hibernate 只是把所有的学生编号(主键)返回给应用程序。也就是说并没有返回完整的学生信息。

它为什么要这么做了?

首先有一点是可以得出结论的,仅仅得到学生编号肯定比获取全部学生信息是要快很多的。

当程序中需要学生其它数据的时候,这时 Hibernate 又会跑一次数据库,根据前面获取到的学生编号构建新的条件查询,从数据库中再次获取数据。

天呀,真不闲累的慌。

为什么要这么做了?

这有点类似于延迟加载,很多时候,程序中并不急着使用数据,可能需要等某些依赖的逻辑成立后再使用。如此,iterate() 方法可快速获取主键值,并安慰开发者,你看,我是有能力帮你获取数据的。等需要更多时,我也是有能力拿到的。

Query 既提供 list() 方法,又提供 iterate() 方法不是没有出发点的。这两个方法很多时候结合起来使用,可以达到一种神奇的效果。

什么效果呢?

看一段实例:

Session session = sessionFactory.openSession();
Transaction transaction = null;
try {
	transaction = session.beginTransaction();
	String hql = "from Student s";
	Query query = session.createQuery(hql);
	System.out.println("------------------第一次使用 list()方法查询-------------------");
	List<Student> stus = query.list();
	System.out.println(stus.size());
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}
session = sessionFactory.openSession();
transaction = null;
try {
	transaction = session.beginTransaction();
	String hql = "from Student s";
	Query query = session.createQuery(hql);
	System.out.println("-----------------第二次使用iterate()方法查询--------------------");
	Iterator<Student> stus = query.iterate();
	while (stus.hasNext()) {
			Student stu = stus.next();
			System.out.println("-------------------输出结果------------------");
			System.out.println("学生姓名:" + stu.getStuName());
	}
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}

两者结合,交织中所碰触出的火花,你 get 到了吗?

先使用 list() 方法查询出所有学生信息, hibernate 会把 list() 方法查询出来的数据全部存储到缓存中。但是,它自己不使用缓存中自己缓存的数据,它是勤劳的小蜜蜂,无私的奉献。

谁会使用 list() 缓存的数据了?

输出结果已经告诉了我们答案,iterate() 方法会使用 list() 方法缓存的数据。

对于一条查询语句,Iterator 会先从数据库中找到所有符合条件的记录的主键 ID,再通过主键 ID 去缓存找,对于缓存中没有的记录,再构造语句从数据中查出,在缓存中没有命中的话,效率很低。

那么,怎么联合使用了?

建议在应用程序启动或修改时使用 list,通过 list 缓存数据。需要更多数据时再使用 iterator

好兄弟,一辈子,江湖上,有你也有我。

3. 查询缓存

是不是 list() 方法真的就不能使用缓存,而只是作为 iterator() 身后的兄弟。

Hibernate 中提供的有查询缓存的概念。查询缓存只对 query.list() 方法起作用。查询缓存依赖于二级缓存,因此一定要打开二级缓存。而且,在默认情况下,查询缓存也是关闭的。

启动查询缓存

  1. 在 Hibernate 的主配置文件中添加如下配置信息:
<property name="cache.use_query_cache">true</property>

切记,使用查询缓存是一定要加入下面的代码:

query.setCacheable(true);

好吧,来一个实例,看看查询缓存的威力。

Session session = sessionFactory.openSession();
Transaction transaction = null;
try {
	transaction = session.beginTransaction();
	String hql = "from Student s";
	Query query = session.createQuery(hql);
	query.setCacheable(true);
	System.out.println("------------------第一次查询-------------------");
	List<Student> stus = query.list();
	System.out.println(stus.size());
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}

session = sessionFactory.openSession();
transaction = null;
try {
	transaction = session.beginTransaction();
	String hql = "from Student s";
	Query query = session.createQuery(hql);
	query.setCacheable(true);
	System.out.println("-----------------第二次查询--------------------");
	List<Student> stus = query.list();
	System.out.println(stus.size());
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
} finally {
	session.close();
}

查看一下控制台上的输出结果:

------------------第一次查询-------------------
Hibernate: 
    select
        student0_.stuId as stuId1_1_,
        student0_.classRoomId as classRoo5_1_,
        student0_.stuName as stuName2_1_,
        student0_.stuPassword as stuPassw3_1_,
        student0_.stuSex as stuSex4_1_ 
    from
        Student student0_
4
-----------------第二次查询--------------------
4

结论很明显,第一次使用 list() 方法时,需要发送 SQL 语句,第二次时,就不再需要了,也就是说 list() 也是可以享受自己缓存的数据。但是必须启动查询缓存,且在代码中明明确确指示出来。

4. 缓存算法

什么是缓存算法?

缓存是一个临时存储数据的地方,但是,这个地方可金贵的很,咱们可不能让那些不经常使用的、过期的数据长时间待在里面。所以,必须有一种机制能随时检查一下缓存中的数据,哪些数据是可以继续待在里面的,哪些数据需要移出去,给新来者挪出空间的,这就是所谓的缓存算法。

常用的缓存算法:

  • LRU : Least Recently Used ,最近最少被使用的,每个缓存对象都记录一个最后使用时间;
  • LFU : Least Frequently Used ,最近使用频率最少;
  • FIFO: First in First Out ,这个简单,定时清理时,先来的,先离开。

SessionSessionFactory 对象也提供的有与缓存管理有关的方法,方便开发者可以随时按需清除缓存。如 evict() 等方法。
上一节课介绍 EHCache 缓存框架时,就要使用它的配置文件,其配置内容就是设置如何管理缓存。

5. 小结

好了!又到了说再见的时候了,本节课继续上一节的内容,向大家介绍了查询缓存,主要介绍了 Query 对象的 listiterate 两个方法,它们有各自的特点,也有各自调用的时机点。

联合使用两者,能更充分的发挥缓存的功效。

后面也给大家介绍了缓存算法,大家需要把此内容当成常识性知识。