10 11Hibernate之緩存機制

Hibernate中的持久態實際上就利用了緩存的處理機制。

1 認識緩存

所謂的緩存實際上指的就是一種查詢性能的有效提升手段。可以避免數據重複查詢所帶來的性能開銷,通過此方式來進行數據的快速存儲,但是並不意味着緩存一定能夠提升性能,相反,如果處理不當,也可能造成致命傷害。

在項目的開發之中有兩種比較常見的緩存組件:OSCache、EHCache,其中Hibernate之中主要使用的是EHCache組件,之所以使用此組件主要是考慮到簡潔問題,在緩存之中至少需要考慮以下幾種情況:
(1)緩存的個數;
(2)緩存數據如何讓保存;
(3)緩存數據如何同步。

在Hibernate之中主要支持的是兩類緩存:
(1)Session級緩存(一級緩存、first_level_cache):在使用get()或load()方法的時候,對象實際上一直就處於持久態的狀態,那麼這一點就是一級緩存的配置,但是如果Session關閉了,那麼一級緩存的內容就將消失,一級緩存永遠存在。
(2)SessionFactory級緩存(二級緩存、second_level_cache):指的是跨越Session的緩存存在,可以在多個Session對象之中共享數據,二級緩存是需要進行額外配置的。

並且如果你的Hibernate需要使用緩存,那麼必須在創建項目的時候就準備好相應的第三方組件包。

2 一級緩存

一級緩存是Session級的緩存,永遠都會存在,也就是說如果用戶執行了save()或者是get()方法後,那麼這個對象在Session關閉前會一直被保留下來。
範例:觀察一級緩存

package org.lks.test;

import java.text.SimpleDateFormat;

import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionCacheDemoA {
	public static void main(String[] args) throws Exception {
		Member vo = new Member();
		vo.setMid("1001");
		vo.setMname("hhy");
		vo.setMage(20);
		vo.setMsalary(2000.0);
		vo.setMbirthday(new SimpleDateFormat("yyyy-MM-dd").parse("1999-08-21"));
		vo.setMnote("big fool!");
		HibernateSessionFactory.getSession().save(vo);  //變爲了持久態
		HibernateSessionFactory.getSession().beginTransaction().commit();
		// 在保存完成之後,並且沒有關閉Session的前提下發出了一個查詢操作
		Member temp = (Member)HibernateSessionFactory.getSession().get(Member.class, "1001");
		System.out.println(temp);
		
	}
}
Hibernate: 
    insert 
    into
        hedb.member
        (mage, mbirthday, mname, mnote, msalary, mid) 
    values
        (?, ?, ?, ?, ?, ?)
org.lks.pojo.Member@6b5f8707

保存完成之後隨後進行查詢,但是發現Hibernate並沒有發出另外一條SELECT語句,因爲此時的對象一旦被保存了,就將在緩存中存在,而如果緩存中存在的內容,進行查詢的時候將不在重複發出查詢。
範例:觀察緩存存在

package org.lks.test;

import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionCacheDemoB {
	public static void main(String[] args) throws Exception {
		// 在保存完成之後,並且沒有關閉Session的前提下發出了一個查詢操作
		Member vo = (Member)HibernateSessionFactory.getSession().get(Member.class, "1001");
		//此時沒有關閉Session,再一次發出同樣的查詢操作
		Member temp = (Member)HibernateSessionFactory.getSession().get(Member.class, "1001");
	}
}

Hibernate: 
    select 第一次查詢發出的查詢指令
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?

一級緩存的操作控制都在Session接口裏面定義了,定義有如下的幾個操作方法:
(1)清空所有緩存數據:public void clear()
(2)強制刷新緩衝區:
(3)清除一個緩存對象:
範例:觀察如何清除一個對象

package org.lks.test;

import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionCacheDemoC {
	public static void main(String[] args) throws Exception {
		// 在保存完成之後,並且沒有關閉Session的前提下發出了一個查詢操作
		Member vo = (Member)HibernateSessionFactory.getSession().get(Member.class, "1001");
		//將之前查詢出來的對象(是保存在緩存中的)進行緩存的清除
		HibernateSessionFactory.getSession().evict(vo);
		//此時沒有關閉Session,再一次發出同樣的查詢操作,此時緩存沒有數據
		Member temp = (Member)HibernateSessionFactory.getSession().get(Member.class, "1001");
	}
}

Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?

此時的代碼發出了兩條查詢指令,原因在於:第一個對象查詢出來之後將其進行了刪除緩存對象的操作,所以在第二次查詢同樣主鍵的時候,如果發現此時數據在緩存裏面不存在,那麼就將重新發出查詢指令。

實際意義:如果說現在使用了save()方法保存了新增加的數據,那麼按照道理來講,這些數據應該都會被緩存(不關閉Session的前提下),那麼也就是說如果緩存的數據很多,那麼程序就會出現問題。
範例:觀察問題引出

package org.lks.test;

import java.text.SimpleDateFormat;

import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionCacheDemoD {
	public static void main(String[] args) throws Exception {
		for(int i = 0; i < 10; i++){
			Member vo = new Member();
			vo.setMid("200" + i);
			vo.setMname("hhy");
			vo.setMage(20);
			vo.setMsalary(2000.0);
			vo.setMbirthday(new SimpleDateFormat("yyyy-MM-dd").parse("1999-08-21"));
			vo.setMnote("big fool!");
			HibernateSessionFactory.getSession().save(vo);  //變爲了持久態
		}
		
		HibernateSessionFactory.getSession().beginTransaction().commit();
		// 在保存完成之後,並且沒有關閉Session的前提下發出了一個查詢操作
		Member vo = (Member)HibernateSessionFactory.getSession().get(Member.class, "2001");
		//此時沒有關閉Session,再一次發出同樣的查詢操作,此時緩存沒有數據
		Member temp = (Member)HibernateSessionFactory.getSession().get(Member.class, "2002");
	}
}

Hibernate: 
    insert 
    into
        hedb.member
        (mage, mbirthday, mname, mnote, msalary, mid) 
    values
        (?, ?, ?, ?, ?, ?)
x10

此時保存了10條記錄,但是由於save()方法保存對象之後,對象會保存在緩存之中,所以此時如果發出了查詢指定的主鍵對象的時候就不會再出現查詢操作了。

那麼請思考一下,如果說現在要求你批量保存100000行記錄呢?按照此時的道理來講,這100000行的記錄都要緩存起來,這樣明顯會造成一個非常危險的舉動,所以在進行實際的開發過程之中,必須要考慮數據的分批處理。
範例:批量數據增加

package org.lks.test;

import java.text.SimpleDateFormat;

import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionCacheDemoE {
	public static void main(String[] args) throws Exception {
		for(int i = 0; i < 200; i++){
			Member vo = new Member();
			vo.setMid("lk" + i);
			vo.setMname("hhy");
			vo.setMage(20);
			vo.setMsalary(2000.0);
			vo.setMbirthday(new SimpleDateFormat("yyyy-MM-dd").parse("1999-08-21"));
			vo.setMnote("big fool!");
			HibernateSessionFactory.getSession().save(vo);  //變爲了持久態
			if(i != 0 && i % 10 == 0){ //每10條記錄進行一次緩衝區的刷新
				HibernateSessionFactory.getSession().flush();
				HibernateSessionFactory.getSession().beginTransaction().commit();
				HibernateSessionFactory.getSession().clear();
			}
		}
		HibernateSessionFactory.getSession().beginTransaction().commit();
	}
}

如果以後遇見了數據的批量處理,那麼一定要注意按照數據的分批保存以及分批清空的原則來進行處理。

面試題:如果使用Hibernate進行數據批處理的話,需要注意哪些問題?
(1)Hibernate之中一級的Session緩存會永遠存在,也就是說如果使用了save()方法,那麼對象信息將一直保留到Session關閉,所以如果存放的內容太多,則有可能造成程序出現問題;
(2)可以使用Session接口中的flush()和clear()兩個方法進行強制性的緩存清空,保證緩存的容量不會佔用過多。

3 二級緩存

在Hibernate的默認狀態下,每一個Session緩存的數據是不能夠進行共享的。
範例:觀察不同Session的數據訪問

package org.lks.test;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionFactoryCacheDemoA {
	public static void main(String[] args) throws Exception {
		// 取得的是SessionFactory類對象
		SessionFactory factory = HibernateSessionFactory.getSessionFactory();
		Session sessionA = factory.openSession(); // 取得一個Session對象
		Member mea = sessionA.get(Member.class, "3161301220"); // 數據的查詢
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
		Session sessionB = factory.openSession(); // 取得另一個Session對象
		Member meb = sessionB.get(Member.class, "3161301220"); // 查詢同一個數據
	}
}

Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?

此時一共產生了兩行查詢語句,也就是說,每當使用不同的Session查詢數據的時候都會重複的執行,也就得出一個結論,此時不同的Session無法實現數據的共享操作。

那麼如果現在希望即使在不同的Session上也可以實現數據的緩存操作,那麼就需要使用二級緩存完成。但是二級緩存必須由用戶單獨進行配置,在之前所設置的一系列的開發包,都屬於二級緩存的準備。

範例:定義一個緩存的配置文件——ehcache.xml文件

<ehcache>
	<!-- 指的是緩存的臨時保存目錄 -->
	<diskStore path="java.io.tmpdir"/>
	<!-- 此處進行默認的緩存配置 -->
	<defaultCache
		maxElementsInMemory="10000"  →可以緩存的最大的POJO類對象個數
		eternal="false"  →是否允許該緩存自動失效
		timeToIdleSeconds="120"   最小的失效時間
		timeToLiveSeconds="120"   →最大的保存時間
		overflowToDisk="true" />  →如果保存過多則自動利用磁盤存儲緩存
</ehcache>

雖然項目裏面已經存在有緩存的支持包以及緩存的配置文件,但是如果要想讓緩存可以使用,還需要再hibernate.cfg.xml文件裏面配置使用的緩存類型。

<property name="hibernate.cache.region.factory_class">
	org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>

此時已經可以使用二級緩存進行開發操作。但是與一級緩存相比,二級緩存的使用更加麻煩,因爲可能在實際的開發之中,並不一定所有的POJO類都可能需要使用到二級緩存。所以來講還應該在支持緩存的位置上進行啓用,有以下三種啓用的操作形式:
(1)如果你的項目是基於*.hbm.xml文件配置的,則可以在Member.hbm.xml文件中增加以下配置:

<cache usage="read-only" />

(2)如果現在使用的是Annotation,則可以利用以下註解在POJO類中配置;

@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)

(3)但是很多時候考慮到你項目有可能經常會需要修改,如果都去找每一個文件配置會比較麻煩,那麼也可以直接在hibernate.cfg.xml文件裏面進行綜合的配置:

<class-cache usage="read-only" class="org.lks.pojo.Member" />

第三種形式只需要維護一個文件就可以了,不需要到處維護。

範例:觀察二級緩存

package org.lks.test;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionFactoryCacheDemoA {
	public static void main(String[] args) throws Exception {
		// 取得的是SessionFactory類對象
		SessionFactory factory = HibernateSessionFactory.getSessionFactory();
		Session sessionA = factory.openSession(); // 取得一個Session對象
		Member mea = sessionA.get(Member.class, "3161301220"); // 數據的查詢
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
		Session sessionB = factory.openSession(); // 取得另一個Session對象
		Member meb = sessionB.get(Member.class, "3161301220"); // 查詢同一個數據
	}
}

Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
+++++++++++++++++++++++++++++++++++++++++++++++++++++++

此時的代碼只發出了一次的查詢操作,所以二級緩存的配置起作用了。

實際上對於緩存的操作一共有以下幾種常用模式:
(1)READ_ONLY(只讀,唯一推薦使用的):只讀緩存,在數據查詢的時候使用,將查詢結果保存在緩存之中,以供其它的Session使用;
(2)READ_WRITE(讀、寫緩存):當數據庫裏面的數據發生變化之後,那麼可以進行及時的數據追蹤。但是這種操作性能是最差的;
(3)NONSTRICT_READ_WRITE(不嚴格的讀寫操作):在數據庫發生變化之後不會立即進行緩存的更新,而是等待一段時間之後再進行數據的更新操作;
(4)TRANSACTIONAL(事務處理緩存):如果數據庫中的數據發生了回滾操作後,內存中的數據也要一起發生回滾操作。

4 緩存交互

如果項目中配置了二級緩存之後,對於程序而言,就相當於只要是通過Session查詢的數據都會默認的保存在二級緩存之中,以供其它的Session使用,但是這些都是默認的配置形式,而用戶也可以根據自己的需要來配置是否需要進行二級緩存,而配置的方法主要是在Session接口中:public void setCacheMode(CacheMode cacheMode)

而在setCacheMode()方法裏面會接收一個CacheMode類型的參數,那麼這屬於枚舉類型,所以在這個枚舉類裏面就定義有如下的幾種緩存模式:
(1)public static final CacheMode GET:指的是從緩存取數據,但是不保存;
(2)public static final CacheMode PUT:指的是向緩存中保存數據,但是不取;
(3)public static final CacheMode NORMAL:正常進行緩存數據的讀、寫(默認情況);
(4)public static final CacheMode REFRESH:可以針對於緩存中的重複數據進行刷新操作。
範例:觀察緩存模式

package org.lks.test;

import org.hibernate.CacheMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionFactoryCacheDemoA {
	public static void main(String[] args) throws Exception {
		// 取得的是SessionFactory類對象
		SessionFactory factory = HibernateSessionFactory.getSessionFactory();
		Session sessionA = factory.openSession(); // 取得一個Session對象
		sessionA.setCacheMode(CacheMode.GET);
		Member mea = sessionA.get(Member.class, "3161301220"); // 數據的查詢
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
		Session sessionB = factory.openSession(); // 取得另一個Session對象
		Member meb = sessionB.get(Member.class, "3161301220"); // 查詢同一個數據
	}
}

Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?

因爲此時第一個查詢使用的緩存模式爲GET,所以內容不會向二級緩存中進行保存。
範例:也可以控制第二個Session,讓其不讀取緩存

package org.lks.test;

import org.hibernate.CacheMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.lks.dbc.HibernateSessionFactory;
import org.lks.pojo.Member;

public class TestSessionFactoryCacheDemoA {
	public static void main(String[] args) throws Exception {
		// 取得的是SessionFactory類對象
		SessionFactory factory = HibernateSessionFactory.getSessionFactory();
		Session sessionA = factory.openSession(); // 取得一個Session對象
		Member mea = sessionA.get(Member.class, "3161301220"); // 數據的查詢
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
		Session sessionB = factory.openSession(); // 取得另一個Session對象
		sessionB.setCacheMode(CacheMode.PUT);
		Member meb = sessionB.get(Member.class, "3161301220"); // 查詢同一個數據
	}
}

Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Hibernate: 
    select
        member0_.mid as mid1_0_0_,
        member0_.mage as mage2_0_0_,
        member0_.mbirthday as mbirthda3_0_0_,
        member0_.mname as mname4_0_0_,
        member0_.mnote as mnote5_0_0_,
        member0_.msalary as msalary6_0_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?

此時同樣發出了兩次查詢語句,因爲第一次查詢的時候正常讀寫,而第二次查詢的時候沒有讀,直接進行了寫操作。

5 查詢緩存

在之前所做的全部查詢都有一個特點,都是基於Session完成的,但是從實際的開發來講,數據的查詢首選的一定是Query接口,那麼在默認狀態下,能否實現緩存呢?
範例:觀察問題

package org.lks.test;

import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.lks.dbc.HibernateSessionFactory;

public class TestQueryCacheDemoA {
	public static void main(String[] args) {
		SessionFactory factory = HibernateSessionFactory.getSessionFactory();
		Query queryA = factory.openSession().createQuery("FROM Member AS m WHERE m.mid=?");
		queryA.setParameter(0, "3161301220");
		queryA.setCacheable(true);
		System.out.println(queryA.uniqueResult());
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
		Query queryB = factory.openSession().createQuery("FROM Member AS m WHERE m.mid=?");
		queryB.setParameter(0, "3161301220");
		queryA.setCacheable(true);
		System.out.println(queryB.uniqueResult());
	}
}

Hibernate: 
    select
        member0_.mid as mid1_0_,
        member0_.mage as mage2_0_,
        member0_.mbirthday as mbirthda3_0_,
        member0_.mname as mname4_0_,
        member0_.mnote as mnote5_0_,
        member0_.msalary as msalary6_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
org.lks.pojo.Member@293cde83
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Hibernate: 
    select
        member0_.mid as mid1_0_,
        member0_.mage as mage2_0_,
        member0_.mbirthday as mbirthda3_0_,
        member0_.mname as mname4_0_,
        member0_.mnote as mnote5_0_,
        member0_.msalary as msalary6_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
org.lks.pojo.Member@293cde83

此時可以發現,如果利用的是Query的查詢,那麼所配置的二級緩存無效,也就是說此時並沒有使用到緩存的配置。

因爲Query接口默認情況下並沒有啓動緩存的支持,在Query接口中定義有如下的方法:
(1)配置是否支持緩存:public Query<R> setCacheable(boolean cacheable)
(2)配置緩存模式:public Query<R> setCacheMode(CacheMode cacheMode)
默認情況下Hibernate同樣是沒有啓用查詢緩存支持,所以如果要想使用查詢緩存,還需要在hibernate.cfg.xml文件裏面增加一個屬性。

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

範例:啓用查詢緩存

package org.lks.test;

import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.lks.dbc.HibernateSessionFactory;

public class TestQueryCacheDemoA {
	public static void main(String[] args) {
		SessionFactory factory = HibernateSessionFactory.getSessionFactory();
		Query queryA = factory.openSession().createQuery("FROM Member AS m WHERE m.mid=?");
		queryA.setParameter(0, "3161301220");
		queryA.setCacheable(true);
		System.out.println(queryA.uniqueResult());
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
		Query queryB = factory.openSession().createQuery("FROM Member AS m WHERE m.mid=?");
		queryB.setParameter(0, "3161301220");
		queryB.setCacheable(true);
		System.out.println(queryB.uniqueResult());
	}
}

Hibernate: 
    select
        member0_.mid as mid1_0_,
        member0_.mage as mage2_0_,
        member0_.mbirthday as mbirthda3_0_,
        member0_.mname as mname4_0_,
        member0_.mnote as mnote5_0_,
        member0_.msalary as msalary6_0_ 
    from
        hedb.member member0_ 
    where
        member0_.mid=?
org.lks.pojo.Member@6b474074
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
org.lks.pojo.Member@2a22ad2b

對於Criteria的查詢也都有相應的方法支持,使用的形式也是一樣的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章