Hibernate 性能之緩存與緩存算法
1. 前言
本節(jié)課和大家一起聊聊查詢緩存和緩存算法。
對于緩存的使用要有針對性,不能濫用緩存,因為緩存本身是需要占用系統(tǒng)資源的,緩存的維護(hù)也需要消耗系統(tǒng)性能。
所以,這個世界是平衡的!如何掌握平衡,多用心感悟!
通過本節(jié)課程的學(xué)習(xí),你將了解到:
- 什么是查詢緩存,如何使用查詢緩存;
- 常用的緩存算法有哪些。
2. list()和 iterate()
在前面的課程里,咱們一起講解過 Query 對象,它提供了 list() 方法,此方法能接受 HQL 語句,查詢出開發(fā)者所需要的數(shù)據(jù)。
那么 list() 方法支持緩存嗎?也就是說 list() 方法查詢出來的數(shù)據(jù)會存儲到緩存中嗎?
本節(jié)課程中的緩存都是指二級緩存。
問題出來了,要找到答案很簡單,編寫一個實例,測試一下便知道結(jié)果 。創(chuàng)建 2 個 Session 對象,分別對同一個 HQL 語句進(jìn)行查詢:
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();
}
查看控制臺上的輸出結(jié)果:
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
從結(jié)果上可以看出,兩次查詢的 HQL 請求是相同的,但每一次都會重新發(fā)送 SQL 語句,是不是就得出結(jié)論,list() 方法與緩存無緣分呢?
結(jié)論可不要提出來的太早。
Query 還提供了一個方法 iterate(),從功能上做比較,和 list() 沒有多大區(qū)別,只是一個返回的是集合對象,一個返回的是迭代器對象,作用是一樣的。
但是不是就沒有其它的區(qū)別了?
不急,先了解一下 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("-------------------輸出結(jié)果------------------");
System.out.println("學(xué)生姓名:"+stu.getStuName());
}
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
截取運行后的一部分控制臺上的內(nèi)容展示如下:
------------------迭代查詢-------------------
Hibernate:
select
student0_.stuId as col_0_0_
from
Student student0_
-------------------輸出結(jié)果------------------
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=?
學(xué)生姓名:Hibernate
當(dāng)我們執(zhí)行 iterate() 方法時,Hibernate 只是把所有的學(xué)生編號(主鍵)返回給應(yīng)用程序。也就是說并沒有返回完整的學(xué)生信息。
它為什么要這么做了?
首先有一點是可以得出結(jié)論的,僅僅得到學(xué)生編號肯定比獲取全部學(xué)生信息是要快很多的。
當(dāng)程序中需要學(xué)生其它數(shù)據(jù)的時候,這時 Hibernate 又會跑一次數(shù)據(jù)庫,根據(jù)前面獲取到的學(xué)生編號構(gòu)建新的條件查詢,從數(shù)據(jù)庫中再次獲取數(shù)據(jù)。
天呀,真不閑累的慌。
為什么要這么做了?
這有點類似于延遲加載,很多時候,程序中并不急著使用數(shù)據(jù),可能需要等某些依賴的邏輯成立后再使用。如此,iterate() 方法可快速獲取主鍵值,并安慰開發(fā)者,你看,我是有能力幫你獲取數(shù)據(jù)的。等需要更多時,我也是有能力拿到的。
Query 既提供 list() 方法,又提供 iterate() 方法不是沒有出發(fā)點的。這兩個方法很多時候結(jié)合起來使用,可以達(dá)到一種神奇的效果。
什么效果呢?
看一段實例:
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("-------------------輸出結(jié)果------------------");
System.out.println("學(xué)生姓名:" + stu.getStuName());
}
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
兩者結(jié)合,交織中所碰觸出的火花,你 get 到了嗎?
先使用 list() 方法查詢出所有學(xué)生信息, hibernate 會把 list() 方法查詢出來的數(shù)據(jù)全部存儲到緩存中。但是,它自己不使用緩存中自己緩存的數(shù)據(jù),它是勤勞的小蜜蜂,無私的奉獻(xiàn)。
誰會使用 list() 緩存的數(shù)據(jù)了?
輸出結(jié)果已經(jīng)告訴了我們答案,iterate() 方法會使用 list() 方法緩存的數(shù)據(jù)。
對于一條查詢語句,Iterator 會先從數(shù)據(jù)庫中找到所有符合條件的記錄的主鍵 ID,再通過主鍵 ID 去緩存找,對于緩存中沒有的記錄,再構(gòu)造語句從數(shù)據(jù)中查出,在緩存中沒有命中的話,效率很低。
那么,怎么聯(lián)合使用了?
建議在應(yīng)用程序啟動或修改時使用 list,通過 list 緩存數(shù)據(jù)。需要更多數(shù)據(jù)時再使用 iterator。
好兄弟,一輩子,江湖上,有你也有我。
3. 查詢緩存
是不是 list() 方法真的就不能使用緩存,而只是作為 iterator() 身后的兄弟。
Hibernate 中提供的有查詢緩存的概念。查詢緩存只對 query.list() 方法起作用。查詢緩存依賴于二級緩存,因此一定要打開二級緩存。而且,在默認(rèn)情況下,查詢緩存也是關(guān)閉的。
啟動查詢緩存
- 在 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();
}
查看一下控制臺上的輸出結(jié)果:
------------------第一次查詢-------------------
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
結(jié)論很明顯,第一次使用 list() 方法時,需要發(fā)送 SQL 語句,第二次時,就不再需要了,也就是說 list() 也是可以享受自己緩存的數(shù)據(jù)。但是必須啟動查詢緩存,且在代碼中明明確確指示出來。
4. 緩存算法
什么是緩存算法?
緩存是一個臨時存儲數(shù)據(jù)的地方,但是,這個地方可金貴的很,咱們可不能讓那些不經(jīng)常使用的、過期的數(shù)據(jù)長時間待在里面。所以,必須有一種機(jī)制能隨時檢查一下緩存中的數(shù)據(jù),哪些數(shù)據(jù)是可以繼續(xù)待在里面的,哪些數(shù)據(jù)需要移出去,給新來者挪出空間的,這就是所謂的緩存算法。
常用的緩存算法:
- LRU : Least Recently Used ,最近最少被使用的,每個緩存對象都記錄一個最后使用時間;
- LFU : Least Frequently Used ,最近使用頻率最少;
- FIFO: First in First Out ,這個簡單,定時清理時,先來的,先離開。
Session 和 SessionFactory 對象也提供的有與緩存管理有關(guān)的方法,方便開發(fā)者可以隨時按需清除緩存。如 evict() 等方法。
上一節(jié)課介紹 EHCache 緩存框架時,就要使用它的配置文件,其配置內(nèi)容就是設(shè)置如何管理緩存。
5. 小結(jié)
好了!又到了說再見的時候了,本節(jié)課繼續(xù)上一節(jié)的內(nèi)容,向大家介紹了查詢緩存,主要介紹了 Query 對象的 list 和 iterate 兩個方法,它們有各自的特點,也有各自調(diào)用的時機(jī)點。
聯(lián)合使用兩者,能更充分的發(fā)揮緩存的功效。
后面也給大家介紹了緩存算法,大家需要把此內(nèi)容當(dāng)成常識性知識。