1. 程式人生 > >Hibernate緩存

Hibernate緩存

發出 一點 nds 適合 pad save ransac 而是 evict

Hibernate中會經經常使用到set等集合來表示1-N的關系。

比方,我有Customer和Order兩個對象。當中,在Customer中有一個Order的set集合,表示在一個顧客能夠擁有多個Order,而在Order對象中存在了一個Customer的對象,表示這個Order是哪個顧客下的單。這個算是比較典型的雙向1-N關聯。

這給我們帶來了非常大的優點,當我得到了Customer對象的時候,我們能夠非常方便的將與其相關聯的Order集合查詢出來,這也非常符合我們的實際業務,畢竟我們不可能給這個Cutomer對象別人的Order吧,這既不安全。並且對Customer的普通顧客來說,並無卵用。

所以我們不得不說Hibernate的ORM做的非常好。但凡事都有可是(要是沒有可是,也就沒有寫這篇文章的必要了)。

我們再對數據庫進行訪問的時候必需要考慮性能問題(通俗點講。就是用少發SQL語句)。當我們設定了1-N這樣的關系後,查詢過程中就有可能出現N+1問題。

關於N+1問題。並非本文的重點。

但關於N+1問題,我們須要知道的是,這個問題會導致SQL語句的添加,也就是要與數據庫進行很多其它的交互。這無疑會給項目以及後臺數據庫帶來影響。

Hibernate緩存


Hibernate是一個持久化框架。常常須要訪問數據庫。

假設我們可以減少應用程序對物理數據庫訪問的頻次,那會提供應用程序的執行性能。緩存內的數據是對物理數據源中的數據的復制,應用程序執行時先從緩存中讀寫數據。

緩存就是數據庫數據在內存中的暫時容器。包含數據庫數據在內存中的暫時拷貝。它位於數據庫與數據庫訪問層中間。ORM在查詢數據時首先會依據自身的緩存管理策略,在緩存中查找相關數據。如發現所需的數據,則直接將此數據作為結果加以利用,從而避免了數據庫調用性能的開銷。而相對內存操作而言。數據庫調用是一個代價高昂的過程。

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

  • Hibernate一級緩存又被成為“Session的緩存”。Session緩存是內置的。不能被卸載,是事務範圍的緩存。

    在一級緩存中。持久化類的每一個實例都具有唯一的OID。

  • Hibernate二級緩存又被稱為“SessionFactory的緩存”。因為SessionFactory對象的生命周期和應用程序的整個過程相應,因此Hibernate二級緩存是進程範圍或者集群範圍的緩存,有可能出現並發問題,因此須要採用適當的並發訪問策略。該策略為被緩存的數據提供了事務隔離級別。

    第二級緩存是可選的,是一個可配置的插件。默認下SessionFactory不會啟用這個插件。

那麽什麽樣的數據適合放入到緩存中?

  • 非常少被改動的數據   
  • 不是非常重要的數據,同意出現偶爾並發的數據   
  • 不會被並發訪問的數據   
  • 常量數據

什麽樣的數據不適合放入到緩存中? 

  • 常常被改動的數據   
  • 絕對不同意出現並發訪問的數據,如財務數據。絕對不同意出現並發   
  • 與其它應用共享的數據

Hibernate一級緩存


Demo

首先看一個很easy的樣例:

1
2
3
4
5
6
7
8
@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());
}

看一下控制臺的輸出:

1
2
3
4
5
6
7
8
9
10
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

我們能夠看到,盡管我們調用了兩次session的load方法。但實際上僅僅發送了一條SQL語句。我們第一次調用load方法時候,得到了查詢結果,然後將結果放到了session的一級緩存中。此時,當我們再次調用load方法。會首先去看緩存中是否存在該對象,假設存在,則直接從緩存中取出,就不會在發送SQL語句了。

可是,我們看一下以下這個樣例:

1
2
3
4
5
6
7
8
9
10
11
@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。這個時候。我們再看一下控制臺的輸出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

我們能夠看到,發送了兩條SQL語句。其原因是:Hibernate一級緩存是session級別的,所以假設session關閉後。緩存就沒了,當我們再次打開session的時候,緩存中是沒有了之前查詢的對象的,所以會再次發送SQL語句。

我們略微對一級緩存的知識點進行總結一下。然後再開始討論關於二級緩存的內容。

作用

Session的緩存有三大作用:

  1. 降低訪問數據庫的頻率。

    應用程序從緩存中讀取持久化對象的速度顯然比到數據中查詢數據的速度快多了。因此Session的緩存能夠提高數據訪問的性能。

  2. 當緩存中的持久化對象之間存在循環關聯關系時。Session會保證不出現訪問對象圖的死循環,以及由死循環引起的JVM堆棧溢出異常。
  3. 保證數據庫中的相關記錄與緩存中的對應對象保持同步。

小結

  • 一級緩存是事務級別的。每一個事務(session)都有單獨的一級緩存。這一級別的緩存是由Hibernate進行管理。普通情況下無需進行幹預。
  • 每一個事務都擁有單獨的一級緩存不會出現並發問題,因此無須提供並發訪問策略。
  • 當應用程序調用Session的save()、update()、saveOrUpdate()、get()或load(),以及調用查詢接口的 list()、iterate()(該方法會出現N+1問題,先查id)方法時,假設在Session緩存中還不存在對應的對象。Hibernate就會把該對象增加到第一級緩存中。

    當清理緩存時,Hibernate會依據緩存中對象的狀態變化來同步更新數據庫。

    Session為應用程序提供了兩個管理緩存的方法: evict(Object obj):從緩存中清除參數指定的持久化對象。 clear():清空緩存中全部持久化對象,flush():使緩存與數據庫同步。

  • 當查詢對應的字段,而不是對象時,不支持緩存。我們能夠非常easy舉一個樣例來說明,看一下以下的代碼。

1
2
3
4
5
6
7
@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對象,看一下控制臺的輸出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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再打開,這個時候一級緩存就不存在了,所以我們再次查詢的時候,會再次發送SQL語句。那麽假設要解決問題,我們該怎麽做?二級緩存能夠幫我們解決問題。

Hibernate二級緩存

Hibernate中沒有自己去實現二級緩存。而是利用第三方的。

簡單敘述一下配置過程,也作為自己以後用到的時候配置的一個參考。

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):

1
2
3
4
<!-- 啟用二級緩存 -->
<property name="cache.use_second_level_cache">true</property>
<!-- 配置使用的二級緩存的產品 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>

3、我們使用的是EHcache,所以我們須要創建一個ehcache.xml的配置文件。來配置我們的緩存信息。這個是EHcache要求的。該文件放到根文件夾下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<ehcache>
    <!--  
    	指定一個文件夾:當 EHCache 把數據寫到硬盤上時, 將把數據寫到這個文件夾下.
    -->
    <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.
        -->
    <!--  
    	設置緩存的默認數據過期策略
    -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />
   	<!--  
   		設定詳細的命名緩存的數據過期策略。

每個命名緩存代表一個緩存區域 緩存區域(region):一個具有名稱的緩存塊。能夠給每個緩存塊設置不同的緩存策略。 假設沒有設置不論什麽的緩存區域。則全部被緩存的對象,都將使用默認的緩存策略。即:<defaultCache.../> Hibernate 在不同的緩存區域保存不同的類/集合。

對於類而言,區域的名稱是類名。

如:com.atguigu.domain.Customer 對於集合而言,區域的名稱是類名加屬性名。

如com.atguigu.domain.Customer.orders --> <!-- name: 設置緩存的名字,它的取值為類的全限定名或類的集合的名字 maxElementsInMemory: 設置基於內存的緩存中可存放的對象最大數目 eternal: 設置對象是否為永久的, true表示永只是期, 此時將忽略timeToIdleSeconds 和 timeToLiveSeconds屬性; 默認值是false timeToIdleSeconds:設置對象空暇最長時間,以秒為單位, 超過這個時間,對象過期。 當對象過期時,EHCache會把它從緩存中清除。假設此值為0,表示對象能夠無限期地處於空暇狀態。 timeToLiveSeconds:設置對象生存最長時間,超過這個時間,對象過期。 假設此值為0,表示對象能夠無限期地存在於緩存中. 該屬性值必須大於或等於 timeToIdleSeconds 屬性值 overflowToDisk:設置基於內存的緩存中的對象數目達到上限後,是否把溢出的對象寫到基於硬盤的緩存中 --> <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文件加一點配置信息:

1
<cache usage="read-only"/>

註意是在標簽內。
假設是使用註解的方法,在要在Customer這個類中。增加@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)這個註解。

5、以下我們再進行一下測試。

還是上面的代碼:

1
2
3
4
5
6
7
8
9
10
11
@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());
}

我們能夠發現控制臺僅僅發出了一條SQL語句。這是我們二級緩存的一個小Demo。

我們的二級緩存是sessionFactory級別的,所以當我們session關閉再打開之後,我們再去查詢對象的時候。此時Hibernate會先去二級緩存中查詢是否有該對象。

相同。二級緩存緩存的是對象,假設我們查詢的是對象的一些屬性,則不會增加到緩存中。

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

已經寫了這麽多了。但好像我們關於緩存的內容還沒有講完。不要著急,再堅持一下。我們的內容不多了。我們還是通過一個樣例來引出下一個話題。
我們說通過二級緩存能夠緩存對象,那麽我們看一下以下的代碼以及輸出結果:

1
2
3
4
5
6
7
8
9
10
11
@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());
}

控制臺的結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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語句同樣。甚至要求所傳入的參數也同樣,Hibernate才幹直接從緩存中取得數據。僅僅有常常使用同樣的查詢語句、而且使用同樣查詢參數才幹通過查詢緩存獲得優點,查詢緩存的生命周期直到屬性被改動了為止。

查詢緩存默認是關閉。要想使用查詢緩存,僅僅須要在hibernate.cfg.xml中增加一條配置就可以:

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

並且,我們在查詢hql語句時,要想使用查詢緩存。就須要在語句中設置這樣一個方法:setCacheable(true)

關於這個的demo我就不進行演示了,大家能夠自己慢慢試著玩一下。

但須要註意的是,我們在開啟查詢緩存的時候,也應該開啟二級緩存。

由於假設不使用二級緩存,也有可能出現N+1的問題。

這是由於查詢緩存緩存的不過對象的ID,所以首先會通過一條SQL將對象的ID都查詢出來,可是當我們後面要得到每一個對象的信息的時候。此時又會發送SQL語句。所以假設我們使用查詢緩存,一定也要開啟二級緩存。

總結

這些就是自己今晚上研究的關於Hibernate緩存的一些問題,其出發點也是為了自己可以對Hibernate緩存的知識有一定的總結。

當然了,下一步還須要深入到緩存是怎樣實現的這個深度中。

Hibernate緩存