[轉(zhuǎn)]加速你的Hibernate引擎(下)

          原文: http://www.infoq.com/cn/articles/hibernate_tuning-ii

          4.6 HQL調(diào)優(yōu)

          4.6.1 索引調(diào)優(yōu)

          HQL看起來(lái)和SQL很相似。從HQL的WHERE子句中通常可以猜到相應(yīng)的SQL WHERE子句。WHERE子句中的字段決定了數(shù)據(jù)庫(kù)將選擇的索引。

          大多數(shù)Hibernate開(kāi)發(fā)者所常犯的一個(gè)錯(cuò)誤是無(wú)論何時(shí),當(dāng)需要新WHERE子句的時(shí)候都會(huì)創(chuàng)建一個(gè)新的索引。因?yàn)樗饕龝?huì)帶來(lái)額外的數(shù)據(jù)更新開(kāi)銷(xiāo),所以應(yīng)該爭(zhēng)取創(chuàng)建少量索引來(lái)覆蓋盡可能多的查詢(xún)。
          4.1節(jié)讓你使用一個(gè)集合來(lái)處理所有可能的數(shù)據(jù)搜索條件。如果這不太實(shí)際,那么你可以使用后端剖析工具來(lái)創(chuàng)建一個(gè)針對(duì)應(yīng)用程序涉及的所有SQL的集合。基于那些搜索條件的分類(lèi),你最終會(huì)得到一個(gè)小的索引集。與此同時(shí),還可以嘗試向WHERE子句中添加額外的謂語(yǔ)來(lái)匹配其他WHERE子句。

          范例7

          有兩個(gè)UI搜索器和一個(gè)后端守護(hù)進(jìn)程搜索器來(lái)搜索名為iso_deals的表。第一個(gè)UI搜索器在unexpectedFlag、dealStatus、tradeDate和isold屬性上有謂語(yǔ)。

          第二個(gè)UI搜索器基于用戶(hù)鍵入的過(guò)濾器,其中包括的內(nèi)容除tradeDate和isold以外還有其他屬性。開(kāi)始時(shí)所有這些過(guò)濾器屬性都是可選的。
          后端搜索器基于isold、participantCode和transactionType屬性。
          經(jīng)過(guò)進(jìn)一步業(yè)務(wù)分析,發(fā)現(xiàn)第二個(gè)UI搜索器實(shí)際是基于一些隱式的unexpectedFlag和dealStatus值來(lái)選擇數(shù)據(jù)的。我們還讓tradeDate成為過(guò)濾器的必要屬性(為了使用數(shù)據(jù)庫(kù)索引,每個(gè)搜索過(guò)濾器都應(yīng)該有必要屬性)。

          鑒于這一點(diǎn),我們依次使用unexpectedFlag、dealStatus、tradeDate和isold構(gòu)造了一個(gè)復(fù)合索引。兩個(gè)UI搜索器都能共用它。(順序很重要,如果你的謂語(yǔ)以不同的順序指定這些屬性或在它們前羅列了其他屬性,數(shù)據(jù)庫(kù)就不會(huì)選擇該復(fù)合索引。)

          后端搜索器和UI搜索器區(qū)別太大,因此我們不得不為它構(gòu)造另一個(gè)復(fù)合索引,依次使用isold、participantCode和transactionType。

          4.6.2綁定參數(shù) vs.字符串拼接

          既可以使用綁定參數(shù)構(gòu)造HQL的WHERE子句,也可以使用字符串拼接的方法,該決定對(duì)性能會(huì)有一定影響。使用綁定參數(shù)的原因是讓數(shù)據(jù)庫(kù)一次解析SQL,對(duì)后續(xù)的重復(fù)請(qǐng)求復(fù)用生成好的執(zhí)行計(jì)劃,這樣做節(jié)省了CPU時(shí)間和內(nèi)存。然而,為達(dá)到最優(yōu)的數(shù)據(jù)訪問(wèn)效率,不同的綁定值可能需要不同的SQL執(zhí)行計(jì)劃。

          例如,一小段數(shù)據(jù)范圍可能只返回?cái)?shù)據(jù)總量的5%,而一大段數(shù)據(jù)范圍可能返回?cái)?shù)據(jù)總量的90%。前者使用索引更好,而后者則最好使用全表掃描。

          建議OLTP使用綁定參數(shù),數(shù)據(jù)倉(cāng)庫(kù)使用字符串拼接,因?yàn)镺LTP通常在一個(gè)事務(wù)中重復(fù)插入和更新數(shù)據(jù),只取少量數(shù)據(jù);數(shù)據(jù)倉(cāng)庫(kù)通常只有少量SQL查詢(xún),有一個(gè)確定的執(zhí)行計(jì)劃比節(jié)省CPU時(shí)間和內(nèi)存更為重要。

          要是你知道你的OLTP搜索對(duì)不同綁定值應(yīng)該使用相同執(zhí)行計(jì)劃又該怎么辦呢?

          Oracle 9i及以后版本在第一次調(diào)用綁定參數(shù)并生成執(zhí)行計(jì)劃時(shí)能探出參數(shù)值。后續(xù)調(diào)用不會(huì)再探測(cè),而是重用之前的執(zhí)行計(jì)劃。

          4.6.3聚合及排序

          你可以在數(shù)據(jù)庫(kù)中進(jìn)行聚合和“order by”,也可以在應(yīng)用程序的服務(wù)層中事先加載所有數(shù)據(jù)然后做聚合和“order by”操作。推薦使用前者,因?yàn)閿?shù)據(jù)庫(kù)在這方面通常會(huì)比你的應(yīng)用程序做得好。此外,這樣做還能節(jié)省網(wǎng)絡(luò)帶寬,這也是一種擁有跨數(shù)據(jù)庫(kù)移植性的做法。

          當(dāng)你的應(yīng)用程序?qū)?shù)據(jù)聚合和排序有HQL不支持的特定業(yè)務(wù)規(guī)則時(shí)除外。

          4.6.4覆蓋抓取策略

          詳見(jiàn)4.7.1節(jié)

          4.6.5本地查詢(xún)

          本地查詢(xún)調(diào)優(yōu)其實(shí)并不直接與HQL有關(guān)。但HQL的確可以讓你直接向底層數(shù)據(jù)庫(kù)傳遞本地查詢(xún)。我們并不建議這么做,因?yàn)楸镜夭樵?xún)?cè)跀?shù)據(jù)庫(kù)間不可移植。

          4.7抓取策略調(diào)優(yōu)

          抓取策略決定了在應(yīng)用程序需要訪問(wèn)關(guān)聯(lián)對(duì)象時(shí),Hibernate以何種方式以及何時(shí)獲取關(guān)聯(lián)對(duì)象。HRD中的第20章“改善性能”對(duì)該主題作了很好的闡述,我們?cè)诖藢㈥P(guān)注它的使用方法。

          4.7.1覆蓋抓取策略

          不同的用戶(hù)可能會(huì)有不同的數(shù)據(jù)抓取要求。Hibernate允許在兩個(gè)地方定義數(shù)據(jù)抓取策略,一處是在映射元數(shù)據(jù)中,另一處是在HQL或Criteria中覆蓋它。

          常見(jiàn)的做法是基于主要的抓取用例在映射元數(shù)據(jù)中定義默認(rèn)抓取策略,針對(duì)少數(shù)用例在HQL和Criteria中覆蓋抓取策略。

          假設(shè)pojoA和pojoB是父子關(guān)系實(shí)例。如果根據(jù)業(yè)務(wù)規(guī)則,只是偶爾需要從實(shí)體兩端加載數(shù)據(jù),那你可以聲明一個(gè)延遲加載集合或代理抓取(proxy fetching)。當(dāng)你需要從實(shí)體兩端獲取數(shù)據(jù)時(shí),可以用立即抓取(eager fetching)覆蓋默認(rèn)策略,例如使用HQL或Criteria配置連接抓取(join fetching)。

          另一方面,如果業(yè)務(wù)規(guī)則在大多數(shù)時(shí)候需要從實(shí)體兩端加載數(shù)據(jù),那么你可以聲明立即抓取并在Criteria中設(shè)置延遲加載集合或代理抓取來(lái)覆蓋它(HQL目前還不支持這樣的覆蓋)。

          4.7.2 N+1模式或是反模式?

          select抓取會(huì)導(dǎo)致N+1問(wèn)題。如果你知道自己總是需要從關(guān)聯(lián)中加載數(shù)據(jù),那么就該始終使用連接抓取。在下面兩個(gè)場(chǎng)景中,你可能會(huì)把N+1視為一種模式而非反模式。

          第一種場(chǎng)景,你不知道用戶(hù)是否會(huì)訪問(wèn)關(guān)聯(lián)對(duì)象。如果他/她沒(méi)有訪問(wèn),那么你贏了;否則你仍然需要額外的N次select SQL語(yǔ)句。這是一種令人左右為難的局面。

          第二種場(chǎng)景,pojoA和很多其他POJO有one-to-many關(guān)聯(lián),例如pojoB和pojoC。使用立即的內(nèi)連接或外連接抓取會(huì)在結(jié)果集中將pojoA重復(fù)很多次。當(dāng)pojoA中有很多非空屬性時(shí),你不得不將大量數(shù)據(jù)加載到持久層中。這種加載需要很多時(shí)間,既有網(wǎng)絡(luò)帶寬的原因,如果Hibernate的會(huì)話(huà)是有狀態(tài)的,其中也會(huì)有會(huì)話(huà)緩存的原因(內(nèi)存消耗和GC暫停)。

          如果你有一個(gè)很長(zhǎng)的one-to-many關(guān)聯(lián)鏈,例如從pojoA到pojoB到pojoC以此類(lèi)推,情況也是類(lèi)似的。

          你也許會(huì)去使用HQL中的DISTINCT關(guān)鍵字或Cirteria中的distinct功能或是Java的Set接口來(lái)消除重復(fù)數(shù)據(jù)。但所有這些都是在Hibernate(在持久層)中實(shí)現(xiàn)的,而非數(shù)據(jù)庫(kù)中。

          如果基于你的網(wǎng)絡(luò)和內(nèi)存配置的測(cè)試表明N+1性能更好,那么你可以使用批量抓取、subselect抓取或二級(jí)緩存來(lái)做進(jìn)一步調(diào)優(yōu)。

          范例8

          以下是一個(gè)使用批量抓取的HBM文件片段:

          <class name="pojoA" table="pojoA">
          …
          <set name="pojoBs" fetch="select" batch-size="10">
          
          <key column="pojoa_id"/>
          …
          </set>
          </class> 
          

          以下是多端pojoB生成的SQL:

          selectfrom pojoB where pojoa_id in(?,?,?,?,?, ?,?,?,?,?);

          問(wèn)號(hào)數(shù)量與batch-size值相等。因此N次額外的關(guān)于pojoB的select SQL語(yǔ)句被減少到了N/10次。

          如果將fetch="select"替換成fetch="subselect",pojoB生成的SQL語(yǔ)句就是這樣的:

          selectfrom pojoB where pojoa_id in(select id from pojoA where …); 

          盡管N次額外的select減少到1次,但這只在重復(fù)運(yùn)行pojoA的查詢(xún)開(kāi)銷(xiāo)很低時(shí)才有好處。

          如果pojoA中的pojoB集合很穩(wěn)定,或pojoB有pojoA的many-to-one關(guān)聯(lián),而且pojoA是只讀引用數(shù)據(jù),那么你可以使用二級(jí)緩存來(lái)緩存pojoA以消除N+1問(wèn)題(4.8.1節(jié)中有一個(gè)例子)。

          4.7.3延遲屬性抓取

          除非有一張擁有很多你不需要的字段的遺留表,否則不應(yīng)該使用這種抓取策略,因?yàn)樗难舆t屬性分組會(huì)帶來(lái)額外的SQL。

          在業(yè)務(wù)分析和設(shè)計(jì)過(guò)程中,你應(yīng)該將不同數(shù)據(jù)獲取或修改分組放到不同的領(lǐng)域?qū)ο髮?shí)體中,而不是使用這種抓取策略。

          如果不能重新設(shè)計(jì)遺留表,可以使用HQL或Criteria提供的投影功能來(lái)獲取數(shù)據(jù)。

          4.8 二級(jí)緩存調(diào)優(yōu)

          HRD第20.2節(jié) “二級(jí)緩存”中的描述對(duì)大多數(shù)開(kāi)發(fā)者來(lái)說(shuō)過(guò)于簡(jiǎn)單,無(wú)法做出選擇。3.3版及以后版本不再推薦使用基于“CacheProvider”的緩存,而用基于“RegionFactory”的緩存,這也讓人更糊涂了。但是就算是最新的3.5參考文檔也沒(méi)有提及如何使用新緩存方法。

          出于下述考慮,我們將繼續(xù)關(guān)注于老方法:

          • 所有流行的Hibernate二級(jí)緩存提供商中只有JBoss Cache 2Infinispan 4Ehcache 2支持新方法。OSCacheSwarmCacheCoherenceGigaspaces XAP-Data Grid只支持老方法。
          • 兩種方法共用相同的<cache>配置。例如,它們?nèi)耘f使用相同的usage屬性值“transactional|read-write|nonstrict-read-write|read-only”。
          • 多個(gè)cache-region適配器仍然內(nèi)置老方法的支持,理解它能幫助你快速理解新方法。

          4.8.1 基于CacheProvider的緩存機(jī)制

          理解該機(jī)制是做出合理選擇的關(guān)鍵。關(guān)鍵的類(lèi)/接口是CacheConcurrencyStrategy和它針對(duì)4中不同緩存使用的實(shí)現(xiàn)類(lèi),還有EntityUpdate/Delete/InsertAction。

          針對(duì)并發(fā)緩存訪問(wèn),有三種實(shí)現(xiàn)模式:

          • 針對(duì)“read-only”的只讀模式。

            無(wú)論是鎖還是事務(wù)都沒(méi)影響,因?yàn)榫彺孀詳?shù)據(jù)從數(shù)據(jù)庫(kù)加載后就不會(huì)改變。

          • 針對(duì)“read-write”和“nonstrict-read-write”的非事務(wù)感知(non-transaction-aware)讀寫(xiě)模式。

            對(duì)緩存的更新發(fā)生在數(shù)據(jù)庫(kù)事務(wù)完成后。緩存需要支持鎖。

          • 針對(duì)“transactional”的事務(wù)感知讀寫(xiě)。

            對(duì)緩存和數(shù)據(jù)庫(kù)的更新被包裝在同一個(gè)JTA事務(wù)中,這樣緩存與數(shù)據(jù)庫(kù)總是保持同步的。數(shù)據(jù)庫(kù)和緩存都必須支持JTA。盡管緩存事務(wù)內(nèi)部依賴(lài)于緩存鎖,但Hibernate不會(huì)顯式調(diào)用任何的緩存鎖函數(shù)。

          以數(shù)據(jù)庫(kù)更新為例。EntityUpdateAction對(duì)于事務(wù)感知讀寫(xiě)、“read-write”的非事務(wù)感知讀寫(xiě),還有“nonstrict-read-write”的非事務(wù)感知讀寫(xiě)相應(yīng)有如下調(diào)用序列:

          • 在一個(gè)JTA事務(wù)中更新數(shù)據(jù)庫(kù);在同一個(gè)事務(wù)中更新緩存。
          • 軟鎖緩存;在一個(gè)事務(wù)中更新數(shù)據(jù)庫(kù);在上一個(gè)事務(wù)成功完成后更新緩存;否則釋放軟鎖。

            軟鎖只是一種特定的緩存值失效表述方式,在它獲得新數(shù)據(jù)庫(kù)值前阻止其他事務(wù)讀寫(xiě)緩存。那些事務(wù)會(huì)轉(zhuǎn)而直接讀取數(shù)據(jù)庫(kù)。

            緩存必須支持鎖;事務(wù)支持則不是必須的。如果緩存是一個(gè)集群,“更新緩存”的調(diào)用會(huì)將新值推送給所有副本,這通常被稱(chēng)為“推(push)”更新策略。

          • 在一個(gè)事務(wù)中更新數(shù)據(jù)庫(kù);在上一個(gè)事務(wù)完成前就清除緩存;為了安全起見(jiàn),無(wú)論事務(wù)成功與否,在事務(wù)完成后再次清除緩存。

            既不需要支持緩存鎖,也不需要支持事務(wù)。如果是緩存集群,“清除緩存”調(diào)用會(huì)讓所有副本都失效,這通常被稱(chēng)為“拉(pull)”更新策略。

          對(duì)于實(shí)體的刪除或插入動(dòng)作,或者集合變更,調(diào)用序列都是相似的。

          實(shí)際上,最后兩個(gè)異步調(diào)用序列仍能保證數(shù)據(jù)庫(kù)和緩存的一致性(基本就是“read committed”的隔離了級(jí)別),這要?dú)w功于第二個(gè)序列中的軟鎖和“更新數(shù)據(jù)庫(kù)”后的“更新緩存”,還有最后一個(gè)調(diào)用序列中的悲觀“清除緩存”。

          基于上述分析,我們的建議是: 

          • 如果數(shù)據(jù)是只讀的,例如引用數(shù)據(jù),那么總是使用“read-only”策略,因?yàn)樗亲詈?jiǎn)單、最高效的策略,也是集群安全的策略。
          • 除非你真的想將緩存更新和數(shù)據(jù)庫(kù)更新放在一個(gè)JTA事務(wù)里,否則不要使用“transactional”策略,因?yàn)镴TA需要漫長(zhǎng)的兩階段提交處理,這導(dǎo)致它基本是性能最差的策略。

            依筆者看來(lái),二級(jí)緩存并非一級(jí)數(shù)據(jù)源,因此使用JTA也未必合理。實(shí)際上最后兩個(gè)調(diào)用序列在大多數(shù)場(chǎng)景下是個(gè)不錯(cuò)的替代方案,這要?dú)w功于它們的數(shù)據(jù)一致性保障。

          • 如果你的數(shù)據(jù)讀很多或者很少有并發(fā)緩存訪問(wèn)和更新,那么可以使用“nonstrict-read-write”策略。感謝它的輕量級(jí)“拉”更新策略,它通常是性能第二好的策略。
          • 如果你的數(shù)據(jù)是又讀又寫(xiě)的,那么使用“read-write”策略。這通常是性能倒數(shù)第二的策略,因?yàn)樗笥芯彺骀i,緩存集群中使用重量級(jí)的“推”更新策略。

          范例9

          以下是一個(gè)ISO收費(fèi)類(lèi)型的HBM文件片段:

          <class name="IsoChargeType">
             <property name="isoId" column="ISO_ID" not-null="true"/>
          
             <many-to-one name="estimateMethod" fetch="join" lazy="false"/>
          
             <many-to-one  name="allocationMethod" fetch="join" lazy="false"/>
          
             <many-to-one name="chargeTypeCategory" fetch="join" lazy="false"/>
          
          </class> 

          一些用戶(hù)只需要ISO收費(fèi)類(lèi)型本身;一些用戶(hù)既需要ISO收費(fèi)類(lèi)型,還需要它的三個(gè)關(guān)聯(lián)對(duì)象。簡(jiǎn)單起見(jiàn),開(kāi)發(fā)者會(huì)立即加載所有三個(gè)關(guān)聯(lián)對(duì)象。如果項(xiàng)目中沒(méi)人負(fù)責(zé)Hibernate調(diào)優(yōu),這是很常見(jiàn)的。

          4.7.1節(jié)中講過(guò)了最好的方法。因?yàn)樗械年P(guān)聯(lián)對(duì)象都是只讀引用數(shù)據(jù),另一種方法是使用延遲抓取,打開(kāi)這些對(duì)象的二級(jí)緩存以避免N+1問(wèn)題。實(shí)際上前一種方法也能從引用數(shù)據(jù)緩存中獲益。

          因?yàn)榇蠖鄶?shù)項(xiàng)目都有很多被其他數(shù)據(jù)引用的只讀引用數(shù)據(jù),上述兩種方法都能改善全局系統(tǒng)性能。

          4.8.2 RegionFactory

          下表是新老兩種方法中對(duì)應(yīng)的主要類(lèi)/接口:

          新方法

          老方法

          RegionFactory

          CacheProvider

          Region

          Cache

          EntityRegionAccessStrategy

          CacheConcurrencyStrategy

          CollectionRegionAccessStrategy

          CacheConcurrencyStrategy

          第一個(gè)改進(jìn)是RegionFactory構(gòu)建了特定的Region,例如EntityRegion和TransactionRegion,而不是使用一個(gè)通用的訪問(wèn)Region。第二個(gè)改進(jìn)是對(duì)于特定緩存的“usage”屬性值,Region要求構(gòu)建自己的訪問(wèn)策略,而不是所有Region都一直使用CacheConcurrencyStrategy的4種實(shí)現(xiàn)。

          要使用新方法,應(yīng)該設(shè)置factory_class而非provider_class配置屬性。以Ehcache 2.0為例:

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

          其他相關(guān)的Hibernate緩存配置都和老方法一樣。

          新方法也能向后兼容遺留方法。如果還是只配了CacheProvider,新方法中將使用下列自說(shuō)明(self-explanatory)適配器和橋隱式地調(diào)用老的接口/類(lèi):

          RegionFactoryCacheProviderBridge、EntityRegionAdapter、CollectionRegionAdapter、QueryResultsRegionAdapter、EntityAccessStrategyAdapter和CollectionAccessStrategyAdapter

          4.8.3 查詢(xún)緩存

          二級(jí)緩存也能緩存查詢(xún)結(jié)果。如果查詢(xún)開(kāi)銷(xiāo)很大而且要重復(fù)運(yùn)行,這也會(huì)很有幫助。

          4.9批量處理調(diào)優(yōu)

          大多數(shù)Hibernate的功能都很適合那些每個(gè)事務(wù)都通常只處理少量數(shù)據(jù)的OLTP系統(tǒng)。但是,如果你有一個(gè)數(shù)據(jù)倉(cāng)庫(kù)或者事務(wù)需要處理大量數(shù)據(jù),那么就另當(dāng)別論了。

          4.9.1使用有狀態(tài)會(huì)話(huà)的非DML風(fēng)格批處理

          如果你已經(jīng)在使用常規(guī)會(huì)話(huà)了,那這是最自然的方法。你需要做三件事:

          • 配置下列3個(gè)屬性以開(kāi)啟批處理特性:
              hibernate.jdbc.batch_size 30
              hibernate.jdbc.batch_versioned_data true
              hibernate.cache.use_second_level_cache false

            batch_size設(shè)置為正值會(huì)開(kāi)啟JDBC2的批量更新,Hibernate的建議值是5到30。基于我們的測(cè)試,極低值和極高值性能都很差。只要取值在合理范圍內(nèi),區(qū)別就只有幾秒而已。如果網(wǎng)絡(luò)夠快,這個(gè)結(jié)果是一定的。

            第二個(gè)配置設(shè)為true,這要求JDBC驅(qū)動(dòng)在executeBatch()方法中返回正確的行數(shù)。對(duì)于Oracle用戶(hù)而言,批量更新時(shí)不能將其設(shè)為true。請(qǐng)閱讀Oracle的《JDBC Developer’s Guide and Reference》中的“標(biāo)準(zhǔn)批處理的Oracle實(shí)現(xiàn)中的更新計(jì)數(shù)”(Update Counts in the Oracle Implementation of Standard Batching)以獲得更多詳細(xì)信息。因?yàn)樗鼘?duì)批量插入來(lái)說(shuō)還是安全的,所以你可以為批量插入創(chuàng)建單獨(dú)的專(zhuān)用數(shù)據(jù)源。最后一個(gè)配置項(xiàng)是可選的,因?yàn)槟憧梢栽跁?huì)話(huà)中顯式關(guān)閉二級(jí)緩存。

          • 像如下范例中那樣定期刷新(flush)并清除一級(jí)會(huì)話(huà)緩存:
             Session session = sessionFactory.openSession();
             Transaction tx = session.beginTransaction();
            

             for ( int i=0; i<100000; i++ ) {
                 Customer customer = new Customer(.....);
                 //if your hibernate.cache.use_second_level_cache is true, call the following:
                 session.setCacheMode(CacheMode.IGNORE);
                 session.save(customer);
                 if (i % 50 == 0) { //50, same as the JDBC batch size
                 //flush a batch of inserts and release memory:
                 session.flush();
                 session.clear();
                 }
             }
             tx.commit();
             session.close();

            批處理通常不需要數(shù)據(jù)緩存,否則你會(huì)將內(nèi)存耗盡并大量增加GC開(kāi)銷(xiāo)。如果內(nèi)存有限,那這種情況會(huì)很明顯。

          • 總是將批量插入嵌套在事務(wù)中。

          每次事務(wù)修改的對(duì)象數(shù)量越少就意味著會(huì)有更多數(shù)據(jù)庫(kù)提交,正如4.5節(jié)所述每次提交都會(huì)帶來(lái)磁盤(pán)相關(guān)的開(kāi)銷(xiāo)。

          另一方面,每次事務(wù)修改的對(duì)象數(shù)量越多就意味著鎖定變更時(shí)間越長(zhǎng),同時(shí)數(shù)據(jù)庫(kù)需要更大的redo log。

          4.9.2使用無(wú)狀態(tài)會(huì)話(huà)的非DML風(fēng)格批處理

          無(wú)狀態(tài)會(huì)話(huà)執(zhí)行起來(lái)比上一種方法更好,因?yàn)樗皇荍DBC的簡(jiǎn)單包裝,而且可以繞開(kāi)很多常規(guī)會(huì)話(huà)要求的操作。例如,它不需要會(huì)話(huà)緩存,也不和任何二級(jí)緩存或查詢(xún)緩存有交互。
          然而它的用法并不簡(jiǎn)單。尤其是它的操作并不會(huì)級(jí)聯(lián)到所關(guān)聯(lián)的實(shí)例上;你必須自己來(lái)處理它們。

          4.9.3 DML風(fēng)格

          使用DML風(fēng)格的插入、更新或刪除,你直接在數(shù)據(jù)庫(kù)中操作數(shù)據(jù),這和前兩種方法在Hibernate中操作數(shù)據(jù)的情況有所不同。

          因?yàn)橐粋€(gè)DML風(fēng)格的更新或刪除相當(dāng)于前兩種方法中的多個(gè)單獨(dú)的更新或刪除,所以如果更新或刪除中的WHERE子句暗示了恰當(dāng)?shù)臄?shù)據(jù)庫(kù)索引,那么使用DML風(fēng)格的操作能節(jié)省網(wǎng)絡(luò)開(kāi)銷(xiāo),執(zhí)行得更好。

          強(qiáng)烈建議結(jié)合使用DML風(fēng)格操作和無(wú)狀態(tài)會(huì)話(huà)。如果使用有狀態(tài)會(huì)話(huà),不要忘記在執(zhí)行DML前清除緩存,否則Hibernate將會(huì)更新或清除相關(guān)緩存(見(jiàn)下面的范例10)。

          4.9.4批量加載

          如果你的HQL或Criteria會(huì)返回很多數(shù)據(jù),那么要注意兩件事:

          • 用下列配置開(kāi)啟批量抓取特性:
            hibernate.jdbc.fetch_size 10

            fetch_size設(shè)置為正值將開(kāi)啟JDBC批量抓取特性。相對(duì)快速網(wǎng)絡(luò),在慢速網(wǎng)絡(luò)中這一點(diǎn)更為重要。Oracle建議的經(jīng)驗(yàn)值是10。你應(yīng)該基于自己的環(huán)境進(jìn)行測(cè)試。

          • 在使用上述任一方法時(shí)都要關(guān)閉緩存,因?yàn)榕考虞d一般是一次性任務(wù)。受限于內(nèi)存容量,向緩存中加載大量數(shù)據(jù)通常也意味著它們很快會(huì)被清除出去,這會(huì)增加GC開(kāi)銷(xiāo)。

          范例10

          我們有一個(gè)后臺(tái)任務(wù),分段加載大量的IsoDeal數(shù)據(jù)用于后續(xù)處理。我們還會(huì)在分段數(shù)據(jù)交給下游系統(tǒng)處理前將其更新為處理中狀態(tài)。最大的一段有50萬(wàn)行數(shù)據(jù)。以下是原始代碼中截取出來(lái)的一段:

          Query query = session.createQuery("FROM IsoDeal d WHERE chunk-clause");
          query.setLockMode("d", LockMode.UPGRADE); //for Inprocess status update
          List<IsoDeal> isoDeals = query.list();
          for (IsoDeal isoDeal : isoDeals) { //update status to Inprocess
             isoDeal.setStatus("Inprocess");
          }
          return isoDeals; 

          包含上述代碼的方法加上了Spring 2.5聲明式事務(wù)的注解。加載并更新50萬(wàn)行數(shù)據(jù)大約花了10分鐘。我們識(shí)別出了以下這些問(wèn)題:

          • 由于會(huì)話(huà)緩存和二級(jí)緩存的原因,系統(tǒng)會(huì)頻繁地內(nèi)存溢出。
          • 就算沒(méi)有內(nèi)存溢出,當(dāng)內(nèi)存消耗很高時(shí)GC的開(kāi)銷(xiāo)也會(huì)很大。
          • 我們還未設(shè)置fetch_size。
          • 就算我們?cè)O(shè)置了batch_size,for循環(huán)也創(chuàng)建了太多update SQL語(yǔ)句。

          不幸的是Spring 2.5不支持Hibernate無(wú)狀態(tài)會(huì)話(huà),所以我們只能關(guān)閉二級(jí)緩存;設(shè)置fetch_size;用DML風(fēng)格的更新來(lái)代替for循環(huán),以此改善性能。

          但是,執(zhí)行時(shí)間還是要6分鐘。將Hibernate的日志級(jí)別調(diào)成trace后,我們發(fā)現(xiàn)是更新會(huì)話(huà)緩存造成了延時(shí)。通過(guò)在DML更新前清除會(huì)話(huà)緩存,我們將時(shí)間縮短到了4分鐘,全部都是將數(shù)據(jù)加載到會(huì)話(huà)緩存中花費(fèi)的時(shí)間。

          4.10 SQL生成調(diào)優(yōu)

          本節(jié)將向你展示如何減少SQL生成的數(shù)量。

          4.10.1 N+1抓取問(wèn)題

          “select抓取”策略會(huì)導(dǎo)致N+1問(wèn)題。如果“連接抓取”策略適合你的話(huà),你應(yīng)該始終使用該策略避免N+1問(wèn)題。

          但是,如果“連接抓取”策略執(zhí)行效果不理想,就像4.7.2節(jié)中那樣,你可以使用“subselect抓取”、“批量抓取”或“延遲集合抓取”來(lái)減少所需的額外SQL語(yǔ)句數(shù)。

          4.10.2 Insert+Update問(wèn)題

          范例11

          我們的ElectricityDeal與DealCharge有單向one-to-many關(guān)聯(lián),如下列HBM文件片段所示:

          <class name="ElectricityDeal"
                 select-before-update="true" dynamic-update="true"
          
                 dynamic-insert="true">
              <id name="key" column="ID">
          
                  <generator class="sequence">
                      <param name="sequence">SEQ_ELECTRICITY_DEALS</param>
          
                  </generator>
              </id>
              …
              <set
          name="dealCharges" cascade="all-delete-orphan">         <key column="DEAL_KEY" not-null="false" update="true"              on-delete="noaction"/>         <one-to-many class="DealCharge"/>     </set> </class>

          在“key”元素中,“not-null”和“update”對(duì)應(yīng)的默認(rèn)值是false和true,上述代碼為了明確這些取值,將它們寫(xiě)了出來(lái)。

          如果你想創(chuàng)建一個(gè)ElectricityDeal和十個(gè)DealCharge,會(huì)生成如下SQL語(yǔ)句:

          • 1句ElectricityDeal的插入語(yǔ)句;
          • 10句DealCharge的插入語(yǔ)句,其中不包括外鍵“DEAL_KEY”;
          • 10句DealCharge字段“DEAL_KEY”的更新語(yǔ)句。

          為了消除那額外的10句更新語(yǔ)句,可以在那10句DealCharge插入語(yǔ)句中包含“DEAL_KEY”,你需要將“not-null”和“update”分別修改為true和false。

          另一種做法是使用雙向或many-to-one關(guān)聯(lián),讓DealCharge來(lái)管理關(guān)聯(lián)。

          4.10.3 更新前執(zhí)行select

          在范例11中,我們?yōu)镋lectricityDeal加上了select-before-update,這會(huì)對(duì)瞬時(shí)(transient)對(duì)象或分離(detached)對(duì)象產(chǎn)生額外的select語(yǔ)句,但卻能避免不必要的數(shù)據(jù)庫(kù)更新。

          你應(yīng)該做出一些權(quán)衡,如果對(duì)象沒(méi)多少屬性,不需要防止不必要的數(shù)據(jù)庫(kù)更新,那么就不要使用該特性,因?yàn)槟隳切┯邢薜臄?shù)據(jù)既沒(méi)有太多網(wǎng)絡(luò)傳輸開(kāi)銷(xiāo),也不會(huì)帶來(lái)太多數(shù)據(jù)庫(kù)更新開(kāi)銷(xiāo)。

          如果對(duì)象的屬性較多,例如是一張大的遺留表,那你應(yīng)該開(kāi)啟該特性,和“dynamic-update”結(jié)合使用以避免太多數(shù)據(jù)庫(kù)更新開(kāi)銷(xiāo)。

          4.10.4 級(jí)聯(lián)刪除

          在范例11中,如果你想刪除1個(gè)ElectricityDeal和它的100個(gè)DealCharge,Hibernate會(huì)對(duì)DealCharge做100次刪除。

          如果將“on-delete”修改為“cascade”,Hibernate不會(huì)執(zhí)行DealCharge的刪除動(dòng)作;而是讓數(shù)據(jù)庫(kù)根據(jù)ON CASCADE DELETE約束自動(dòng)刪除那100個(gè)DealCharge。不過(guò),需要讓DBA開(kāi)啟ON CASCADE DELETE約束,大多數(shù)DBA不愿意這么做,因?yàn)樗麄兿氡苊飧笇?duì)象的意外刪除級(jí)聯(lián)到它的依賴(lài)對(duì)象上。此外,還要注意,該特性會(huì)繞過(guò)Hibernate對(duì)版本數(shù)據(jù)(versioned data)的常用樂(lè)觀鎖策略。

          4.10.5 增強(qiáng)的序列標(biāo)識(shí)符生成器

          范例11中使用Oracle的序列作為標(biāo)識(shí)符生成器。假設(shè)我們保存100個(gè)ElectricityDeal,Hibernate會(huì)將下面的SQL語(yǔ)句執(zhí)行100次來(lái)獲取下一個(gè)可用的標(biāo)識(shí)符:

          select SEQ_ELECTRICITY_DEALS.NEXTVAL from dual; 

          如果網(wǎng)絡(luò)不是很快,那這無(wú)疑會(huì)降低效率。3.2.3及后續(xù)版本中增加了一個(gè)增強(qiáng)的生成器“SequenceStyleGenerator”,它帶了兩個(gè)優(yōu)化器:hilo和pooled。盡管HRD的第5章“基礎(chǔ)O/R映射” 講到了這兩個(gè)優(yōu)化器,不過(guò)內(nèi)容有限。兩個(gè)優(yōu)化器都使用了HiLo算法,該算法生成的標(biāo)識(shí)符等于Hi值加上Lo值,其中Hi值代表組號(hào),Lo值順序且重復(fù)地從1迭代到最大組大小,組號(hào)在Lo值“轉(zhuǎn)回到”1時(shí)加1。

          假設(shè)組大小是5(可以用max_lo或increment_size參數(shù)來(lái)表示),下面是個(gè)例子:

          • hilo優(yōu)化器

            組號(hào)取自數(shù)據(jù)庫(kù)序列的下一個(gè)可用值,Hi值由Hibernate定義,是組號(hào)乘以increment_size參數(shù)值。

          • pooled優(yōu)化器

            Hi值直接取自數(shù)據(jù)庫(kù)序列的下一個(gè)可用值。數(shù)據(jù)庫(kù)序列的增量應(yīng)該設(shè)置為increment_size參數(shù)值。

          直到內(nèi)存組中的值耗盡后,兩個(gè)優(yōu)化器才會(huì)去訪問(wèn)數(shù)據(jù)庫(kù),上面的例子每5個(gè)標(biāo)識(shí)值符訪問(wèn)一次數(shù)據(jù)庫(kù)。使用hilo優(yōu)化器時(shí),你的序列不能再被其他應(yīng)用程序使用,除非它們使用與Hibernate相同的邏輯。使用pooled優(yōu)化器,在其他應(yīng)用程序使用同一序列時(shí)則相當(dāng)安全。

          兩個(gè)優(yōu)化器都有一個(gè)問(wèn)題,如果Hibernate崩潰,當(dāng)前組內(nèi)的一些標(biāo)識(shí)符值就會(huì)丟失,然而大多數(shù)應(yīng)用程序都不要求擁有連續(xù)的標(biāo)識(shí)符值(如果你的數(shù)據(jù)庫(kù),比方說(shuō)Oracle,緩存了序列值,當(dāng)它崩潰時(shí)你也會(huì)丟失標(biāo)識(shí)符值)。

          如果在范例11中使用pooled優(yōu)化器,新的id配置如下:

          <id name="key" column="ID">
              <generator class="org.hibernate.id.enhance.SequenceStyleGenerator">
          <param name="sequence_name">SEQ_ELECTRICITY_DEALS</param> <param name="initial_value">0</param> <param name="increment_size">100</param> <param name="optimizer ">pooled</param> </generator> </id>

          5 總結(jié)

          本文涵蓋了大多數(shù)你在Hibernate應(yīng)用程序調(diào)優(yōu)時(shí)會(huì)覺(jué)得很有用的調(diào)優(yōu)技巧,其中的大多數(shù)時(shí)間都在討論那些行之有效卻缺乏文檔的調(diào)優(yōu)主題,例如繼承映射、二級(jí)緩存和增強(qiáng)的序列標(biāo)識(shí)符生成器。

          它還提到了一些Hibernate調(diào)優(yōu)所必需的數(shù)據(jù)庫(kù)知識(shí)。一些范例中包含了你可能遇到的問(wèn)題的實(shí)際解決方案。

          除此之外,值得一提的是Hibernate也可以和In-Memory Data Grid(IMDG)一起使用,例如Oracle的Coherance或GigaSpaces IMDG,這能讓你的應(yīng)用程序達(dá)到毫秒級(jí)別。

          6 資源

          [1] Latest Hibernate Reference Documentation on jboss.com

          [2] Oracle 9i Performance Tuning Guide and Reference

          [3] Performance Engineering on Wikipedia

          [4] Program Optimization on Wikipedia

          [5] Pareto Principle (the 80/20 rule) on Wikipedia

          [6] Premature Optimization on acm.org

          [7] Java Performance Tuning by Jack Shirazi

          [8] The Law of Leaky Abstractions by Joel Spolsky

          [9] Hibernate’s StatisticsService Mbean configuration with Spring

          [10] JProbe by Quest Software

          [11] Java VisualVM

          [12] Column-oriented DBMS on Wikipedia

          [13] Apache DBCP BasicDataSource

          [14] JDBC Connection Pool by Oracle

          [15] Connection Failover by Oracle

          [16] Last Resource Commit Optimization (LRCO)

          [17] GigaSpaces for Hibernate ORM Users



          posted on 2011-06-18 12:03 ... 閱讀(207) 評(píng)論(0)  編輯  收藏 所屬分類(lèi): Struts Hibernate Spring MyBatis

          <2011年6月>
          2930311234
          567891011
          12131415161718
          19202122232425
          262728293012
          3456789

          導(dǎo)航

          統(tǒng)計(jì)

          常用鏈接

          留言簿

          隨筆分類(lèi)

          隨筆檔案

          收藏夾

          搜索

          最新評(píng)論

          閱讀排行榜

          評(píng)論排行榜

          主站蜘蛛池模板: 青阳县| 南川市| 绥滨县| 万载县| 南阳市| 阿鲁科尔沁旗| 禄劝| 甘肃省| 香港| 新乡市| 印江| 台安县| 衡南县| 广西| 永年县| 五原县| 临泉县| 侯马市| 宁晋县| 八宿县| 永川市| 北碚区| 枝江市| 苗栗县| 施甸县| 南溪县| 怀远县| 津南区| 盐边县| 伽师县| 恩施市| 玉田县| 安远县| 淳化县| 文水县| 六安市| 新昌县| 信阳市| 察哈| 丰镇市| 孟村|