一、案件背景:
圖片來自於電影《天生殺人狂》
Hibernate並沒有為巨型數據集合提供良好的幫助,這也許是開發者認為這樣沒有太大必要,反而增加Hibernate框架復雜性的緣故吧。於是“極大數據量==批量處理”、“Hibernate/java不是批處理的最佳場所”的觀念在Hibernate開發中大行其道,有些開發者甚至直接利用Hibernate建立session,獲取其connection進而進行jdbc操作。Jdbc並不是古董,但在Hibernate中再次call它,難免有些令人無奈。最近在Hibernate的官方壇子上看到Gavin寫給初級用戶的“understand FlushMode.NEVER”,並參考了一下Stripes項目(本人時常關注的時髦項目)作者Tim的blog。在閱讀兩位大家言論後,和大家share一下。
二、性能殺手何在?
圖片來自於電影《這個殺手不太冷》
Tim在其Blog寫道:“我目前的DNA重組系統,具有復雜而海量的OLTP數據,對付這些在內存的復雜對象(數千個)的方式是依賴用戶接口(非批量處理)來實現用例驅動。”這句半開玩笑的話,是我想起了那男耕女織的生產力低下的生活,真的讓每個開發者都使用算盤運算嗎?
session.setFlushMode(FlushMode.NEVER);
這條語句及其簡單,但解決了大問題。它告知Hibernate session無論何時也不要flush任何的狀態變化到數據庫,除非開發者直接調用session.flush()。聽上去很合乎邏輯,但它為何在一些場景中對性能影響甚深,而在其他的場景中卻好似輕如鵝毛般?
在Tim的項目中存在著一個十分典型的case(我也不大了解生物,這不能怪我):在實驗中利用PCR Primers對遺傳基因(genes)和DNA中的核苷酸序列(exons),這裡的PCR Primers是在PCR處理過程中用於檢測DNA片段的物質,對不起大家,本人對生物學詞匯實在無能為力。檢測匹配過程大致分為以下7步:
1.發現本次實驗中所有exons(個數在5000個以上);
2.查詢本次實驗所有已經排序的PCR Primers;
3.查詢本次實驗所有待排序的PCR Primers;
4.得到與exons對應的Primers找出那些無需轉換的部分;
5.在系統中為無需轉換的區域查詢所有可能的PCR Primers;
6.測試每個primer找出最佳exons匹配者(Primer);
7.保存找出的Primer。
不用擔心,步驟細節不大明白也不會影響後面的理解。
由於domain model極其海量,在第4步我們可能在一個session中排序20000-30000個對象。而在5、6步的查詢將帶來0-20個附加對象。有趣之處在於當執行第7步將對象save到數據庫時,沒有一個前面裝載的對象被修改過。整個實驗的目的就是僅僅獲得這0-20個對象。
在回顧了Tim的生物學場景之後,讓我們重新回到FlushMode.NEVER的討論上來吧。你可能認為既然直到最後一步都沒有修改或是持久化任何東西,那麼改變flush模式將收效甚微。當然這是不正確的未參透實質的理解。實際上,在上面流程的起始設置Never這個flush模式、在流程終點手動flush將節省一半的run time,請注意這裡僅提到了run time而沒有將內存、IO計算在內。
三、這個殺手不太“冷”
圖片來自於電影《黑衣人》
幸好,這個殺手不太“冷”!這都歸結於Hibernate的髒檢查(dirty checking)。每次裝載一個對象到內存(不能去evict它)時,session始終跟蹤它的修改。於是每次對數據的查詢,session都將跌代所有的session中的對象並檢查髒數據,將髒數據flush到數據庫。Hibernate這樣做的良苦用心是為了確保在執行查詢之前所有可能影響查詢的變化都被提交到數據庫。這對零星數據量的應用來講,不足為言。但面對數千個對象和千余次的查詢來講,它將使性能的真正殺手。
了解真相後,我們可以使用session.setFlushMode(FlushMode.NEVER)語句將在查詢時不需髒檢查的數據(生物實驗中的Primers)標識髒數據,這樣Hibernate無處不在的代理機制將被“欺騙”,直接將它添加到髒數據列表(列表中的數據不會被flush到數據庫)中,這樣“殺手”就會因無處遁形而自動消失了。
四、打造制勝武器:
圖片來自於電影《致命武器3》
在讀完這個簡短的“案件”後,我們也學到了如何在一個session中讀取、查詢大量數據對象情況下的制勝武器- FlushMode.NEVER。當然要記住,在此過程中你不可以修改這些數據,不然就真的把數據“搞髒”了。
使用武器秘訣如下:
FlushMode previous = session.getFlushMode();
session.flush(); // who know's what been done till now
session.setFlushMode(FlushMode.NEVER);
// Do some querying
// Do some more querying
// Really load up that session
// Execute a few more queries
// Write back to some tables
session.flush();
session.setFlushMode(previous);