1. 程式人生 > >Hibernate持久層框架使用【八】效能優化與快取

Hibernate持久層框架使用【八】效能優化與快取

一級快取:

在hibernate中一級快取是預設開啟的,它與session相關,例如當你對資料庫中的資料進行查詢後,它會將查詢到的物件儲存到記憶體中,再次查詢時便直接從記憶體中讀取,從記憶體中讀取的速度顯然比從資料庫中讀取資料要快得多。

為了證明,可以寫一個測試類對快取進行測試

public class Test {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Configuration configuration = new Configuration().configure();
        SessionFactory sessionFactory = configuration.buildSessionFactory();
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();
        
        //記錄開始時的時間
        Long startTime = System.currentTimeMillis();
        //查詢id為1的資料
        Student student = (Student) session.get(Student.class, 1);
        //記錄結束時的時間
        Long endTime = System.currentTimeMillis();
        
        //輸出所用的時間
        System.out.println("所用時間:"+(endTime-startTime));
        System.out.println(student.getName());
        
        //再次查詢這條資料
        //記錄開始時的時間
        Long startTime2 = System.currentTimeMillis();
        //查詢id為1的資料
        Student student2 = (Student) session.get(Student.class, 1);
        //記錄結束時的時間
        Long endTime2 = System.currentTimeMillis();
        
        //輸出所用的時間
        System.out.println("所用時間:"+(endTime2-startTime2));
        System.out.println(student2.getName());
        
        transaction.commit();
        session.close();
        sessionFactory.close();
    }
    
}

上面的程式碼首先記錄了開始查詢前的時間,並且查詢了id為1的一條資料,記錄查詢結束時的時間(此時這個查詢到的Student物件已經被儲存到記憶體中了)

最後,輸出所用的時間

同樣複製上面這段程式碼,查詢同樣一條id為1的資料,記錄下所用的時間並輸出

執行程式

Hibernate: 
    select
        student0_.sid as sid1_1_0_,
        student0_.age as age2_1_0_,
        student0_.name as name3_1_0_ 
    from
        student student0_ 
    where
        student0_.sid=?
所用時間:172
關羽
所用時間:1
關羽

可以看到,在第一次查詢資料時生成了查詢語句,所用時間172ms

再次查詢時不僅沒有生成查詢語句,所用的時間也僅為1ms

可見,從記憶體中讀取的速度要快得多

既然一級快取是通過session儲存在記憶體中的,那麼只要將儲存在session記憶體中的所有物件清空,再次查詢時就會重寫生成查詢語句進行查詢了,同樣使用上面的程式碼,在第一次查詢與第二次查詢之間加入這一行程式碼:

//清空session中儲存的所有物件
session.clear();

再次執行程式

Hibernate: 
    select
        student0_.sid as sid1_1_0_,
        student0_.age as age2_1_0_,
        student0_.name as name3_1_0_ 
    from
        student student0_ 
    where
        student0_.sid=?
所用時間:184
關羽
Hibernate: 
    select
        student0_.sid as sid1_1_0_,
        student0_.age as age2_1_0_,
        student0_.name as name3_1_0_ 
    from
        student student0_ 
    where
        student0_.sid=?
所用時間:4
關羽

控制檯確實輸出了兩次查詢語句,同樣是往資料庫查詢資料,這裡第一條查詢所用的時間為184ms,而第二條雖然沒有像前面從記憶體中讀取那麼快,但也只用了4ms,這是因為第一次查詢時hibernate需要與資料庫進行通訊,而接下來的操作則不再需要

二級快取:

二級快取同樣會將物件快取到記憶體中,但是如果當記憶體存滿時,它會將資料存放到磁碟中

它是於SessionFactory相關的,所以即使Session被清空,查詢出的資料依舊會使用二級快取通過SessionFactory儲存在記憶體中,直到SessionFactory被關閉或快取到達時效

在hibernate中二級快取是預設關閉的,在使用二級快取時需要加入第三方的jar包才能使用

首先開啟從官網下載好的hibernate,解壓(詳細參考第一篇部落格【配置hibernate】)

解壓後進入lib資料夾下的optional資料夾,這裡有一些可選的第三方jar包資料夾,選擇ehcache,將資料夾下的三個jar包拷貝到專案中引入(我的分別是slf4j-api-1.6.1.jar、hibernate-ehcache-4.3.8.Final.jar、ehcache-core-2.4.3.jar)

接著開啟hibernate-release-4.3.8.Final\project\etc這個資料夾,將ehcache.xml這個快取配置檔案拷貝到專案的src路徑下

開啟這個配置檔案,大概是這樣的:

<ehcache>

    <diskStore path="D:\\ehcache"/>
    
    <defaultCache
        maxElementsInMemory="100"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />
        
    <cache name="sampleCache1"
        maxElementsInMemory="3"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="true"
        />
        
</ehcache>

這個配置檔案儲存了使用這個快取的一些配置

其中,<diskStore path="D:\\ehcache"/>表示當你記憶體存滿時,暫存的磁碟路徑

另外還有兩個類似的配置

<defaultCache
   maxElementsInMemory="100"
   eternal="false"
   timeToIdleSeconds="120"
   timeToLiveSeconds="120"
   overflowToDisk="true"
/>

這個配置表示預設的快取配置,即當你不宣告使用哪個快取配置時,預設使用這個配置

在預設配置中:
maxElementsInMemory="100"  表示最大的快取數量
eternal="false"  表示是否永久儲存
 timeToIdleSeconds="120"  表示閒置時間,120s
timeToLiveSeconds="120"  表示存活時間,120s(時間結束時快取清空)
overflowToDisk="true"  表示當超過快取數量時是否儲存到磁碟,這裡寫了true,(儲存的路徑就是上面宣告的diskStore)

還有一個自定義的配置

<cache name="sampleCache1"
   maxElementsInMemory="3"
   eternal="false"
   timeToIdleSeconds="300"
   timeToLiveSeconds="600"
   overflowToDisk="true"
/>

和前面的預設配置一樣,只不過加了一個name,在後面使用快取時可以通過這個name來宣告使用這個配置,為了方便測試,這裡的快取數量我改為3

快取配置完成後再開啟你的hibernate配置檔案hibernate.cfg.xml

加入下面這兩個配置來啟用快取,以及啟用哪個快取

<!-- 配置開啟二級快取 -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- 配置二級快取的提供商 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>

這兩個配置開啟hibernate-release-4.3.8.Final\project\etc下的hibernate.properties按ctrl+f搜尋cache就能找到了

最後,還需要進行一個配置,來對持久化類使用快取

<!-- 對持久化類Student使用快取 -->
<class-cache usage="read-write" class="domain.Student" region="sampleCache1"/>

這個配置要加在持久化類的mapping對映下面

測試二級快取:

public class EhCache {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Configuration configuration = new Configuration().configure();
		SessionFactory sessionFactory = configuration.buildSessionFactory();
		
		query(sessionFactory);
		query(sessionFactory);
		
		//sessionFactory.close();
	}
	
	public static void query(SessionFactory sessionFactory){
		Session session = sessionFactory.openSession();
		Transaction transaction = session.beginTransaction();
		
		//記錄開始時的時間
		Long startTime = System.currentTimeMillis();
		//查詢id為1的資料
		List<Object> students = session.createSQLQuery("select * from Student").list();
		//記錄結束時的時間
		Long endTime = System.currentTimeMillis();
		
		//輸出所用的時間
		System.out.println("所用時間:"+(endTime-startTime));

		
		transaction.commit();
		session.close();
	}

}

因為這個二級快取是與SessionFactory相關的,為了排除一級快取的影響,這裡寫了一個方法,傳入SessionFactory,在查詢完成後關閉Session,在main方法中重複執行兩次,因為每次呼叫這個方法執行完成後都會將session關閉,所以排除了一級快取的影響。

執行程式,看到控制檯列印的資料

Hibernate: 
    select
        * 
    from
        Student
所用時間:95
Hibernate: 
    select
        * 
    from
        Student
所用時間:0

前後兩次查詢,每次查詢完成後都關閉了session,但是查詢所用的時間卻截然不同,第一次使用了95ms,第二次則顯示0ms

在前面對持久化類進行快取配置時,使用了自定義的快取配置,即最大快取數為3,但這裡,我資料庫查詢出來的結果卻不只3條,那麼剩下的物件應該被儲存到了前面配置的磁碟路徑中了,開啟D:\ehcache,果然看到了兩個data檔案,這些檔案就是二級快取超出記憶體時暫存到磁碟的資料了

main方法中還有一行程式碼被註釋掉了,即//sessionFactory.close();

因為二級快取是和sessionFactory相關的,如果開啟這行程式碼,在執行完畢後將sessionFactory關閉,那麼在關閉時,暫存到磁碟的資料也會被清空

效能優化:

我們在對資料表中插入資料時,一般這些插入的物件會被儲存到session中,而session會使用JVM分配到的記憶體進行儲存,直到transaction進行commit,再一次性將session中的物件提交到資料庫中,當session中儲存的資料量超過記憶體時,就會發生記憶體溢位,導致資料無法成功儲存

例如下面插入1億條資料

public class Test01 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Configuration configuration = new Configuration().configure();
		SessionFactory sessionFactory = configuration.buildSessionFactory();
		Session session = sessionFactory.openSession();
		Transaction transaction = session.beginTransaction();
		
		for(int i=0; i<100000000; i++)
		{
			Student student = new Student();
			student.setName("馬超"+i);
			student.setAge(i);
			session.save(student);
		}
		
		transaction.commit();
		session.close();
		sessionFactory.close();
	}

}

如果不斷地執行下去,最後將會發生記憶體溢位的錯誤

為了解決這個問題,我們可以對此進行一些優化,如下,在for迴圈中加入一個if語句,判斷每10條資料進行一次同步(將資料同步到資料庫中)

public class Test01 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Configuration configuration = new Configuration().configure();
		SessionFactory sessionFactory = configuration.buildSessionFactory();
		Session session = sessionFactory.openSession();
		Transaction transaction = session.beginTransaction();
		
		for(int i=0; i<100000000; i++)
		{
			Student student = new Student();
			student.setName("馬超"+i);
			student.setAge(i);
			session.save(student);
			
			if (i % 10 == 0) {
				session.flush();
			}
		}
		
		transaction.commit();
		session.close();
		sessionFactory.close();
	}

}