Hibernate 性能之緩存
1. 前言
本節(jié)課程和大家一起聊聊性能優(yōu)化方案之:緩存。通過本節(jié)課程學(xué)習(xí),你將了解到:
- 什么是緩存,緩存的作用;
- HIbernate 中的緩存級(jí)別;
- 如何使用緩存。
2. 緩存
2.1 緩存是什么
現(xiàn)實(shí)世界里,緩存是一個(gè)無處不在的概念。
家里的米桶中都會(huì)儲(chǔ)存大米,需要下鍋時(shí),直接從米桶里拿出來,而不是等米下鍋時(shí)去商店采購。只有等到米桶中沒有米時(shí),才會(huì)去商店。
米桶就是一個(gè)類似于緩存的存儲(chǔ)體,它的作用是用來緩存大米。
程序中,通俗講,緩存就是一個(gè)用來臨時(shí)存儲(chǔ)數(shù)據(jù)的地方,便于需要時(shí)伸手便可拿到。
更專業(yè)上講,緩存可以在兩個(gè)速度不匹配的設(shè)備之間建立一個(gè)緩沖帶,適配兩者速度。
2.2 Hibernate 中的為什么需要緩存
要搞清楚 Hibernate 為什么需要緩存,那就要了解 Hibernate 使用緩存做什么?
Hibernate 的任務(wù)是幫助開發(fā)者發(fā)送 SQL 語句,從數(shù)據(jù)庫中獲取數(shù)據(jù)。
這個(gè)過程并不輕松。從微觀角度上講,Hibernate 要背上行李,通過縱橫交織的網(wǎng)絡(luò)交通,到達(dá)數(shù)據(jù)庫服務(wù)器,獲取數(shù)據(jù)。然后背起數(shù)據(jù),繼續(xù)行走在四通八達(dá)的網(wǎng)絡(luò)交通,回到程序中。
運(yùn)氣不好時(shí),碰到網(wǎng)絡(luò)擁堵,就會(huì)產(chǎn)生延遲,遇到網(wǎng)絡(luò)斷線,則會(huì)丟失數(shù)據(jù)。
理論上講,對(duì)于每次的數(shù)據(jù)請(qǐng)求,這個(gè)過程都是必須的。
但是,如果多次的請(qǐng)求是同樣數(shù)據(jù)的時(shí)候,也就是用戶的請(qǐng)求 SQL 是一樣的時(shí)候,有必要這么不停地來往于數(shù)據(jù)庫服務(wù)器嗎?
面對(duì)這種情況,Hibernate 提供的緩存就起作用了,可以緩存曾經(jīng)從數(shù)據(jù)庫中獲取過的數(shù)據(jù)。如果下次再需要時(shí),只需要從緩存中獲取,而無需翻山涉水,通過網(wǎng)絡(luò)獲取。
Hibernate 的緩存主要是存儲(chǔ)曾經(jīng)操作過的數(shù)據(jù),程序邏輯向 Hibernate 發(fā)送數(shù)據(jù)請(qǐng)求操作時(shí),Hibernate 會(huì)先查詢緩存中有沒有,如果存在,則直接從緩存中獲取,沒有時(shí),才會(huì)行走于網(wǎng)絡(luò)通道,從數(shù)據(jù)庫中獲取。
3. Session 緩存
Hibernate 提供有一級(jí)和二級(jí)緩存,一級(jí)緩存也叫 Session 緩存,二級(jí)緩存也叫 SessionFactory 緩存。
前面課程中和大家聊過,Session 的使用原則是,需要時(shí)創(chuàng)建,用完后關(guān)閉,其作用域一般為方法級(jí)別。
一級(jí)緩存的生命周期和 Session 是一致的,所以,一級(jí)緩存中所存儲(chǔ)的數(shù)據(jù)其生命周期也不長,其實(shí)際意義就論情況來看了。
SessionFactory 在前面也討論過,SessionFactory 是應(yīng)用程序級(jí)別的生命周期,所以與其關(guān)聯(lián)的緩存中所保存的數(shù)據(jù)也可以長時(shí)間存在。
默認(rèn)情況下,Hibernate 的一級(jí)緩存是可以直接使用的,二級(jí)緩存是沒有打開的。需要根據(jù)實(shí)際情況進(jìn)行選擇。
驗(yàn)證一級(jí)緩存
需求:在 Session 關(guān)閉之前,連續(xù)查詢相同的學(xué)生兩次。
Session session = sessionFactory.openSession();
Transaction transaction = null;
Student stu = null;
try {
transaction = session.beginTransaction();
stu = (Student) session.get(Student.class, new Integer(1));
System.out.println(stu.getStuName());
// 查詢前面查詢過的學(xué)生
System.out.println("--------------第二次查詢------------------");
stu = (Student) session.get(Student.class, new Integer(1));
System.out.println(stu.getStuName());
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
運(yùn)行結(jié)果如下:
Hibernate:
select
student0_.stuId as stuId1_3_0_,
student0_.classRoomId as classRoo6_3_0_,
student0_.stuName as stuName2_3_0_,
student0_.stuPassword as stuPassw3_3_0_,
student0_.stuPic as stuPic4_3_0_,
student0_.stuSex as stuSex5_3_0_
from
Student student0_
where
student0_.stuId=?
Hibernate
--------------第二次查詢------------------
Hibernate
從輸出結(jié)果中能得到什么結(jié)論?
只有在第一次查詢的時(shí)候,Hibernate 才會(huì)向數(shù)據(jù)庫發(fā)送 SQL 語句請(qǐng)求,第二查詢時(shí),不需要再發(fā)送 SQL 請(qǐng)求,因?yàn)榫彺嬷幸呀?jīng)存在。
稍微改動(dòng)一下上述實(shí)例,創(chuàng)建兩個(gè) Session 對(duì)象,用來查詢同一個(gè)學(xué)生:
Session session = sessionFactory.openSession();
Transaction transaction = null;
Student stu = null;
try {
transaction = session.beginTransaction();
System.out.println("--------------第一次查詢------------------");
stu = (Student) session.get(Student.class, new Integer(1));
System.out.println(stu.getStuName());
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
session = sessionFactory.openSession();
try {
transaction = session.beginTransaction();
// 查詢前面查詢過的學(xué)生
System.out.println("--------------第二次查詢------------------");
stu = (Student) session.get(Student.class, new Integer(1));
System.out.println(stu.getStuName());
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
查看控制臺(tái)上的輸出結(jié)果:
Hibernate:
select
student0_.stuId as stuId1_3_0_,
student0_.classRoomId as classRoo6_3_0_,
student0_.stuName as stuName2_3_0_,
student0_.stuPassword as stuPassw3_3_0_,
student0_.stuPic as stuPic4_3_0_,
student0_.stuSex as stuSex5_3_0_
from
Student student0_
where
student0_.stuId=?
Hibernate
--------------第二次查詢------------------
Hibernate:
select
student0_.stuId as stuId1_3_0_,
student0_.classRoomId as classRoo6_3_0_,
student0_.stuName as stuName2_3_0_,
student0_.stuPassword as stuPassw3_3_0_,
student0_.stuPic as stuPic4_3_0_,
student0_.stuSex as stuSex5_3_0_
from
Student student0_
where
student0_.stuId=?
Hibernate
每次查詢都會(huì)發(fā)送 SQL 請(qǐng)求,這是因?yàn)?Session 緩存中的數(shù)據(jù)只能提供給本 Session 對(duì)象使用。不能跨 Session 使用。
- 當(dāng)調(diào)用 save ()、update () 或 saveOrUpdate () 方法傳遞一個(gè)對(duì)象時(shí),或使用 load ()、 get ()、list ()、iterate () 方法獲得一個(gè)對(duì)象時(shí),該對(duì)象都將被加入到 Session 的內(nèi)部緩存中;
- 可以通過調(diào)用 close()、clear()、evict() 方法手工清空緩存中的數(shù)據(jù)。
前面說過的,Session 生命周期很短,與 Session 關(guān)聯(lián)的一級(jí)緩存的生命周期也很短,所以緩存的命中率是很低的。其對(duì)系統(tǒng)性能的改善也有限得很。Session 內(nèi)部緩存的主要作用是保持 Session 內(nèi)部數(shù)據(jù)狀態(tài)同步。
4. SessionFactory 緩存
SessionFactory 緩存也稱其為二級(jí)緩存,是應(yīng)用程序級(jí)別的緩存。二級(jí)緩存在默認(rèn)情況下是沒有啟動(dòng)的,如果開發(fā)者想使用二級(jí)緩存所提供的功能,則需要通過一系列的操作流程方能讓其現(xiàn)身。
Hibernate 本身也提供有二級(jí)緩存的功能模塊,但只建議用于測試或?qū)W習(xí)過程。對(duì)于生產(chǎn)環(huán)境,Hibernae 建議使用專業(yè)的第三方緩存框架,如 EhCache 緩存框架。
常用緩存框架:
- EhCache;
- OSCache;
- SwarmCache;
- JBossCache。
啟動(dòng)二級(jí)緩存
- 在 Hibernate 的主配置文件中啟動(dòng)并指定二級(jí)緩存的實(shí)現(xiàn)者;
<property name="cache.use_structured_entries">true</property>
<property name="cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
- 添加 EhCache 相關(guān)的 JAR 包。這些 JAR 包都可以在下載的 Hibernate 框架包的 lib 文件夾中找到;
- ehcache-core-2.4.3.jar;
- hibernate-ehcache-4.2.0.Final.jar。
- 在項(xiàng)目的 src 中添加 EhCache 緩存框架的配置文件 ehcache.xml;
這個(gè)配置文件可以在下載的 Hibernate 框架包中的 project 目錄下的 etc 中找到。此配置文件中的內(nèi)容用來配置緩存管理相關(guān)信息。
<ehcache>
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
</ehcache>
配置說明:
- maxElementsInMemory: 緩存最大數(shù)目;
- eternal : 緩存是否持久;
- overflowToDisk : 是否保存到磁盤,當(dāng)系統(tǒng)當(dāng)機(jī)時(shí);
- timeToIdleSeconds : 當(dāng)緩存閑置 n 秒后銷毀;
- timeToLiveSeconds : 當(dāng)緩存存活 n 秒后銷毀。
- 在需要緩存的實(shí)體類上添加 @cache 注解
@Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL,include="all",region="student")
只有被 @Cache 注解的實(shí)體才會(huì)被存儲(chǔ)進(jìn)二級(jí)緩存中,此注解有一個(gè) usage 屬性,用來配置緩存的策略,是一個(gè)枚舉類型,有如下幾種選擇:
- CacheConcurrencyStrategy.NONE;
- CacheConcurrencyStrategy.NONSTRICT_READ_WRITE: 非嚴(yán)格讀寫緩存;
- CacheConcurrencyStrategy.READ_ONLY: 只讀緩存;
- CacheConcurrencyStrategy.READ_WRITE: 讀寫緩存;
- CacheConcurrencyStrategy.TRANSACTIONAL: 事務(wù)緩存。
Region 指定二級(jí)緩存中的區(qū)域名,默認(rèn)為類或者集合的名字。
include 有幾個(gè)選項(xiàng),non-lazy 當(dāng)屬性延遲抓取打開時(shí),標(biāo)記為 lazy=“true” 的實(shí)體的屬性可能無法被緩存。
做完上面的事情后,再執(zhí)行前面的兩個(gè) Session 對(duì)象查詢同一個(gè)學(xué)生的代碼,再查看控制臺(tái)上的信息:
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
--------------第二次查詢------------------
學(xué)生姓名:Hibernate
第一次查詢時(shí),需要發(fā)送 SQL 請(qǐng)求,第二查詢時(shí),不再發(fā)送 SQL 請(qǐng)求,因?yàn)椴樵冞^的信息已經(jīng)被存儲(chǔ)在了二級(jí)緩存中,Hibernate 會(huì)直接從緩存查詢。
二級(jí)緩存并不支持緩存 Blob 類型的數(shù)據(jù)。
5. 小結(jié)
本節(jié)課和大家一起了解了 Hibernate 提供的緩存機(jī)制,Hibernate 提供了一級(jí)緩存和二級(jí)緩存。一級(jí)緩存因生命周期較短,主要用于內(nèi)部服務(wù)。二級(jí)緩存因生命周期較長,命中率會(huì)較高,緩存中一般存放經(jīng)常被訪問、改動(dòng)不頻繁、數(shù)量有限的數(shù)據(jù)。
有了緩存機(jī)制的加持,HIbernate 在響應(yīng)開發(fā)者的請(qǐng)求時(shí),又會(huì)少了許多延遲。速度對(duì)于程序來講,是一個(gè)重要的性能指標(biāo)。