Hibernate中會經(jīng)常用到set等集合來表示1-N的關(guān)系。比如,我有Customer和Order兩個對象。其中,在Customer中有一個Order的set集合,表示在一個顧客可以擁有多個Order,而在Order對象中存在了一個Customer的對象,表示這個Order是哪個顧客下的單。這個算是比較典型的雙向1-N關(guān)聯(lián)。
這給我們帶來了很大的好處,當我得到了Customer對象的時候,我們可以很方便的將與其相關(guān)聯(lián)的Order集合查詢出來,這也非常符合我們的實際業(yè)務(wù),畢竟我們不可能給這個Cutomer對象別人的Order吧,這既不安全,而且對Customer的普通顧客來說,并無卵用。所以我們不得不說Hibernate的ORM做的很好,但凡事都有但是(要是沒有但是,也就沒有寫這篇文章的必要了)。
我們再對數(shù)據(jù)庫進行訪問的時候必須要考慮性能問題(通俗點講,就是用少發(fā)SQL語句),當我們設(shè)定了1-N這種關(guān)系后,查詢過程中就有可能出現(xiàn)N+1問題。
關(guān)于N+1問題,并不是本文的重點。但關(guān)于N+1問題,我們需要知道的是,這個問題會導(dǎo)致SQL語句的增加,也就是要與數(shù)據(jù)庫進行更多的交互,這無疑會給項目以及后臺數(shù)據(jù)庫帶來影響。
Hibernate是一個持久化框架,經(jīng)常需要訪問數(shù)據(jù)庫。如果我們能夠降低應(yīng)用程序?qū)ξ锢頂?shù)據(jù)庫訪問的頻次,那會提供應(yīng)用程序的運行性能。緩存內(nèi)的數(shù)據(jù)是對物理數(shù)據(jù)源中的數(shù)據(jù)的復(fù)制,應(yīng)用程序運行時先從緩存中讀寫數(shù)據(jù)。
緩存就是數(shù)據(jù)庫數(shù)據(jù)在內(nèi)存中的臨時容器,包括數(shù)據(jù)庫數(shù)據(jù)在內(nèi)存中的臨時拷貝,它位于數(shù)據(jù)庫與數(shù)據(jù)庫訪問層中間。ORM在查詢數(shù)據(jù)時首先會根據(jù)自身的緩存管理策略,在緩存中查找相關(guān)數(shù)據(jù),如發(fā)現(xiàn)所需的數(shù)據(jù),則直接將此數(shù)據(jù)作為結(jié)果加以利用,從而避免了數(shù)據(jù)庫調(diào)用性能的開銷。而相對內(nèi)存操作而言,數(shù)據(jù)庫調(diào)用是一個代價高昂的過程。
Hibernate緩存包括兩大類:一級緩存和二級緩存。
那么什么樣的數(shù)據(jù)適合放入到緩存中?
什么樣的數(shù)據(jù)不適合放入到緩存中?
首先看一個非常簡單的例子:
@Test
public void test() {
Customer customer1 = (Customer) session.load(Customer.class, 1);
System.out.println(customer1.getCustomerName());
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
看一下控制臺的輸出:
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
Customer1
我們可以看到,雖然我們調(diào)用了兩次session的load方法,但實際上只發(fā)送了一條SQL語句。我們第一次調(diào)用load方法時候,得到了查詢結(jié)果,然后將結(jié)果放到了session的一級緩存中。此時,當我們再次調(diào)用load方法,會首先去看緩存中是否存在該對象,如果存在,則直接從緩存中取出,就不會在發(fā)送SQL語句了。
但是,我們看一下下面這個例子:
@Test
public void test() {
Customer customer1 = (Customer) session.load(Customer.class, 1);
System.out.println(customer1.getCustomerName());
transaction.commit();
session.close();
session = sessionFactory.openSession();
transaction = session.beginTransaction();
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
我們解釋一下上面的代碼,在第5、6、7、8行,我們是先將session關(guān)閉,然后又重新打開了新的session,這個時候,我們再看一下控制臺的輸出結(jié)果:
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
我們可以看到,發(fā)送了兩條SQL語句。其原因是:Hibernate一級緩存是session級別的,所以如果session關(guān)閉后,緩存就沒了,當我們再次打開session的時候,緩存中是沒有了之前查詢的對象的,所以會再次發(fā)送SQL語句。
我們稍微對一級緩存的知識點進行總結(jié)一下,然后再開始討論關(guān)于二級緩存的內(nèi)容。
Session的緩存有三大作用:
@Test
public void test() {
List<Customer> customers = session.createQuery("select c.customerName from Customer c").list();
System.out.println(customers.size());
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
我們首先是只取出Customer的name屬性,然后又嘗試著去Load一個Customer對象,看一下控制臺的輸出:
Hibernate:
select
customer0_.CUSTOMER_NAME as col_0_0_
from
CUSTOMERS customer0_
3
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CUSTOMER_ID=?
Customer1
這一點其實很好理解,我本身就沒有查處Customer的所有屬性,那我又怎么能給你把所有屬性都緩存到這個對象中呢?
我們在講之前的例子中,提到我們關(guān)閉session再打開,這個時候一級緩存就不存在了,所以我們再次查詢的時候,會再次發(fā)送SQL語句。那么如果要解決這個問題,我們該怎么做?二級緩存可以幫我們解決這個問題。
Hibernate中沒有自己去實現(xiàn)二級緩存,而是利用第三方的。簡單敘述一下配置過程,也作為自己以后用到的時候配置的一個參考。
1、我們需要加入額外的二級緩存包,例如EHcache,將其包導(dǎo)入。需要:ehcache-core-2.4.3.jar , hibernate-ehcache-4.2.4.Final.jar ,slf4j-api-1.6.1.jar 2、在hibernate.cfg.xml配置文件中配置我們二級緩存的一些屬性(此處針對的是Hibernate4):
<!-- 啟用二級緩存 -->
<property name="cache.use_second_level_cache">true</property>
<!-- 配置使用的二級緩存的產(chǎn)品 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
3、我們使用的是EHcache,所以我們需要創(chuàng)建一個ehcache.xml的配置文件,來配置我們的緩存信息,這個是EHcache要求的。該文件放到根目錄下。
<ehcache>
<!--
指定一個目錄:當 EHCache 把數(shù)據(jù)寫到硬盤上時, 將把數(shù)據(jù)寫到這個目錄下.
-->
<diskStore path="d:\\tempDirectory"/>
<!--Default Cache configuration. These will applied to caches programmatically created through
the CacheManager.
The following attributes are required for defaultCache:
maxInMemory - Sets the maximum number of objects that will be created in memory
eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the element
is never expired.
timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
if the element is not eternal. Idle time is now - last accessed time
timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
if the element is not eternal. TTL is now - creation time
overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache
has reached the maxInMemory limit.
-->
<!--
設(shè)置緩存的默認數(shù)據(jù)過期策略
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<!--
設(shè)定具體的命名緩存的數(shù)據(jù)過期策略。每個命名緩存代表一個緩存區(qū)域
緩存區(qū)域(region):一個具有名稱的緩存塊,可以給每一個緩存塊設(shè)置不同的緩存策略。
如果沒有設(shè)置任何的緩存區(qū)域,則所有被緩存的對象,都將使用默認的緩存策略。即:<defaultCache.../>
Hibernate 在不同的緩存區(qū)域保存不同的類/集合。
對于類而言,區(qū)域的名稱是類名。如:com.atguigu.domain.Customer
對于集合而言,區(qū)域的名稱是類名加屬性名。如com.atguigu.domain.Customer.orders
-->
<!--
name: 設(shè)置緩存的名字,它的取值為類的全限定名或類的集合的名字
maxElementsInMemory: 設(shè)置基于內(nèi)存的緩存中可存放的對象最大數(shù)目
eternal: 設(shè)置對象是否為永久的, true表示永不過期,
此時將忽略timeToIdleSeconds 和 timeToLiveSeconds屬性; 默認值是false
timeToIdleSeconds:設(shè)置對象空閑最長時間,以秒為單位, 超過這個時間,對象過期。
當對象過期時,EHCache會把它從緩存中清除。如果此值為0,表示對象可以無限期地處于空閑狀態(tài)。
timeToLiveSeconds:設(shè)置對象生存最長時間,超過這個時間,對象過期。
如果此值為0,表示對象可以無限期地存在于緩存中. 該屬性值必須大于或等于 timeToIdleSeconds 屬性值
overflowToDisk:設(shè)置基于內(nèi)存的緩存中的對象數(shù)目達到上限后,是否把溢出的對象寫到基于硬盤的緩存中
-->
<cache name="com.atguigu.hibernate.entities.Employee"
maxElementsInMemory="1"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<cache name="com.atguigu.hibernate.entities.Department.emps"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/>
</ehcache>
在注釋中,有一些對變量的解釋。
4、開啟二級緩存。我們在這里使用的xml的配置方式,所以要在Customer.hbm.xml文件加一點配置信息:
<cache usage="read-only"/>
注意是在標簽內(nèi)。
如果是使用注解的方法,在要在Customer這個類中,加入@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)
這個注解。
5、下面我們再進行一下測試。還是上面的代碼:
@Test
public void test() {
Customer customer1 = (Customer) session.load(Customer.class, 1);
System.out.println(customer1.getCustomerName());
transaction.commit();
session.close();
session = sessionFactory.openSession();
transaction = session.beginTransaction();
Customer customer2 = (Customer) session.load(Customer.class, 1);
System.out.println(customer2.getCustomerName());
}
我們可以發(fā)現(xiàn)控制臺只發(fā)出了一條SQL語句。這是我們二級緩存的一個小Demo。
我們的二級緩存是sessionFactory級別的,所以當我們session關(guān)閉再打開之后,我們再去查詢對象的時候,此時Hibernate會先去二級緩存中查詢是否有該對象。
同樣,二級緩存緩存的是對象,如果我們查詢的是對象的一些屬性,則不會加入到緩存中。
我們通過二級緩存是可以解決之前提到的N+1問題。
已經(jīng)寫了這么多了,但好像我們關(guān)于緩存的內(nèi)容還沒有講完。不要著急,再堅持一下,我們的內(nèi)容不多了。我們還是通過一個例子來引出下一個話題。 我們說通過二級緩存可以緩存對象,那么我們看一下下面的代碼以及輸出結(jié)果:
@Test
public void test() {
List<Customer> customers1 = session.createQuery("from Customer").list();
System.out.println(customers1.size());
tansaction.commit();
session.close();
session = sessionFactory.openSession();
transaction = session.beginTransaction();
List<Customer> customers2 = session.createQuery("from Customer").list();
System.out.println(customers2.size());
}
控制臺的結(jié)果:
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_
from
CUSTOMERS customer0_
3
Hibernate:
select
customer0_.CUSTOMER_ID as CUSTOMER1_0_,
customer0_.CUSTOMER_NAME as CUSTOMER2_0_
from
CUSTOMERS customer0_
3
我們的緩存好像沒有起作用哎?這是為啥?當我們通過list()去查詢兩次對象的時候,二級緩存雖然會緩存插敘出來的對象,但不會緩存我們的hql查詢語句,要想解決這個問題,我們需要用到查詢緩存。
在前文中也提到了,我們的一級二級緩存都是對整個實體進行緩存,它不會緩存普通屬性,如果想對普通屬性進行緩存,則可以考慮使用查詢緩存。
但需要注意的是,大部分情況下,查詢緩存并不能提高應(yīng)用程序的性能,甚至反而會降低應(yīng)用性能,因此實際項目中要謹慎的使用查詢緩存。
對于查詢緩存來說,它緩存的key就是查詢所用的HQL或者SQL語句,需要指出的是:查詢緩存不僅要求所使用的HQL、SQL語句相同,甚至要求所傳入的參數(shù)也相同,Hibernate才能直接從緩存中取得數(shù)據(jù)。只有經(jīng)常使用相同的查詢語句、并且使用相同查詢參數(shù)才能通過查詢緩存獲得好處,查詢緩存的生命周期直到屬性被修改了為止。
查詢緩存默認是關(guān)閉。要想使用查詢緩存,只需要在hibernate.cfg.xml中加入一條配置即可:
<property name="hibernate.cache.use_query_cache">true</property>
而且,我們在查詢hql語句時,要想使用查詢緩存,就需要在語句中設(shè)置這樣一個方法:setCacheable(true)
。關(guān)于這個的demo我就不進行演示了,大家可以自己慢慢試著玩一下。
但需要注意的是,我們在開啟查詢緩存的時候,也應(yīng)該開啟二級緩存。因為如果不使用二級緩存,也有可能出現(xiàn)N+1的問題。
這是因為查詢緩存緩存的僅僅是對象的ID,所以首先會通過一條SQL將對象的ID都查詢出來,但是當我們后面要得到每個對象的信息的時候,此時又會發(fā)送SQL語句,所以如果我們使用查詢緩存,一定也要開啟二級緩存。
這些就是自己今晚上研究的關(guān)于Hibernate緩存的一些問題,其出發(fā)點也是為了自己能夠?qū)ibernate緩存的知識有一定的總結(jié)。當然了,下一步還需要深入到緩存是如何實現(xiàn)的這個深度中。
另外PS一句,最近打球打的很累,都感覺自己打的有點乏力了。休息幾天再去玩。
更多建議: