Hibernate:緩存機(jī)制的學(xué)習(xí)

2018-08-12 21:21 更新

Hibernate中會(huì)經(jīng)常用到set等集合來(lái)表示1-N的關(guān)系。比如,我有Customer和Order兩個(gè)對(duì)象。其中,在Customer中有一個(gè)Order的set集合,表示在一個(gè)顧客可以擁有多個(gè)Order,而在Order對(duì)象中存在了一個(gè)Customer的對(duì)象,表示這個(gè)Order是哪個(gè)顧客下的單。這個(gè)算是比較典型的雙向1-N關(guān)聯(lián)。

這給我們帶來(lái)了很大的好處,當(dāng)我得到了Customer對(duì)象的時(shí)候,我們可以很方便的將與其相關(guān)聯(lián)的Order集合查詢出來(lái),這也非常符合我們的實(shí)際業(yè)務(wù),畢竟我們不可能給這個(gè)Cutomer對(duì)象別人的Order吧,這既不安全,而且對(duì)Customer的普通顧客來(lái)說(shuō),并無(wú)卵用。所以我們不得不說(shuō)Hibernate的ORM做的很好,但凡事都有但是(要是沒(méi)有但是,也就沒(méi)有寫(xiě)這篇文章的必要了)。

我們?cè)賹?duì)數(shù)據(jù)庫(kù)進(jìn)行訪問(wèn)的時(shí)候必須要考慮性能問(wèn)題(通俗點(diǎn)講,就是用少發(fā)SQL語(yǔ)句),當(dāng)我們?cè)O(shè)定了1-N這種關(guān)系后,查詢過(guò)程中就有可能出現(xiàn)N+1問(wèn)題。

關(guān)于N+1問(wèn)題,并不是本文的重點(diǎn)。但關(guān)于N+1問(wèn)題,我們需要知道的是,這個(gè)問(wèn)題會(huì)導(dǎo)致SQL語(yǔ)句的增加,也就是要與數(shù)據(jù)庫(kù)進(jìn)行更多的交互,這無(wú)疑會(huì)給項(xiàng)目以及后臺(tái)數(shù)據(jù)庫(kù)帶來(lái)影響。

Hibernate緩存


Hibernate是一個(gè)持久化框架,經(jīng)常需要訪問(wèn)數(shù)據(jù)庫(kù)。如果我們能夠降低應(yīng)用程序?qū)ξ锢頂?shù)據(jù)庫(kù)訪問(wèn)的頻次,那會(huì)提供應(yīng)用程序的運(yùn)行性能。緩存內(nèi)的數(shù)據(jù)是對(duì)物理數(shù)據(jù)源中的數(shù)據(jù)的復(fù)制,應(yīng)用程序運(yùn)行時(shí)先從緩存中讀寫(xiě)數(shù)據(jù)。

緩存就是數(shù)據(jù)庫(kù)數(shù)據(jù)在內(nèi)存中的臨時(shí)容器,包括數(shù)據(jù)庫(kù)數(shù)據(jù)在內(nèi)存中的臨時(shí)拷貝,它位于數(shù)據(jù)庫(kù)與數(shù)據(jù)庫(kù)訪問(wèn)層中間。ORM在查詢數(shù)據(jù)時(shí)首先會(huì)根據(jù)自身的緩存管理策略,在緩存中查找相關(guān)數(shù)據(jù),如發(fā)現(xiàn)所需的數(shù)據(jù),則直接將此數(shù)據(jù)作為結(jié)果加以利用,從而避免了數(shù)據(jù)庫(kù)調(diào)用性能的開(kāi)銷。而相對(duì)內(nèi)存操作而言,數(shù)據(jù)庫(kù)調(diào)用是一個(gè)代價(jià)高昂的過(guò)程。

Hibernate緩存包括兩大類:一級(jí)緩存和二級(jí)緩存。

  • Hibernate一級(jí)緩存又被成為“Session的緩存”。Session緩存是內(nèi)置的,不能被卸載,是事務(wù)范圍的緩存。在一級(jí)緩存中,持久化類的每個(gè)實(shí)例都具有唯一的OID。
  • Hibernate二級(jí)緩存又被稱為“SessionFactory的緩存”。由于SessionFactory對(duì)象的生命周期和應(yīng)用程序的整個(gè)過(guò)程對(duì)應(yīng),因此Hibernate二級(jí)緩存是進(jìn)程范圍或者集群范圍的緩存,有可能出現(xiàn)并發(fā)問(wèn)題,因此需要采用適當(dāng)?shù)牟l(fā)訪問(wèn)策略,該策略為被緩存的數(shù)據(jù)提供了事務(wù)隔離級(jí)別。第二級(jí)緩存是可選的,是一個(gè)可配置的插件,默認(rèn)下SessionFactory不會(huì)啟用這個(gè)插件。

那么什么樣的數(shù)據(jù)適合放入到緩存中?

  • 很少被修改的數(shù)據(jù)   
  • 不是很重要的數(shù)據(jù),允許出現(xiàn)偶爾并發(fā)的數(shù)據(jù)   
  • 不會(huì)被并發(fā)訪問(wèn)的數(shù)據(jù)   
  • 常量數(shù)據(jù)

什么樣的數(shù)據(jù)不適合放入到緩存中? 

  • 經(jīng)常被修改的數(shù)據(jù)   
  • 絕對(duì)不允許出現(xiàn)并發(fā)訪問(wèn)的數(shù)據(jù),如財(cái)務(wù)數(shù)據(jù),絕對(duì)不允許出現(xiàn)并發(fā)   
  • 與其他應(yīng)用共享的數(shù)據(jù)

Hibernate一級(jí)緩存


Demo

首先看一個(gè)非常簡(jiǎn)單的例子:

@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());
}

看一下控制臺(tái)的輸出:

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方法,但實(shí)際上只發(fā)送了一條SQL語(yǔ)句。我們第一次調(diào)用load方法時(shí)候,得到了查詢結(jié)果,然后將結(jié)果放到了session的一級(jí)緩存中。此時(shí),當(dāng)我們?cè)俅握{(diào)用load方法,會(huì)首先去看緩存中是否存在該對(duì)象,如果存在,則直接從緩存中取出,就不會(huì)在發(fā)送SQL語(yǔ)句了。

但是,我們看一下下面這個(gè)例子:

@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)閉,然后又重新打開(kāi)了新的session,這個(gè)時(shí)候,我們?cè)倏匆幌驴刂婆_(tái)的輸出結(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語(yǔ)句。其原因是:Hibernate一級(jí)緩存是session級(jí)別的,所以如果session關(guān)閉后,緩存就沒(méi)了,當(dāng)我們?cè)俅未蜷_(kāi)session的時(shí)候,緩存中是沒(méi)有了之前查詢的對(duì)象的,所以會(huì)再次發(fā)送SQL語(yǔ)句。

我們稍微對(duì)一級(jí)緩存的知識(shí)點(diǎn)進(jìn)行總結(jié)一下,然后再開(kāi)始討論關(guān)于二級(jí)緩存的內(nèi)容。

作用

Session的緩存有三大作用:

  1. 減少訪問(wèn)數(shù)據(jù)庫(kù)的頻率。應(yīng)用程序從緩存中讀取持久化對(duì)象的速度顯然比到數(shù)據(jù)中查詢數(shù)據(jù)的速度快多了,因此Session的緩存可以提高數(shù)據(jù)訪問(wèn)的性能。
  2. 當(dāng)緩存中的持久化對(duì)象之間存在循環(huán)關(guān)聯(lián)關(guān)系時(shí),Session會(huì)保證不出現(xiàn)訪問(wèn)對(duì)象圖的死循環(huán),以及由死循環(huán)引起的JVM堆棧溢出異常。
  3. 保證數(shù)據(jù)庫(kù)中的相關(guān)記錄與緩存中的相應(yīng)對(duì)象保持同步。

小結(jié)

  • 一級(jí)緩存是事務(wù)級(jí)別的,每個(gè)事務(wù)(session)都有單獨(dú)的一級(jí)緩存。這一級(jí)別的緩存是由Hibernate進(jìn)行管理,一般情況下無(wú)需進(jìn)行干預(yù)。
  • 每個(gè)事務(wù)都擁有單獨(dú)的一級(jí)緩存不會(huì)出現(xiàn)并發(fā)問(wèn)題,因此無(wú)須提供并發(fā)訪問(wèn)策略。
  • 當(dāng)應(yīng)用程序調(diào)用Session的save()、update()、saveOrUpdate()、get()或load(),以及調(diào)用查詢接口的 list()、iterate()(該方法會(huì)出現(xiàn)N+1問(wèn)題,先查id)方法時(shí),如果在Session緩存中還不存在相應(yīng)的對(duì)象,Hibernate就會(huì)把該對(duì)象加入到第一級(jí)緩存中。當(dāng)清理緩存時(shí),Hibernate會(huì)根據(jù)緩存中對(duì)象的狀態(tài)變化來(lái)同步更新數(shù)據(jù)庫(kù)。 Session為應(yīng)用程序提供了兩個(gè)管理緩存的方法: evict(Object obj):從緩存中清除參數(shù)指定的持久化對(duì)象。 clear():清空緩存中所有持久化對(duì)象,flush():使緩存與數(shù)據(jù)庫(kù)同步。
  • 當(dāng)查詢相應(yīng)的字段,而不是對(duì)象時(shí),不支持緩存。我們可以很容易舉一個(gè)例子來(lái)說(shuō)明,看一下下面的代碼。
@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一個(gè)Customer對(duì)象,看一下控制臺(tái)的輸出:

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

這一點(diǎn)其實(shí)很好理解,我本身就沒(méi)有查處Customer的所有屬性,那我又怎么能給你把所有屬性都緩存到這個(gè)對(duì)象中呢?

我們?cè)谥v之前的例子中,提到我們關(guān)閉session再打開(kāi),這個(gè)時(shí)候一級(jí)緩存就不存在了,所以我們?cè)俅尾樵兊臅r(shí)候,會(huì)再次發(fā)送SQL語(yǔ)句。那么如果要解決這個(gè)問(wèn)題,我們?cè)撛趺醋觯慷?jí)緩存可以幫我們解決這個(gè)問(wèn)題。

Hibernate二級(jí)緩存

Hibernate中沒(méi)有自己去實(shí)現(xiàn)二級(jí)緩存,而是利用第三方的。簡(jiǎn)單敘述一下配置過(guò)程,也作為自己以后用到的時(shí)候配置的一個(gè)參考。

1、我們需要加入額外的二級(jí)緩存包,例如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配置文件中配置我們二級(jí)緩存的一些屬性(此處針對(duì)的是Hibernate4):

<!-- 啟用二級(jí)緩存 -->
<property name="cache.use_second_level_cache">true</property>
<!-- 配置使用的二級(jí)緩存的產(chǎn)品 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>

3、我們使用的是EHcache,所以我們需要?jiǎng)?chuàng)建一個(gè)ehcache.xml的配置文件,來(lái)配置我們的緩存信息,這個(gè)是EHcache要求的。該文件放到根目錄下。

<ehcache>
    <!--  
        指定一個(gè)目錄:當(dāng) EHCache 把數(shù)據(jù)寫(xiě)到硬盤(pán)上時(shí), 將把數(shù)據(jù)寫(xiě)到這個(gè)目錄下.
    -->
    <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è)置緩存的默認(rèn)數(shù)據(jù)過(guò)期策略
    -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />
    <!--  
        設(shè)定具體的命名緩存的數(shù)據(jù)過(guò)期策略。每個(gè)命名緩存代表一個(gè)緩存區(qū)域
        緩存區(qū)域(region):一個(gè)具有名稱的緩存塊,可以給每一個(gè)緩存塊設(shè)置不同的緩存策略。
        如果沒(méi)有設(shè)置任何的緩存區(qū)域,則所有被緩存的對(duì)象,都將使用默認(rèn)的緩存策略。即:<defaultCache.../>
        Hibernate 在不同的緩存區(qū)域保存不同的類/集合。
            對(duì)于類而言,區(qū)域的名稱是類名。如:com.atguigu.domain.Customer
            對(duì)于集合而言,區(qū)域的名稱是類名加屬性名。如com.atguigu.domain.Customer.orders
    -->
    <!--  
        name: 設(shè)置緩存的名字,它的取值為類的全限定名或類的集合的名字
        maxElementsInMemory: 設(shè)置基于內(nèi)存的緩存中可存放的對(duì)象最大數(shù)目

        eternal: 設(shè)置對(duì)象是否為永久的, true表示永不過(guò)期,
        此時(shí)將忽略timeToIdleSeconds 和 timeToLiveSeconds屬性; 默認(rèn)值是false
        timeToIdleSeconds:設(shè)置對(duì)象空閑最長(zhǎng)時(shí)間,以秒為單位, 超過(guò)這個(gè)時(shí)間,對(duì)象過(guò)期。
        當(dāng)對(duì)象過(guò)期時(shí),EHCache會(huì)把它從緩存中清除。如果此值為0,表示對(duì)象可以無(wú)限期地處于空閑狀態(tài)。
        timeToLiveSeconds:設(shè)置對(duì)象生存最長(zhǎng)時(shí)間,超過(guò)這個(gè)時(shí)間,對(duì)象過(guò)期。
        如果此值為0,表示對(duì)象可以無(wú)限期地存在于緩存中. 該屬性值必須大于或等于 timeToIdleSeconds 屬性值

        overflowToDisk:設(shè)置基于內(nèi)存的緩存中的對(duì)象數(shù)目達(dá)到上限后,是否把溢出的對(duì)象寫(xiě)到基于硬盤(pán)的緩存中
    -->
    <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>

在注釋中,有一些對(duì)變量的解釋。

4、開(kāi)啟二級(jí)緩存。我們?cè)谶@里使用的xml的配置方式,所以要在Customer.hbm.xml文件加一點(diǎn)配置信息:

<cache usage="read-only"/>

注意是在標(biāo)簽內(nèi)。 如果是使用注解的方法,在要在Customer這個(gè)類中,加入@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)這個(gè)注解。

5、下面我們?cè)龠M(jìn)行一下測(cè)試。還是上面的代碼:

@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)控制臺(tái)只發(fā)出了一條SQL語(yǔ)句。這是我們二級(jí)緩存的一個(gè)小Demo。

我們的二級(jí)緩存是sessionFactory級(jí)別的,所以當(dāng)我們session關(guān)閉再打開(kāi)之后,我們?cè)偃ゲ樵儗?duì)象的時(shí)候,此時(shí)Hibernate會(huì)先去二級(jí)緩存中查詢是否有該對(duì)象。

同樣,二級(jí)緩存緩存的是對(duì)象,如果我們查詢的是對(duì)象的一些屬性,則不會(huì)加入到緩存中。

我們通過(guò)二級(jí)緩存是可以解決之前提到的N+1問(wèn)題。

已經(jīng)寫(xiě)了這么多了,但好像我們關(guān)于緩存的內(nèi)容還沒(méi)有講完。不要著急,再堅(jiān)持一下,我們的內(nèi)容不多了。我們還是通過(guò)一個(gè)例子來(lái)引出下一個(gè)話題。 我們說(shuō)通過(guò)二級(jí)緩存可以緩存對(duì)象,那么我們看一下下面的代碼以及輸出結(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());
}

控制臺(tái)的結(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

我們的緩存好像沒(méi)有起作用哎?這是為啥?當(dāng)我們通過(guò)list()去查詢兩次對(duì)象的時(shí)候,二級(jí)緩存雖然會(huì)緩存插敘出來(lái)的對(duì)象,但不會(huì)緩存我們的hql查詢語(yǔ)句,要想解決這個(gè)問(wèn)題,我們需要用到查詢緩存。

查詢緩存


在前文中也提到了,我們的一級(jí)二級(jí)緩存都是對(duì)整個(gè)實(shí)體進(jìn)行緩存,它不會(huì)緩存普通屬性,如果想對(duì)普通屬性進(jìn)行緩存,則可以考慮使用查詢緩存。

但需要注意的是,大部分情況下,查詢緩存并不能提高應(yīng)用程序的性能,甚至反而會(huì)降低應(yīng)用性能,因此實(shí)際項(xiàng)目中要謹(jǐn)慎的使用查詢緩存。

對(duì)于查詢緩存來(lái)說(shuō),它緩存的key就是查詢所用的HQL或者SQL語(yǔ)句,需要指出的是:查詢緩存不僅要求所使用的HQL、SQL語(yǔ)句相同,甚至要求所傳入的參數(shù)也相同,Hibernate才能直接從緩存中取得數(shù)據(jù)。只有經(jīng)常使用相同的查詢語(yǔ)句、并且使用相同查詢參數(shù)才能通過(guò)查詢緩存獲得好處,查詢緩存的生命周期直到屬性被修改了為止。

查詢緩存默認(rèn)是關(guān)閉。要想使用查詢緩存,只需要在hibernate.cfg.xml中加入一條配置即可:

<property name="hibernate.cache.use_query_cache">true</property>

而且,我們?cè)诓樵僪ql語(yǔ)句時(shí),要想使用查詢緩存,就需要在語(yǔ)句中設(shè)置這樣一個(gè)方法:setCacheable(true)。關(guān)于這個(gè)的demo我就不進(jìn)行演示了,大家可以自己慢慢試著玩一下。

但需要注意的是,我們?cè)陂_(kāi)啟查詢緩存的時(shí)候,也應(yīng)該開(kāi)啟二級(jí)緩存。因?yàn)槿绻皇褂枚?jí)緩存,也有可能出現(xiàn)N+1的問(wèn)題。

這是因?yàn)椴樵兙彺婢彺娴膬H僅是對(duì)象的ID,所以首先會(huì)通過(guò)一條SQL將對(duì)象的ID都查詢出來(lái),但是當(dāng)我們后面要得到每個(gè)對(duì)象的信息的時(shí)候,此時(shí)又會(huì)發(fā)送SQL語(yǔ)句,所以如果我們使用查詢緩存,一定也要開(kāi)啟二級(jí)緩存。

總結(jié)

這些就是自己今晚上研究的關(guān)于Hibernate緩存的一些問(wèn)題,其出發(fā)點(diǎn)也是為了自己能夠?qū)ibernate緩存的知識(shí)有一定的總結(jié)。當(dāng)然了,下一步還需要深入到緩存是如何實(shí)現(xiàn)的這個(gè)深度中。

另外PS一句,最近打球打的很累,都感覺(jué)自己打的有點(diǎn)乏力了。休息幾天再去玩。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)