【永恒的瞬間】
          ?Give me hapy ?

          第 20 章 提升性能

          20.1.  抓取策略(Fetching strategies)

          抓取策略(fetching strategy) 是指:當應用程序需要在(Hibernate實體對象圖的)關聯關系間進行導航的時候, Hibernate如何獲取關聯對象的策略。抓取策略可以在O/R映射的元數據中聲明,也可以在特定的HQL 或條件查詢(Criteria Query)中重載聲明。

          Hibernate3 定義了如下幾種抓取策略:

          • 連接抓取(Join fetching) - Hibernate通過 在SELECT語句使用OUTER JOIN(外連接)來 獲得對象的關聯實例或者關聯集合。

          • 查詢抓取(Select fetching) - 另外發送一條 SELECT 語句抓取當前對象的關聯實體或集合。除非你顯式的指定lazy="false"禁止 延遲抓取(lazy fetching),否則只有當你真正訪問關聯關系的時候,才會執行第二條select語句。

          • 子查詢抓取(Subselect fetching) - 另外發送一條SELECT 語句抓取在前面查詢到(或者抓取到)的所有實體對象的關聯集合。除非你顯式的指定lazy="false" 禁止延遲抓取(lazy fetching),否則只有當你真正訪問關聯關系的時候,才會執行第二條select語句。

          • 批量抓取(Batch fetching) - 對查詢抓取的優化方案, 通過指定一個主鍵或外鍵列表,Hibernate使用單條SELECT語句獲取一批對象實例或集合。

          Hibernate會區分下列各種情況:

          • Immediate fetching,立即抓取 - 當宿主被加載時,關聯、集合或屬性被立即抓取。

          • Lazy collection fetching,延遲集合抓取- 直到應用程序對集合進行了一次操作時,集合才被抓取。(對集合而言這是默認行為。)

          • Proxy fetching,代理抓取 - 對返回單值的關聯而言,當其某個方法被調用,而非對其關鍵字進行get操作時才抓取。

          • Lazy attribute fetching,屬性延遲加載 - 對屬性或返回單值的關聯而言,當其實例變量被訪問的時候進行抓取(需要運行時字節碼強化)。這一方法很少是必要的。

          這里有兩個正交的概念:關聯何時被抓取,以及被如何抓取(會采用什么樣的SQL語句)。不要混淆它們!我們使用抓取來改善性能。我們使用延遲來定義一些契約,對某特定類的某個脫管的實例,知道有哪些數據是可以使用的。

          20.1.1. 操作延遲加載的關聯

          默認情況下,Hibernate 3對集合使用延遲select抓取,對返回單值的關聯使用延遲代理抓取。對幾乎是所有的應用而言,其絕大多數的關聯,這種策略都是有效的。

          注意:假若你設置了hibernate.default_batch_fetch_size,Hibernate會對延遲加載采取批量抓取優化措施(這種優化也可能會在更細化的級別打開)。

          然而,你必須了解延遲抓取帶來的一個問題。在一個打開的Hibernate session上下文之外調用延遲集合會導致一次意外。比如:

          s = sessions.openSession();
          Transaction tx = s.beginTransaction();
          User u = (User) s.createQuery("from User u where u.name=:userName")
          .setString("userName", userName).uniqueResult();
          Map permissions = u.getPermissions();
          tx.commit();
          s.close();
          Integer accessLevel = (Integer) permissions.get("accounts");  // Error!

          Session關閉后,permessions集合將是未實例化的、不再可用,因此無法正常載入其狀態。 Hibernate對脫管對象不支持延遲實例化. 這里的修改方法是:將permissions讀取數據的代碼 移到tx.commit()之前。

          除此之外,通過對關聯映射指定lazy="false",我們也可以使用非延遲的集合或關聯。但是, 對絕大部分集合來說,更推薦使用延遲方式抓取數據。如果在你的對象模型中定義了太多的非延遲關聯,Hibernate最終幾乎需要在每個事務中載入整個數據庫到內存中!

          但是,另一方面,在一些特殊的事務中,我們也經常需要使用到連接抓取(它本身上就是非延遲的),以代替查詢抓取。 下面我們將會很快明白如何具體的定制Hibernate中的抓取策略。在Hibernate3中,具體選擇哪種抓取策略的機制是和選擇 單值關聯或集合關聯相一致的。

          20.1.2.  調整抓取策略(Tuning fetch strategies)

          查詢抓取(默認的)在N+1查詢的情況下是極其脆弱的,因此我們可能會要求在映射文檔中定義使用連接抓取:

          <set name="permissions"
          fetch="join">
          <key column="userId"/>
          <one-to-many class="Permission"/>
          </set
          <many-to-one name="mother" class="Cat" fetch="join"/>

          在映射文檔中定義的抓取策略將會有產生以下影響:

          • 通過get()load()方法取得數據。

          • 只有在關聯之間進行導航時,才會隱式的取得數據(延遲抓取)。

          • 條件查詢

          通常情況下,我們并不使用映射文檔進行抓取策略的定制。更多的是,保持其默認值,然后在特定的事務中, 使用HQL的左連接抓取(left join fetch) 對其進行重載。這將通知 Hibernate在第一次查詢中使用外部關聯(outer join),直接得到其關聯數據。 在條件查詢 API中,應該調用 setFetchMode(FetchMode.JOIN)語句。

          也許你喜歡僅僅通過條件查詢,就可以改變get()load()語句中的數據抓取策略。例如:

          User user = (User) session.createCriteria(User.class)
          .setFetchMode("permissions", FetchMode.JOIN)
          .add( Restrictions.idEq(userId) )
          .uniqueResult();

          (這就是其他ORM解決方案的“抓取計劃(fetch plan)”在Hibernate中的等價物。)

          截然不同的一種避免N+1次查詢的方法是,使用二級緩存。

          20.1.3. 單端關聯代理(Single-ended association proxies)

          在Hinerbate中,對集合的延遲抓取的采用了自己的實現方法。但是,對于單端關聯的延遲抓取,則需要采用 其他不同的機制。單端關聯的目標實體必須使用代理,Hihernate在運行期二進制級(通過優異的CGLIB庫), 為持久對象實現了延遲載入代理。

          默認的,Hibernate3將會為所有的持久對象產生代理(在啟動階段),然后使用他們實現 多對一(many-to-one)關聯和一對一(one-to-one) 關聯的延遲抓取。

          在映射文件中,可以通過設置proxy屬性為目標class聲明一個接口供代理接口使用。 默認的,Hibernate將會使用該類的一個子類。 注意:被代理的類必須實現一個至少包可見的默認構造函數,我們建議所有的持久類都應擁有這樣的構造函數

          在如此方式定義一個多態類的時候,有許多值得注意的常見性的問題,例如:

          <class name="Cat" proxy="Cat">
          ......
          <subclass name="DomesticCat">
          .....
          </subclass>
          </class>

          首先,Cat實例永遠不可以被強制轉換為DomesticCat, 即使它本身就是DomesticCat實例。

          Cat cat = (Cat) session.load(Cat.class, id);  // instantiate a proxy (does not hit the db)
          if ( cat.isDomesticCat() ) {                  // hit the db to initialize the proxy
          DomesticCat dc = (DomesticCat) cat;       // Error!
          ....
          }

          其次,代理的“==”可能不再成立。

          Cat cat = (Cat) session.load(Cat.class, id);            // instantiate a Cat proxy
          DomesticCat dc =
          (DomesticCat) session.load(DomesticCat.class, id);  // acquire new DomesticCat proxy!
          System.out.println(cat==dc);                            // false

          雖然如此,但實際情況并沒有看上去那么糟糕。雖然我們現在有兩個不同的引用,分別指向這兩個不同的代理對象, 但實際上,其底層應該是同一個實例對象:

          cat.setWeight(11.0);  // hit the db to initialize the proxy
          System.out.println( dc.getWeight() );  // 11.0

          第三,你不能對“final類”或“具有final方法的類”使用CGLIB代理。

          最后,如果你的持久化對象在實例化時需要某些資源(例如,在實例化方法、默認構造方法中), 那么代理對象也同樣需要使用這些資源。實際上,代理類是持久化類的子類。

          這些問題都源于Java的單根繼承模型的天生限制。如果你希望避免這些問題,那么你的每個持久化類必須實現一個接口, 在此接口中已經聲明了其業務方法。然后,你需要在映射文檔中再指定這些接口。例如:

          <class name="CatImpl" proxy="Cat">
          ......
          <subclass name="DomesticCatImpl" proxy="DomesticCat">
          .....
          </subclass>
          </class>

          這里CatImpl實現了Cat接口, DomesticCatImpl實現DomesticCat接口。 在load()iterate()方法中就會返回 CatDomesticCat的代理對象。 (注意list()并不會返回代理對象。)

          Cat cat = (Cat) session.load(CatImpl.class, catid);
          Iterator iter = session.iterate("from CatImpl as cat where cat.name='fritz'");
          Cat fritz = (Cat) iter.next();

          這里,對象之間的關系也將被延遲載入。這就意味著,你應該將屬性聲明為Cat,而不是CatImpl

          但是,在有些方法中是不需要使用代理的。例如:

          • equals()方法,如果持久類沒有重載equals()方法。

          • hashCode()方法,如果持久類沒有重載hashCode()方法。

          • 標志符的getter方法。

          Hibernate將會識別出那些重載了equals()、或hashCode()方法的持久化類。

          20.1.4. 實例化集合和代理(Initializing collections and proxies)

          Session范圍之外訪問未初始化的集合或代理,Hibernate將會拋出LazyInitializationException異常。 也就是說,在分離狀態下,訪問一個實體所擁有的集合,或者訪問其指向代理的屬性時,會引發此異常。

          有時候我們需要保證某個代理或者集合在Session關閉前就已經被初始化了。 當然,我們可以通過強行調用cat.getSex()或者cat.getKittens().size()之類的方法來確保這一點。 但是這樣的程序會造成讀者的疑惑,也不符合通常的代碼規范。

          靜態方法Hibernate.initialized() 為你的應用程序提供了一個便捷的途徑來延遲加載集合或代理。 只要它的Session處于open狀態,Hibernate.initialize(cat) 將會為cat強制對代理實例化。 同樣,Hibernate.initialize( cat.getKittens() ) 對kittens的集合具有同樣的功能。

          還有另外一種選擇,就是保持Session一直處于open狀態,直到所有需要的集合或代理都被載入。 在某些應用架構中,特別是對于那些使用Hibernate進行數據訪問的代碼,以及那些在不同應用層和不同物理進程中使用Hibernate的代碼。 在集合實例化時,如何保證Session處于open狀態經常會是一個問題。有兩種方法可以解決此問題:

          • 在一個基于Web的應用中,可以利用servlet過濾器(filter),在用戶請求(request)結束、頁面生成 結束時關閉Session(這里使用了在展示層保持打開Session模式(Open Session in View)), 當然,這將依賴于應用框架中異常需要被正確的處理。在返回界面給用戶之前,乃至在生成界面過程中發生異常的情況下, 正確關閉Session和結束事務將是非常重要的, Servlet過濾器必須如此訪問Session,才能保證正確使用Session。 我們推薦使用ThreadLocal 變量保存當前的Session (可以參考第 1.4 節 “與Cat同樂”的例子實現)。

          • 在一個擁有單獨業務層的應用中,業務層必須在返回之前,為web層“準備”好其所需的數據集合。這就意味著 業務層應該載入所有表現層/web層所需的數據,并將這些已實例化完畢的數據返回。通常,應用程序應該 為web層所需的每個集合調用Hibernate.initialize()(這個調用必須發生咱session關閉之前); 或者使用帶有FETCH從句,或FetchMode.JOIN的Hibernate查詢, 事先取得所有的數據集合。如果你在應用中使用了Command模式,代替Session Facade , 那么這項任務將會變得簡單的多。

          • 你也可以通過merge()lock()方法,在訪問未實例化的集合(或代理)之前, 為先前載入的對象綁定一個新的Session。 顯然,Hibernate將不會,也不應該自動完成這些任務,因為這將引入一個特殊的事務語義。

          有時候,你并不需要完全實例化整個大的集合,僅需要了解它的部分信息(例如其大小)、或者集合的部分內容。

          你可以使用集合過濾器得到其集合的大小,而不必實例化整個集合:

          ( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()

          這里的createFilter()方法也可以被用來有效的抓取集合的部分內容,而無需實例化整個集合:

          s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();

          20.1.5. 使用批量抓取(Using batch fetching)

          Hibernate可以充分有效的使用批量抓取,也就是說,如果僅一個訪問代理(或集合),那么Hibernate將不載入其他未實例化的代理。 批量抓取是延遲查詢抓取的優化方案,你可以在兩種批量抓取方案之間進行選擇:在類級別和集合級別。

          類/實體級別的批量抓取很容易理解。假設你在運行時將需要面對下面的問題:你在一個Session中載入了25個 Cat實例,每個Cat實例都擁有一個引用成員owner, 其指向Person,而Person類是代理,同時lazy="true"。 如果你必須遍歷整個cats集合,對每個元素調用getOwner()方法,Hibernate將會默認的執行25次SELECT查詢, 得到其owner的代理對象。這時,你可以通過在映射文件的Person屬性,顯式聲明batch-size,改變其行為:

          <class name="Person" batch-size="10">...</class>

          隨之,Hibernate將只需要執行三次查詢,分別為10、10、 5。

          你也可以在集合級別定義批量抓取。例如,如果每個Person都擁有一個延遲載入的Cats集合, 現在,Sesssion中載入了10個person對象,遍歷person集合將會引起10次SELECT查詢, 每次查詢都會調用getCats()方法。如果你在Person的映射定義部分,允許對cats批量抓取, 那么,Hibernate將可以預先抓取整個集合。請看例子:

          <class name="Person">
          <set name="cats" batch-size="3">
          ...
          </set>
          </class>

          如果整個的batch-size是3(筆誤?),那么Hibernate將會分四次執行SELECT查詢, 按照3、3、3、1的大小分別載入數據。這里的每次載入的數據量還具體依賴于當前Session中未實例化集合的個數。

          如果你的模型中有嵌套的樹狀結構,例如典型的帳單-原料結構(bill-of-materials pattern),集合的批量抓取是非常有用的。 (盡管在更多情況下對樹進行讀取時,嵌套集合(nested set)原料路徑(materialized path)(××) 是更好的解決方法。)

          20.1.6. 使用子查詢抓取(Using subselect fetching)

          假若一個延遲集合或單值代理需要抓取,Hibernate會使用一個subselect重新運行原來的查詢,一次性讀入所有的實例。這和批量抓取的實現方法是一樣的,不會有破碎的加載。

          20.1.7. 使用延遲屬性抓取(Using lazy property fetching)

          Hibernate3對單獨的屬性支持延遲抓取,這項優化技術也被稱為組抓取(fetch groups)。 請注意,該技術更多的屬于市場特性。在實際應用中,優化行讀取比優化列讀取更重要。但是,僅載入類的部分屬性在某些特定情況下會有用,例如在原有表中擁有幾百列數據、數據模型無法改動的情況下。

          可以在映射文件中對特定的屬性設置lazy,定義該屬性為延遲載入。

          <class name="Document">
          <id name="id">
          <generator class="native"/>
          </id>
          <property name="name" not-null="true" length="50"/>
          <property name="summary" not-null="true" length="200" lazy="true"/>
          <property name="text" not-null="true" length="2000" lazy="true"/>
          </class>

          屬性的延遲載入要求在其代碼構建時加入二進制指示指令(bytecode instrumentation),如果你的持久類代碼中未含有這些指令, Hibernate將會忽略這些屬性的延遲設置,仍然將其直接載入。

          你可以在Ant的Task中,進行如下定義,對持久類代碼加入“二進制指令。”

          <target name="instrument" depends="compile">
          <taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask">
          <classpath path="${jar.path}"/>
          <classpath path="${classes.dir}"/>
          <classpath refid="lib.class.path"/>
          </taskdef>
          <instrument verbose="true">
          <fileset dir="${testclasses.dir}/org/hibernate/auction/model">
          <include name="*.class"/>
          </fileset>
          </instrument>
          </target>

          還有一種可以優化的方法,它使用HQL或條件查詢的投影(projection)特性,可以避免讀取非必要的列, 這一點至少對只讀事務是非常有用的。它無需在代碼構建時“二進制指令”處理,因此是一個更加值得選擇的解決方法。

          有時你需要在HQL中通過抓取所有屬性,強行抓取所有內容。

          20.2. 二級緩存(The Second Level Cache)

          Hibernate的Session在事務級別進行持久化數據的緩存操作。 當然,也有可能分別為每個類(或集合),配置集群、或JVM級別(SessionFactory級別)的緩存。 你甚至可以為之插入一個集群的緩存。注意,緩存永遠不知道其他應用程序對持久化倉庫(數據庫)可能進行的修改 (即使可以將緩存數據設定為定期失效)。

          默認情況下,Hibernate使用EHCache進行JVM級別的緩存(目前,Hibernate已經廢棄了對JCS的支持,未來版本中將會去掉它)。 你可以通過設置hibernate.cache.provider_class屬性,指定其他的緩存策略, 該緩存策略必須實現org.hibernate.cache.CacheProvider接口。

          表 20.1.  緩存策略提供商(Cache Providers)

          Cache Provider class Type Cluster Safe Query Cache Supported
          Hashtable (not intended for production use) org.hibernate.cache.HashtableCacheProvider memory   yes
          EHCache org.hibernate.cache.EhCacheProvider memory, disk   yes
          OSCache org.hibernate.cache.OSCacheProvider memory, disk   yes
          SwarmCache org.hibernate.cache.SwarmCacheProvider clustered (ip multicast) yes (clustered invalidation)  
          JBoss TreeCache org.hibernate.cache.TreeCacheProvider clustered (ip multicast), transactional yes (replication) yes (clock sync req.)

          20.2.1. 緩存映射(Cache mappings)

          類或者集合映射的“<cache>元素”可以有下列形式:

          <cache
          usage="transactional|read-write|nonstrict-read-write|read-only"  (1)
          />
          (1)

          usage說明了緩存的策略: transactionalread-writenonstrict-read-writeread-only

          另外(首選?), 你可以在hibernate.cfg.xml中指定<class-cache><collection-cache> 元素。

          這里的usage 屬性指明了緩存并發策略(cache concurrency strategy)

          20.2.2. 策略:只讀緩存(Strategy: read only)

          如果你的應用程序只需讀取一個持久化類的實例,而無需對其修改, 那么就可以對其進行只讀 緩存。這是最簡單,也是實用性最好的方法。甚至在集群中,它也能完美地運作。

          <class name="eg.Immutable" mutable="false">
          <cache usage="read-only"/>
          ....
          </class>

          20.2.3.  策略:讀/寫緩存(Strategy: read/write)

          如果應用程序需要更新數據,那么使用讀/寫緩存 比較合適。 如果應用程序要求“序列化事務”的隔離級別(serializable transaction isolation level),那么就決不能使用這種緩存策略。 如果在JTA環境中使用緩存,你必須指定hibernate.transaction.manager_lookup_class屬性的值, 通過它,Hibernate才能知道該應用程序中JTA的TransactionManager的具體策略。 在其它環境中,你必須保證在Session.close()、或Session.disconnect()調用前, 整個事務已經結束。 如果你想在集群環境中使用此策略,你必須保證底層的緩存實現支持鎖定(locking)。Hibernate內置的緩存策略并不支持鎖定功能。

          <class name="eg.Cat" .... >
          <cache usage="read-write"/>
          ....
          <set name="kittens" ... >
          <cache usage="read-write"/>
          ....
          </set>
          </class>

          20.2.4.  策略:非嚴格讀/寫緩存(Strategy: nonstrict read/write)

          如果應用程序只偶爾需要更新數據(也就是說,兩個事務同時更新同一記錄的情況很不常見),也不需要十分嚴格的事務隔離, 那么比較適合使用非嚴格讀/寫緩存策略。如果在JTA環境中使用該策略, 你必須為其指定hibernate.transaction.manager_lookup_class屬性的值, 在其它環境中,你必須保證在Session.close()、或Session.disconnect()調用前, 整個事務已經結束。

          20.2.5.  策略:事務緩存(transactional)

          Hibernate的事務緩存策略提供了全事務的緩存支持, 例如對JBoss TreeCache的支持。這樣的緩存只能用于JTA環境中,你必須指定 為其hibernate.transaction.manager_lookup_class屬性。

          沒有一種緩存提供商能夠支持上列的所有緩存并發策略。下表中列出了各種提供器、及其各自適用的并發策略。

          表 20.2.  各種緩存提供商對緩存并發策略的支持情況(Cache Concurrency Strategy Support)

          Cache read-only nonstrict-read-write read-write transactional
          Hashtable (not intended for production use) yes yes yes  
          EHCache yes yes yes  
          OSCache yes yes yes  
          SwarmCache yes yes    
          JBoss TreeCache yes     yes

          20.3.  管理緩存(Managing the caches)

          無論何時,當你給save()update()saveOrUpdate()方法傳遞一個對象時,或使用load()get()list()iterate()scroll()方法獲得一個對象時, 該對象都將被加入到Session的內部緩存中。

          當隨后flush()方法被調用時,對象的狀態會和數據庫取得同步。 如果你不希望此同步操作發生,或者你正處理大量對象、需要對有效管理內存時,你可以調用evict() 方法,從一級緩存中去掉這些對象及其集合。

          ScrollableResult cats = sess.createQuery("from Cat as cat").scroll(); //a huge result set
          while ( cats.next() ) {
          Cat cat = (Cat) cats.get(0);
          doSomethingWithACat(cat);
          sess.evict(cat);
          }

          Session還提供了一個contains()方法,用來判斷某個實例是否處于當前session的緩存中。

          如若要把所有的對象從session緩存中徹底清除,則需要調用Session.clear()

          對于二級緩存來說,在SessionFactory中定義了許多方法, 清除緩存中實例、整個類、集合實例或者整個集合。

          sessionFactory.evict(Cat.class, catId); //evict a particular Cat
          sessionFactory.evict(Cat.class);  //evict all Cats
          sessionFactory.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens
          sessionFactory.evictCollection("Cat.kittens"); //evict all kitten collections

          CacheMode參數用于控制具體的Session如何與二級緩存進行交互。

          • CacheMode.NORMAL - 從二級緩存中讀、寫數據。

          • CacheMode.GET - 從二級緩存中讀取數據,僅在數據更新時對二級緩存寫數據。

          • CacheMode.PUT - 僅向二級緩存寫數據,但不從二級緩存中讀數據。

          • CacheMode.REFRESH - 僅向二級緩存寫數據,但不從二級緩存中讀數據。通過 hibernate.cache.use_minimal_puts的設置,強制二級緩存從數據庫中讀取數據,刷新緩存內容。

          如若需要查看二級緩存或查詢緩存區域的內容,你可以使用統計(Statistics) API。

          Map cacheEntries = sessionFactory.getStatistics()
          .getSecondLevelCacheStatistics(regionName)
          .getEntries();

          此時,你必須手工打開統計選項。可選的,你可以讓Hibernate更人工可讀的方式維護緩存內容。

          hibernate.generate_statistics true
          hibernate.cache.use_structured_entries true

          20.4. 查詢緩存(The Query Cache)

          查詢的結果集也可以被緩存。只有當經常使用同樣的參數進行查詢時,這才會有些用處。 要使用查詢緩存,首先你必須打開它:

          hibernate.cache.use_query_cache true

          該設置將會創建兩個緩存區域 - 一個用于保存查詢結果集(org.hibernate.cache.StandardQueryCache); 另一個則用于保存最近查詢的一系列表的時間戳(org.hibernate.cache.UpdateTimestampsCache)。 請注意:在查詢緩存中,它并不緩存結果集中所包含的實體的確切狀態;它只緩存這些實體的標識符屬性的值、以及各值類型的結果。 所以查詢緩存通常會和二級緩存一起使用。

          絕大多數的查詢并不能從查詢緩存中受益,所以Hibernate默認是不進行查詢緩存的。如若需要進行緩存,請調用 Query.setCacheable(true)方法。這個調用會讓查詢在執行過程中時先從緩存中查找結果, 并將自己的結果集放到緩存中去。

          如果你要對查詢緩存的失效政策進行精確的控制,你必須調用Query.setCacheRegion()方法, 為每個查詢指定其命名的緩存區域。

          List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger")
          .setEntity("blogger", blogger)
          .setMaxResults(15)
          .setCacheable(true)
          .setCacheRegion("frontpages")
          .list();

          如果查詢需要強行刷新其查詢緩存區域,那么你應該調用Query.setCacheMode(CacheMode.REFRESH)方法。 這對在其他進程中修改底層數據(例如,不通過Hibernate修改數據),或對那些需要選擇性更新特定查詢結果集的情況特別有用。 這是對SessionFactory.evictQueries()的更為有效的替代方案,同樣可以清除查詢緩存區域。

          20.5.  理解集合性能(Understanding Collection performance)

          前面我們已經對集合進行了足夠的討論。本段中,我們將著重講述集合在運行時的事宜。

          20.5.1.  分類(Taxonomy)

          Hibernate定義了三種基本類型的集合:

          • 值數據集合

          • 一對多關聯

          • 多對多關聯

          這個分類是區分了不同的表和外鍵關系類型,但是它沒有告訴我們關系模型的所有內容。 要完全理解他們的關系結構和性能特點,我們必須同時考慮“用于Hibernate更新或刪除集合行數據的主鍵的結構”。 因此得到了如下的分類:

          • 有序集合類

          • 集合(sets)

          • 包(bags)

          所有的有序集合類(maps, lists, arrays)都擁有一個由<key><index>組成的主鍵。 這種情況下集合類的更新是非常高效的——主鍵已經被有效的索引,因此當Hibernate試圖更新或刪除一行時,可以迅速找到該行數據。

          集合(sets)的主鍵由<key>和其他元素字段構成。 對于有些元素類型來說,這很低效,特別是組合元素或者大文本、大二進制字段; 數據庫可能無法有效的對復雜的主鍵進行索引。 另一方面,對于一對多、多對多關聯,特別是合成的標識符來說,集合也可以達到同樣的高效性能。( 附注:如果你希望SchemaExport為你的<set>創建主鍵, 你必須把所有的字段都聲明為not-null="true"。)

          <idbag>映射定義了代理鍵,因此它總是可以很高效的被更新。事實上, <idbag>擁有著最好的性能表現。

          Bag是最差的。因為bag允許重復的元素值,也沒有索引字段,因此不可能定義主鍵。 Hibernate無法判斷出重復的行。當這種集合被更改時,Hibernate將會先完整地移除 (通過一個(in a single DELETE))整個集合,然后再重新創建整個集合。 因此Bag是非常低效的。

          請注意:對于一對多關聯來說,“主鍵”很可能并不是數據庫表的物理主鍵。 但就算在此情況下,上面的分類仍然是有用的。(它仍然反映了Hibernate在集合的各數據行中是如何進行“定位”的。)

          20.5.2.  Lists, maps 和sets用于更新效率最高

          根據我們上面的討論,顯然有序集合類型和大多數set都可以在增加、刪除、修改元素中擁有最好的性能。

          可論證的是對于多對多關聯、值數據集合而言,有序集合類比集合(set)有一個好處。因為Set的內在結構, 如果“改變”了一個元素,Hibernate并不會更新(UPDATE)這一行。 對于Set來說,只有在插入(INSERT)刪除(DELETE) 操作時“改變”才有效。再次強調:這段討論對“一對多關聯”并不適用。

          注意到數組無法延遲載入,我們可以得出結論,list, map和idbags是最高效的(非反向)集合類型,set則緊隨其后。 在Hibernate中,set應該時最通用的集合類型,這時因為“set”的語義在關系模型中是最自然的。

          但是,在設計良好的Hibernate領域模型中,我們通常可以看到更多的集合事實上是帶有inverse="true" 的一對多的關聯。對于這些關聯,更新操作將會在多對一的這一端進行處理。因此對于此類情況,無需考慮其集合的更新性能。

          20.5.3.  Bag和list是反向集合類中效率最高的

          在把bag扔進水溝之前,你必須了解,在一種情況下,bag的性能(包括list)要比set高得多: 對于指明了inverse="true"的集合類(比如說,標準的雙向的一對多關聯), 我們可以在未初始化(fetch)包元素的情況下直接向bag或list添加新元素! 這是因為Collection.add())或者Collection.addAll() 方法 對bag或者List總是返回true(這點與與Set不同)。因此對于下面的相同代碼來說,速度會快得多。

          Parent p = (Parent) sess.load(Parent.class, id);
          Child c = new Child();
          c.setParent(p);
          p.getChildren().add(c);  //no need to fetch the collection!
          sess.flush();

          20.5.4.  一次性刪除(One shot delete)

          偶爾的,逐個刪除集合類中的元素是相當低效的。Hibernate并沒那么笨, 如果你想要把整個集合都刪除(比如說調用list.clear()),Hibernate只需要一個DELETE就搞定了。

          假設我們在一個長度為20的集合類中新增加了一個元素,然后再刪除兩個。 Hibernate會安排一條INSERT語句和兩條DELETE語句(除非集合類是一個bag)。 這當然是顯而易見的。

          但是,假設我們刪除了18個數據,只剩下2個,然后新增3個。則有兩種處理方式:

          • 逐一的刪除這18個數據,再新增三個;

          • 刪除整個集合類(只用一句DELETE語句),然后增加5個數據。

          Hibernate還沒那么聰明,知道第二種選擇可能會比較快。 (也許讓Hibernate不這么聰明也是好事,否則可能會引發意外的“數據庫觸發器”之類的問題。)

          幸運的是,你可以強制使用第二種策略。你需要取消原來的整個集合類(解除其引用), 然后再返回一個新的實例化的集合類,只包含需要的元素。有些時候這是非常有用的。

          顯然,一次性刪除并不適用于被映射為inverse="true"的集合。

          20.6.  監測性能(Monitoring performance)

          沒有監測和性能參數而進行優化是毫無意義的。Hibernate為其內部操作提供了一系列的示意圖,因此可以從 每個SessionFactory抓取其統計數據。

          20.6.1.  監測SessionFactory

          你可以有兩種方式訪問SessionFactory的數據記錄,第一種就是自己直接調用 sessionFactory.getStatistics()方法讀取、顯示統計數據。

          此外,如果你打開StatisticsService MBean選項,那么Hibernate則可以使用JMX技術 發布其數據記錄。你可以讓應用中所有的SessionFactory同時共享一個MBean,也可以每個 SessionFactory分配一個MBean。下面的代碼即是其演示代碼:

          // MBean service registration for a specific SessionFactory
          Hashtable tb = new Hashtable();
          tb.put("type", "statistics");
          tb.put("sessionFactory", "myFinancialApp");
          ObjectName on = new ObjectName("hibernate", tb); // MBean object name
          StatisticsService stats = new StatisticsService(); // MBean implementation
          stats.setSessionFactory(sessionFactory); // Bind the stats to a SessionFactory
          server.registerMBean(stats, on); // Register the Mbean on the server
          // MBean service registration for all SessionFactory's
          Hashtable tb = new Hashtable();
          tb.put("type", "statistics");
          tb.put("sessionFactory", "all");
          ObjectName on = new ObjectName("hibernate", tb); // MBean object name
          StatisticsService stats = new StatisticsService(); // MBean implementation
          server.registerMBean(stats, on); // Register the MBean on the server

          TODO:仍需要說明的是:在第一個例子中,我們直接得到和使用MBean;而在第二個例子中,在使用MBean之前 我們則需要給出SessionFactory的JNDI名,使用hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name") 得到SessionFactory,然后將MBean保存于其中。

          你可以通過以下方法打開或關閉SessionFactory的監測功能:

          • 在配置期間,將hibernate.generate_statistics設置為truefalse

          • 在運行期間,則可以可以通過sf.getStatistics().setStatisticsEnabled(true)hibernateStatsBean.setStatisticsEnabled(true)

          你也可以在程序中調用clear()方法重置統計數據,調用logSummary() 在日志中記錄(info級別)其總結。

          20.6.2.  數據記錄(Metrics)

          Hibernate提供了一系列數據記錄,其記錄的內容包括從最基本的信息到與具體場景的特殊信息。所有的測量值都可以由 Statistics接口進行訪問,主要分為三類:

          • 使用Session的普通數據記錄,例如打開的Session的個數、取得的JDBC的連接數等;

          • 實體、集合、查詢、緩存等內容的統一數據記錄

          • 和具體實體、集合、查詢、緩存相關的詳細數據記錄

          例如:你可以檢查緩存的命中成功次數,緩存的命中失敗次數,實體、集合和查詢的使用概率,查詢的平均時間等。請注意 Java中時間的近似精度是毫秒。Hibernate的數據精度和具體的JVM有關,在有些平臺上其精度甚至只能精確到10秒。

          你可以直接使用getter方法得到全局數據記錄(例如,和具體的實體、集合、緩存區無關的數據),你也可以在具體查詢中通過標記實體名、 或HQL、SQL語句得到某實體的數據記錄。請參考StatisticsEntityStatisticsCollectionStatisticsSecondLevelCacheStatistics、 和QueryStatistics的API文檔以抓取更多信息。下面的代碼則是個簡單的例子:

          Statistics stats = HibernateUtil.sessionFactory.getStatistics();
          double queryCacheHitCount  = stats.getQueryCacheHitCount();
          double queryCacheMissCount = stats.getQueryCacheMissCount();
          double queryCacheHitRatio =
          queryCacheHitCount / (queryCacheHitCount + queryCacheMissCount);
          log.info("Query Hit ratio:" + queryCacheHitRatio);
          EntityStatistics entityStats =
          stats.getEntityStatistics( Cat.class.getName() );
          long changes =
          entityStats.getInsertCount()
          + entityStats.getUpdateCount()
          + entityStats.getDeleteCount();
          log.info(Cat.class.getName() + " changed " + changes + "times"  );

          如果你想得到所有實體、集合、查詢和緩存區的數據,你可以通過以下方法獲得實體、集合、查詢和緩存區列表: getQueries()getEntityNames()getCollectionRoleNames()getSecondLevelCacheRegionNames()

          Hibernate程序性能優化的考慮要點
          MENGCHUCHEN

          本文依照HIBERNATE幫助文檔,一些網絡書籍及項目經驗整理而成,只提供要點和思路,具體做法可以留言探討,或是找一些更詳細更有針對性的資料。

          初用HIBERNATE的人也許都遇到過性能問題,實現同一功能,用HIBERNATE與用JDBC性能相差十幾倍很正常,如果不及早調整,很可能影響整個項目的進度。

          大體上,對于HIBERNATE性能調優的主要考慮點如下:

          • 數據庫設計調整
          • HQL優化
          • API的正確使用(如根據不同的業務類型選用不同的集合及查詢API)
          • 主配置參數(日志,查詢緩存,fetch_size, batch_size等)
          • 映射文件優化(ID生成策略,二級緩存,延遲加載,關聯優化)
          • 一級緩存的管理
          • 針對二級緩存,還有許多特有的策略
          • 事務控制策略。

          1、 數據庫設計

          a) 降低關聯的復雜性

          b) 盡量不使用聯合主鍵

          c) ID的生成機制,不同的數據庫所提供的機制并不完全一樣

          d) 適當的冗余數據,不過分追求高范式

          2、 HQL優化

          HQL如果拋開它同HIBERNATE本身一些緩存機制的關聯,HQL的優化技巧同普通的SQL優化技巧一樣,可以很容易在網上找到一些經驗之談。

          3、 主配置

          a) 查詢緩存,同下面講的緩存不太一樣,它是針對HQL語句的緩存,即完全一樣的語句再次執行時可以利用緩存數據。但是,查詢緩存在一個交易系統(數據變更頻繁,查詢條件相同的機率并不大)中可能會起反作用:它會白白耗費大量的系統資源但卻難以派上用場。

          b) fetch_size,同JDBC的相關參數作用類似,參數并不是越大越好,而應根據業務特征去設置

          c) batch_size同上。

          d) 生產系統中,切記要關掉SQL語句打印。

          4、 緩存

          a) 數據庫級緩存:這級緩存是最高效和安全的,但不同的數據庫可管理的層次并不一樣,比如,在ORACLE中,可以在建表時指定將整個表置于緩存當中。

          b) SESSION緩存:在一個HIBERNATE SESSION有效,這級緩存的可干預性不強,大多于HIBERNATE自動管理,但它提供清除緩存的方法,這在大批量增加/更新操作是有效的。比如,同時增加十萬條記錄,按常規方式進行,很可能會發現OutofMemeroy的異常,這時可能需要手動清除這一級緩存:Session.evict以及Session.clear

          c) 應用緩存:在一個SESSIONFACTORY中有效,因此也是優化的重中之重,因此,各類策略也考慮的較多,在將數據放入這一級緩存之前,需要考慮一些前提條件:

          i. 數據不會被第三方修改(比如,是否有另一個應用也在修改這些數據?)

          ii. 數據不會太大

          iii. 數據不會頻繁更新(否則使用CACHE可能適得其反)

          iv. 數據會被頻繁查詢

          v. 數據不是關鍵數據(如涉及錢,安全等方面的問題)。

          緩存有幾種形式,可以在映射文件中配置:read-only(只讀,適用于很少變更的靜態數據/歷史數據),nonstrict-read-write,read-write(比較普遍的形式,效率一般),transactional(JTA中,且支持的緩存產品較少)

          d) 分布式緩存:同c)的配置一樣,只是緩存產品的選用不同,在目前的HIBERNATE中可供選擇的不多,oscache, jboss cache,目前的大多數項目,對它們的用于集群的使用(特別是關鍵交易系統)都持保守態度。在集群環境中,只利用數據庫級的緩存是最安全的。

          5、 延遲加載

          a) 實體延遲加載:通過使用動態代理實現

          b) 集合延遲加載:通過實現自有的SET/LIST,HIBERNATE提供了這方面的支持

          c) 屬性延遲加載:

          6、 方法選用

          a) 完成同樣一件事,HIBERNATE提供了可供選擇的一些方式,但具體使用什么方式,可能用性能/代碼都會有影響。顯示,一次返回十萬條記錄(List/Set/Bag/Map等)進行處理,很可能導致內存不夠的問題,而如果用基于游標(ScrollableResults)或Iterator的結果集,則不存在這樣的問題。

          b) Session的load/get方法,前者會使用二級緩存,而后者則不使用。

          c) Query和list/iterator,如果去仔細研究一下它們,你可能會發現很多有意思的情況,二者主要區別(如果使用了Spring,在HibernateTemplate中對應find,iterator方法):

          i. list只能利用查詢緩存(但在交易系統中查詢緩存作用不大),無法利用二級緩存中的單個實體,但list查出的對象會寫入二級緩存,但它一般只生成較少的執行SQL語句,很多情況就是一條(無關聯)。

          ii. iterator則可以利用二級緩存,對于一條查詢語句,它會先從數據庫中找出所有符合條件的記錄的ID,再通過ID去緩存找,對于緩存中沒有的記錄,再構造語句從數據庫中查出,因此很容易知道,如果緩存中沒有任何符合條件的記錄,使用iterator會產生N+1條SQL語句(N為符合條件的記錄數)

          iii. 通過iterator,配合緩存管理API,在海量數據查詢中可以很好的解決內存問題,如:

           while(it.hasNext()){

            YouObject object = (YouObject)it.next();

            session.evict(youObject);

            sessionFactory.evice(YouObject.class, youObject.getId());

            }

          如果用list方法,很可能就出OutofMemory錯誤了。

          iv. 通過上面的說明,我想你應該知道如何去使用這兩個方法了。

          7、 集合的選用

          在HIBERNATE 3.1文檔的“19.5. Understanding Collection performance”中有詳細的說明。

          8、 事務控制

          事務方面對性能有影響的主要包括:事務方式的選用,事務隔離級別以及鎖的選用

          a) 事務方式選用:如果不涉及多個事務管理器事務的話,不需要使用JTA,只有JDBC的事務控制就可以。

          b) 事務隔離級別:參見標準的SQL事務隔離級別

          c) 鎖的選用:悲觀鎖(一般由具體的事務管理器實現),對于長事務效率低,但安全。樂觀鎖(一般在應用級別實現),如在HIBERNATE中可以定義VERSION字段,顯然,如果有多個應用操作數據,且這些應用不是用同一種樂觀鎖機制,則樂觀鎖會失效。因此,針對不同的數據應有不同的策略,同前面許多情況一樣,很多時候我們是在效率與安全/準確性上找一個平衡點,無論如何,優化都不是一個純技術的問題,你應該對你的應用和業務特征有足夠的了解。

          9、 批量操作

          即使是使用JDBC,在進行大批數據更新時,BATCH與不使用BATCH有效率上也有很大的差別。我們可以通過設置batch_size來讓其支持批量操作。

          舉個例子,要批量刪除某表中的對象,如“delete Account”,打出來的語句,會發現HIBERNATE找出了所有ACCOUNT的ID,再進行刪除,這主要是為了維護二級緩存,這樣效率肯定高不了,在后續的版本中增加了bulk delete/update,但這也無法解決緩存的維護問題。也就是說,由于有了二級緩存的維護問題,HIBERNATE的批量操作效率并不盡如人意!

          從前面許多要點可以看出,很多時候我們是在效率與安全/準確性上找一個平衡點,無論如何,優化都不是一個純技術的問題,你應該對你的應用和業務特征有足夠的了解,一般的,優化方案應在架構設計期就基本確定,否則可能導致沒必要的返工,致使項目延期,而作為架構師和項目經理,還要面對開發人員可能的抱怨,必竟,我們對用戶需求更改的控制力不大,但技術/架構風險是應該在初期意識到并制定好相關的對策。

          還有一點要注意,應用層的緩存只是錦上添花,永遠不要把它當救命稻草,應用的根基(數據庫設計,算法,高效的操作語句,恰當API的選擇等)才是最重要的。

          posted on 2007-05-15 15:58 ???MengChuChen 閱讀(3333) 評論(2)  編輯  收藏 所屬分類: hibernate

          FeedBack:
          # re: HIbernate提升性能
          2008-08-30 17:05 | sqxy
          b) Session的load/get方法,前者會使用二級緩存,而后者則不使用。
          =============================

          確定?  回復  更多評論
            
          # re: HIbernate提升性能
          2008-08-30 17:07 | sqxy
          據我試驗,load和get的區別是在查詢不到內容時,一個拋異常,一個返回null。
          還有就是,load延遲加載,get是即時加載的。
          至于2級緩存,我做過試驗,get同樣利用到了二級緩存。  回復  更多評論
            
          主站蜘蛛池模板: 蚌埠市| 麻城市| 安宁市| 榆社县| 安达市| 六盘水市| 政和县| 长沙市| 冕宁县| 日土县| 亳州市| 苍南县| 兴安盟| 扎鲁特旗| 晋中市| 即墨市| 盘山县| 迁安市| 察隅县| 沈丘县| 涟水县| 伊宁市| 德安县| 阿坝| 潮安县| 乌海市| 仁寿县| 黑山县| 惠水县| 星座| 安溪县| 铜梁县| 澜沧| 辽宁省| 靖边县| 泾源县| 康平县| 平阳县| 依兰县| 侯马市| 泰兴市|