鍍金池/ 教程/ Java/ Hibernate緩存
雙向1-1關聯(lián)
單向N-N關聯(lián)
導讀
概述
Hibernate緩存
悲觀鎖
雙向1-N關聯(lián)
作者簡介
概述
load和get的簡單介紹
單向N-1關聯(lián)

Hibernate緩存


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

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

Hibernate緩存包括兩大類:一級緩存和二級緩存。

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

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

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

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

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

Hibernate一級緩存


Demo

首先看一個非常簡單的例子:

@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關閉,然后又重新打開了新的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關閉后,緩存就沒了,當我們再次打開session的時候,緩存中是沒有了之前查詢的對象的,所以會再次發(fā)送SQL語句。

我們稍微對一級緩存的知識點進行總結(jié)一下,然后再開始討論關于二級緩存的內(nèi)容。

作用

Session的緩存有三大作用:

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

小結(jié)

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

我們在講之前的例子中,提到我們關閉session再打開,這個時候一級緩存就不存在了,所以我們再次查詢的時候,會再次發(fā)送SQL語句。那么如果要解決這個問題,我們該怎么做?二級緩存可以幫我們解決這個問題。

Hibernate二級緩存

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

1、我們需要加入額外的二級緩存包,例如EHcache,將其包導入。需要: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ù)據(jù)過期策略
    -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />
    <!--  
        設定具體的命名緩存的數(shù)據(jù)過期策略。每個命名緩存代表一個緩存區(qū)域
        緩存區(qū)域(region):一個具有名稱的緩存塊,可以給每一個緩存塊設置不同的緩存策略。
        如果沒有設置任何的緩存區(qū)域,則所有被緩存的對象,都將使用默認的緩存策略。即:<defaultCache.../>
        Hibernate 在不同的緩存區(qū)域保存不同的類/集合。
            對于類而言,區(qū)域的名稱是類名。如:com.atguigu.domain.Customer
            對于集合而言,區(qū)域的名稱是類名加屬性名。如com.atguigu.domain.Customer.orders
    -->
    <!--  
        name: 設置緩存的名字,它的取值為類的全限定名或類的集合的名字
        maxElementsInMemory: 設置基于內(nèi)存的緩存中可存放的對象最大數(shù)目

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

        overflowToDisk:設置基于內(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關閉再打開之后,我們再去查詢對象的時候,此時Hibernate會先去二級緩存中查詢是否有該對象。

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

我們通過二級緩存是可以解決之前提到的N+1問題。

已經(jīng)寫了這么多了,但好像我們關于緩存的內(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查詢語句,要想解決這個問題,我們需要用到查詢緩存。

查詢緩存


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

但需要注意的是,大部分情況下,查詢緩存并不能提高應用程序的性能,甚至反而會降低應用性能,因此實際項目中要謹慎的使用查詢緩存。

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

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

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

而且,我們在查詢hql語句時,要想使用查詢緩存,就需要在語句中設置這樣一個方法:setCacheable(true)。關于這個的demo我就不進行演示了,大家可以自己慢慢試著玩一下。

但需要注意的是,我們在開啟查詢緩存的時候,也應該開啟二級緩存。因為如果不使用二級緩存,也有可能出現(xiàn)N+1的問題。

這是因為查詢緩存緩存的僅僅是對象的ID,所以首先會通過一條SQL將對象的ID都查詢出來,但是當我們后面要得到每個對象的信息的時候,此時又會發(fā)送SQL語句,所以如果我們使用查詢緩存,一定也要開啟二級緩存。

總結(jié)

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

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