1.引言
Hibernate是最流行的對(duì)象關(guān)系映射(ORM)引擎之一,它提供了數(shù)據(jù)持久化和查詢服務(wù)。
相關(guān)廠商內(nèi)容
在你的項(xiàng)目中引入Hibernate并讓它跑起來(lái)是很容易的。但是,要讓它跑得好卻是需要很多時(shí)間和經(jīng)驗(yàn)的。
通過(guò)我們的使用Hibernate 3.3.1和Oracle 9i的能源項(xiàng)目中的一些例子,本文涵蓋了很多Hibernate調(diào)優(yōu)技術(shù)。其中還提供了一些掌握Hibernate調(diào)優(yōu)技術(shù)所必需的數(shù)據(jù)庫(kù)知識(shí)。
我們假設(shè)讀者對(duì)Hibernate有一個(gè)基本的了解。如果一個(gè)調(diào)優(yōu)方法在Hibernate 參考文檔(下文簡(jiǎn)稱HRD)或其他調(diào)優(yōu)文章中有詳細(xì)描述,我們僅提供一個(gè)對(duì)該文檔的引用并從不同角度對(duì)其做簡(jiǎn)單說(shuō)明。我們關(guān)注于那些行之有效,但又缺乏文檔的調(diào)優(yōu)方法。
2.Hibernate性能調(diào)優(yōu)
調(diào)優(yōu)是一個(gè)迭代的、持續(xù)進(jìn)行的過(guò)程,涉及軟件開(kāi)發(fā)生命周期(SDLC)的所有階段。在一個(gè)典型的使用Hibernate進(jìn)行持久化的Java EE應(yīng)用程序中,調(diào)優(yōu)會(huì)涉及以下幾個(gè)方面:
- 業(yè)務(wù)規(guī)則調(diào)優(yōu)
- 設(shè)計(jì)調(diào)優(yōu)
- Hibernate調(diào)優(yōu)
- Java GC調(diào)優(yōu)
- 應(yīng)用程序容器調(diào)優(yōu)
- 底層系統(tǒng)調(diào)優(yōu),包括數(shù)據(jù)庫(kù)和OS。
沒(méi)有一套精心設(shè)計(jì)的方案就去進(jìn)行以上調(diào)優(yōu)是非常耗時(shí)的,而且很可能收效甚微。好的調(diào)優(yōu)方法的重要部分是為調(diào)優(yōu)內(nèi)容劃分優(yōu)先級(jí)。可以用Pareto定律(又稱“80/20法則”)來(lái)解釋這一點(diǎn),即通常80%的應(yīng)用程序性能改善源自頭20%的性能問(wèn)題[5]。
相比基于磁盤和網(wǎng)絡(luò)的訪問(wèn),基于內(nèi)存和CPU的訪問(wèn)能提供更低的延遲和更高的吞吐量。這種基于IO的Hibernate調(diào)優(yōu)與底層系統(tǒng)IO部分的調(diào)優(yōu)應(yīng)該優(yōu)先于基于CPU和內(nèi)存的底層系統(tǒng)GC、CPU和內(nèi)存部分的調(diào)優(yōu)。
范例1
我們調(diào)優(yōu)了一個(gè)選擇電流的HQL查詢,把它從30秒降到了1秒以內(nèi)。如果我們?cè)诶厥辗矫嫦鹿Ψ颍赡苁招跷ⅰ苍S只有幾毫秒或者最多幾秒,相比HQL的改進(jìn),GC方面的改善可以忽略不計(jì)。
好的調(diào)優(yōu)方法的另一個(gè)重要部分是決定何時(shí)優(yōu)化[4]。
積極優(yōu)化的提倡者主張開(kāi)始時(shí)就進(jìn)行調(diào)優(yōu),例如在業(yè)務(wù)規(guī)則和設(shè)計(jì)階段,在整個(gè)SDLC都持續(xù)進(jìn)行優(yōu)化,因?yàn)樗麄冋J(rèn)為后期改變業(yè)務(wù)規(guī)則和重新設(shè)計(jì)代價(jià)太大。
另一派人提倡在SDLC末期進(jìn)行調(diào)優(yōu),因?yàn)樗麄儽г骨捌谡{(diào)優(yōu)經(jīng)常會(huì)讓設(shè)計(jì)和編碼變得復(fù)雜。他們經(jīng)常引用Donald Knuth的名言“過(guò)早優(yōu)化是萬(wàn)惡之源” [6]。
為了平衡調(diào)優(yōu)和編碼需要一些權(quán)衡。根據(jù)筆者的經(jīng)驗(yàn),適當(dāng)?shù)那捌谡{(diào)優(yōu)能帶來(lái)更明智的設(shè)計(jì)和細(xì)致的編碼。很多項(xiàng)目就失敗在應(yīng)用程序調(diào)優(yōu)上,因?yàn)樯厦嫣岬降?#8220;過(guò)早優(yōu)化”階段在被引用時(shí)脫離了上下文,而且相應(yīng)的調(diào)優(yōu)不是被推遲得太晚就是投入資源過(guò)少。
但是,要做很多前期調(diào)優(yōu)也不太可能,因?yàn)闆](méi)有經(jīng)過(guò)剖析,你并不能確定應(yīng)用程序的瓶頸究竟在何處,應(yīng)用程序一般都是這樣演化的。
對(duì)我們的多線程企業(yè)級(jí)應(yīng)用程序的剖析也表現(xiàn)出大多數(shù)應(yīng)用程序平均只有20-50%的CPU使用率。剩余的CPU開(kāi)銷只是在等待數(shù)據(jù)庫(kù)和網(wǎng)絡(luò)相關(guān)的IO。
基于上述分析,我們得出這樣一個(gè)結(jié)論,結(jié)合業(yè)務(wù)規(guī)則和設(shè)計(jì)的Hibernate調(diào)優(yōu)在Pareto定律中20%的那個(gè)部分,相應(yīng)的它們的優(yōu)先級(jí)更高。
一種比較實(shí)際的做法是:
- 識(shí)別出主要瓶頸,可以預(yù)見(jiàn)其中多數(shù)是Hibernate、業(yè)務(wù)規(guī)則和設(shè)計(jì)方面的(其數(shù)量視你的調(diào)優(yōu)目標(biāo)而定;但三到五個(gè)是不錯(cuò)的開(kāi)端)。
- 修改應(yīng)用程序以便消除這些瓶頸。
- 測(cè)試應(yīng)用程序,然后重復(fù)步驟1,直到達(dá)到你的調(diào)優(yōu)目標(biāo)為止。
你能在Jack Shirazi的《Java Performance Tuning》 [7]一書中找到更多關(guān)于性能調(diào)優(yōu)階段的常見(jiàn)建議。
下面的章節(jié)中,我們會(huì)按照調(diào)優(yōu)的大致順序(列在前面的通常影響最大)去解釋一些特定的調(diào)優(yōu)技術(shù)。
3. 監(jiān)控和剖析
沒(méi)有對(duì)Hibernate應(yīng)用程序的有效監(jiān)控和剖析,你無(wú)法得知性能瓶頸以及何處需要調(diào)優(yōu)。
3.1.1 監(jiān)控SQL生成
盡管使用Hibernate的主要目的是將你從直接使用SQL的痛苦中解救出來(lái),為了對(duì)應(yīng)用程序進(jìn)行調(diào)優(yōu),你必須知道Hibernate生成了哪些SQL。JoeSplosky在他的《The Law of Leaky Abstractions》一文中詳細(xì)描述了這個(gè)問(wèn)題。
你可以在log4j中將org.hibernate.SQL包的日志級(jí)別設(shè)為DEBUG,這樣便能看到生成的所有SQL。你還可以將其他包的日志級(jí)別設(shè)為DEBUG,甚至TRACE來(lái)定位一些性能問(wèn)題。
3.1.2 查看Hibernate統(tǒng)計(jì)
如果開(kāi)啟hibernate.generate.statistics,Hibernate會(huì)導(dǎo)出實(shí)體、集合、會(huì)話、二級(jí)緩存、查詢和會(huì)話工廠的統(tǒng)計(jì)信息,這對(duì)通過(guò)SessionFactory.getStatistics()進(jìn)行的調(diào)優(yōu)很有幫助。為了簡(jiǎn)單起見(jiàn),Hibernate還可以使用MBean“org.hibernate.jmx.StatisticsService”通過(guò)JMX來(lái)導(dǎo)出統(tǒng)計(jì)信息。你可以在這個(gè)網(wǎng)站找到配置范例 。
3.1.3 剖析
一個(gè)好的剖析工具不僅有利于Hibernate調(diào)優(yōu),還能為應(yīng)用程序的其他部分帶來(lái)好處。然而,大多數(shù)商業(yè)工具(例如JProbe [10])都很昂貴。幸運(yùn)的是Sun/Oracle的JDK1.6自帶了一個(gè)名為“Java VisualVM” [11]的調(diào)試接口。雖然比起那些商業(yè)競(jìng)爭(zhēng)對(duì)手,它還相當(dāng)基礎(chǔ),但它提供了很多調(diào)試和調(diào)優(yōu)信息。
4. 調(diào)優(yōu)技術(shù)
4.1 業(yè)務(wù)規(guī)則與設(shè)計(jì)調(diào)優(yōu)
盡管業(yè)務(wù)規(guī)則和設(shè)計(jì)調(diào)優(yōu)并不屬于Hibernate調(diào)優(yōu)的范疇,但此處的決定對(duì)后面Hibernate的調(diào)優(yōu)有很大影響。因此我們特意指出一些與Hibernate調(diào)優(yōu)有關(guān)的點(diǎn)。
在業(yè)務(wù)需求收集與調(diào)優(yōu)過(guò)程中,你需要知道:
- 數(shù)據(jù)獲取特性包括引用數(shù)據(jù)(reference data)、只讀數(shù)據(jù)、讀分組(read group)、讀取大小、搜索條件以及數(shù)據(jù)分組和聚合。
- 數(shù)據(jù)修改特性包括數(shù)據(jù)變更、變更組、變更大小、無(wú)效修改補(bǔ)償、數(shù)據(jù)庫(kù)(所有變更都在一個(gè)數(shù)據(jù)庫(kù)中或在多個(gè)數(shù)據(jù)庫(kù)中)、變更頻率和并發(fā)性,以及變更響應(yīng)和吞吐量要求。
- 數(shù)據(jù)關(guān)系,例如關(guān)聯(lián)(association)、泛化(generalization)、實(shí)現(xiàn)(realization)和依賴(dependency)。
基于業(yè)務(wù)需求,你會(huì)得到一個(gè)最優(yōu)設(shè)計(jì),其中決定了應(yīng)用程序類型(是OLTP還是數(shù)據(jù)倉(cāng)庫(kù),亦或者與其中某一種比較接近)和分層結(jié)構(gòu)(將持久層和服務(wù)層分離還是合并),創(chuàng)建領(lǐng)域?qū)ο螅ㄍǔJ荘OJO),決定數(shù)據(jù)聚合的地方(在數(shù)據(jù)庫(kù)中進(jìn)行聚合能利用強(qiáng)大的數(shù)據(jù)庫(kù)功能,節(jié)省網(wǎng)絡(luò)帶寬;但是除了像COUNT、SUM、AVG、MIN和MAX這樣的標(biāo)準(zhǔn)聚合,其他的聚合通常不具有移植性。在應(yīng)用服務(wù)器上進(jìn)行聚合允許你應(yīng)用更復(fù)雜的業(yè)務(wù)邏輯;但你需要先在應(yīng)用程序中載入詳細(xì)的數(shù)據(jù))。
范例2
分析員需要查看一個(gè)取自大數(shù)據(jù)表的電流ISO(Independent System Operator)聚合列表。最開(kāi)始他們想要顯示大多數(shù)字段,盡管數(shù)據(jù)庫(kù)能在1分鐘內(nèi)做出響應(yīng),應(yīng)用程序也要花30分鐘將1百萬(wàn)行數(shù)據(jù)加載到前端UI。經(jīng)過(guò)重新分析,分析員保留了14個(gè)字段。因?yàn)槿サ袅撕芏嗫蛇x的高聚合度字段,從剩下的字段中進(jìn)行聚合分組返回的數(shù)據(jù)要少很多,而且大多數(shù)情況下的數(shù)據(jù)加載時(shí)間也縮小到了可接受的范圍內(nèi)。
范例3
過(guò)24個(gè)“非標(biāo)準(zhǔn)”(shaped,表示每小時(shí)都可以有自己的電量和價(jià)格;如果所有24小時(shí)的電量和價(jià)格相同,我們稱之為“標(biāo)準(zhǔn)”)小時(shí)會(huì)修改小時(shí)電流交易,其中包括2個(gè)屬性:每小時(shí)電量和價(jià)格。起初我們使用Hibernate的select-before-update特性,就是更新24行數(shù)據(jù)需要24次選擇。因?yàn)槲覀冎恍枰?個(gè)屬性,而且如果不修改電量或價(jià)格的話也沒(méi)有業(yè)務(wù)規(guī)則禁止無(wú)效修改,我們就關(guān)閉了select-before-update特性,避免了24次選擇。
4.2繼承映射調(diào)優(yōu)
盡管繼承映射是領(lǐng)域?qū)ο蟮囊徊糠郑鲇谒闹匾晕覀儗⑺鼏为?dú)出來(lái)。HRD [1]中的第9章“繼承映射”已經(jīng)說(shuō)得很清楚了,所以我們將關(guān)注SQL生成和針對(duì)每個(gè)策略的調(diào)優(yōu)建議。
以下是HRD中范例的類圖:
4.2.1 每個(gè)類層次一張表
只需要一張表,一條多態(tài)查詢生成的SQL大概是這樣的:
select id, payment_type, amount, currency, rtn, credit_card_type from payment
針對(duì)具體子類(例如CashPayment)的查詢生成的SQL是這樣的:
select id, amount, currency from payment where payment_type=’CASH’
這樣做的優(yōu)點(diǎn)包括只有一張表、查詢簡(jiǎn)單以及容易與其他表進(jìn)行關(guān)聯(lián)。第二個(gè)查詢中不需要包含其他子類中的屬性。所有這些特性讓該策略的性能調(diào)優(yōu)要比其他策略容易得多。這種方法通常比較適合數(shù)據(jù)倉(cāng)庫(kù)系統(tǒng),因?yàn)樗袛?shù)據(jù)都在一張表里,不需要做表連接。
主要的缺點(diǎn)整個(gè)類層次中的所有屬性都擠在一張大表里,如果有很多子類特有的屬性,數(shù)據(jù)庫(kù)中就會(huì)有太多字段的取值為null,這為當(dāng)前基于行的數(shù)據(jù)庫(kù)(使用基于列的DBMS的數(shù)據(jù)倉(cāng)庫(kù)處理這個(gè)會(huì)更好些)的SQL調(diào)優(yōu)增加了難度。除非進(jìn)行分區(qū),否則唯一的數(shù)據(jù)表會(huì)成為熱點(diǎn),OLTP系統(tǒng)通常在這方面都不太好。
4.2.2每個(gè)子類一張表
需要4張表,多態(tài)查詢生成的SQL如下:
select id, payment_type, amount, currency, rtn, credit_card type, case when c.payment_id is not null then 1 when ck.payment_id is not null then 2 when cc.payment_id is not null then 3 when p.id is not null then 0 end as clazz from payment p left join cash_payment c on p.id=c.payment_id left join cheque_payment ck on p.id=ck.payment_id left join credit_payment cc on p.id=cc.payment_id;
針對(duì)具體子類(例如CashPayment)的查詢生成的SQL是這樣的:
select id, payment_type, amount, currency from payment p left join cash_payment c on p.id=c.payment_id;
優(yōu)點(diǎn)包括數(shù)據(jù)表比較緊湊(沒(méi)有不需要的可空字段),數(shù)據(jù)跨三個(gè)子類的表進(jìn)行分區(qū),容易使用超類的表與其他表進(jìn)行關(guān)聯(lián)。緊湊的數(shù)據(jù)表可以針對(duì)基于行的數(shù)據(jù)庫(kù)做存儲(chǔ)塊優(yōu)化,讓SQL執(zhí)行得更好。數(shù)據(jù)分區(qū)增加了數(shù)據(jù)修改的并發(fā)性(除了超類,沒(méi)有熱點(diǎn)),OLTP系統(tǒng)通常會(huì)更好些。
同樣的,第二個(gè)查詢不需要包含其他子類的屬性。
缺點(diǎn)是在所有策略中它使用的表和表連接最多,SQL語(yǔ)句稍顯復(fù)雜(看看Hibernate動(dòng)態(tài)鑒別器的長(zhǎng)CASE子句)。相比單張表,數(shù)據(jù)庫(kù)要花更多時(shí)間調(diào)優(yōu)數(shù)據(jù)表連接,數(shù)據(jù)倉(cāng)庫(kù)在使用該策略時(shí)通常不太理想。
因?yàn)椴荒芸绯惡妥宇惖淖侄蝸?lái)建立復(fù)合索引,如果需要按這些列進(jìn)行查詢,性能會(huì)受影響。任何子類數(shù)據(jù)的修改都涉及兩張表:超類的表和子類的表。
4.2.3每個(gè)具體類一張表
涉及三張或更多的表,多態(tài)查詢生成的SQL是這樣的:
select p.id, p.amount, p.currency, p.rtn, p. credit_card_type, p.clazz from (select id, amount, currency, null as rtn,null as credit_card type, 1 as clazz from cash_payment union all select id, amount, null as currency, rtn,null as credit_card type, 2 as clazz from cheque_payment union all select id, amount, null as currency, null as rtn,credit_card type, 3 as clazz from credit_payment) p;
針對(duì)具體子類(例如CashPayment)的查詢生成的SQL是這樣的:
select id, payment_type, amount, currency from cash_payment;
優(yōu)點(diǎn)和上面的“每個(gè)子類一張表”策略相似。因?yàn)槌愅ǔJ浅橄蟮模跃唧w的三張表是必須的[開(kāi)頭處說(shuō)的3張或更多的表是必須的],任何子類的數(shù)據(jù)修改只涉及一張表,運(yùn)行起來(lái)更快。
缺點(diǎn)是SQL(from子句和union all子查詢)太復(fù)雜。但是大多數(shù)數(shù)據(jù)庫(kù)對(duì)此類SQL的調(diào)優(yōu)都很好。
如果一個(gè)類想和Payment超類關(guān)聯(lián),數(shù)據(jù)庫(kù)無(wú)法使用引用完整性(referential integrity)來(lái)實(shí)現(xiàn)它;必須使用觸發(fā)器來(lái)實(shí)現(xiàn)它。這對(duì)數(shù)據(jù)庫(kù)性能有些影響。
4.2.4使用隱式多態(tài)實(shí)現(xiàn)每個(gè)具體類一張表
只需要三張表。對(duì)于Payment的多態(tài)查詢生成三條獨(dú)立的SQL語(yǔ)句,每個(gè)對(duì)應(yīng)一個(gè)子類。Hibernate引擎通過(guò)Java反射找出Payment的所有三個(gè)子類。
具體子類的查詢只生成該子類的SQL。這些SQL語(yǔ)句都很簡(jiǎn)單,這里就不再闡述了。
它的優(yōu)點(diǎn)和上節(jié)類似:緊湊數(shù)據(jù)表、跨三個(gè)具體子類的數(shù)據(jù)分區(qū)以及對(duì)子類任意數(shù)據(jù)的修改都只涉及一張表。
缺點(diǎn)是用三條獨(dú)立的SQL語(yǔ)句代替了一條聯(lián)合SQL,這會(huì)帶來(lái)更多網(wǎng)絡(luò)IO。Java反射也需要時(shí)間。假設(shè)如果你有一大堆領(lǐng)域?qū)ο螅銖淖钌蠈拥腛bject類進(jìn)行隱式選擇查詢,那該需要多長(zhǎng)時(shí)間啊!
根據(jù)你的映射策略制定合理的選擇查詢并非易事;這需要你仔細(xì)調(diào)優(yōu)業(yè)務(wù)需求,基于特定的數(shù)據(jù)場(chǎng)景制定合理的設(shè)計(jì)決策。
以下是一些建議:
- 設(shè)計(jì)細(xì)粒度的類層次和粗粒度的數(shù)據(jù)表。細(xì)粒度的數(shù)據(jù)表意味著更多數(shù)據(jù)表連接,相應(yīng)的查詢也會(huì)更復(fù)雜。
- 如非必要,不要使用多態(tài)查詢。正如上文所示,對(duì)具體類的查詢只選擇需要的數(shù)據(jù),沒(méi)有不必要的表連接和聯(lián)合。
- “每個(gè)類層次一張表”對(duì)有高并發(fā)、簡(jiǎn)單查詢并且沒(méi)有共享列的OLTP系統(tǒng)來(lái)說(shuō)是個(gè)不錯(cuò)的選擇。如果你想用數(shù)據(jù)庫(kù)的引用完整性來(lái)做關(guān)聯(lián),那它也是個(gè)合適的選擇。
- “每個(gè)具體類一張表”對(duì)有高并發(fā)、復(fù)雜查詢并且沒(méi)有共享列的OLTP系統(tǒng)來(lái)說(shuō)是個(gè)不錯(cuò)的選擇。當(dāng)然你不得不犧牲超類與其他類之間的關(guān)聯(lián)。
- 采用混合策略,例如“每個(gè)類層次一張表”中嵌入“每個(gè)子類一張表”,這樣可以利用不同策略的優(yōu)勢(shì)。隨著你項(xiàng)目的進(jìn)化,如果你要反復(fù)重新映射,那你可能也會(huì)采用該策略。
- “使用隱式多態(tài)實(shí)現(xiàn)每個(gè)具體類一張表”這種做法并不推薦,因?yàn)槠渑渲眠^(guò)于繁縟、使用“any”元素的復(fù)雜關(guān)聯(lián)語(yǔ)法和隱式查詢的潛在危險(xiǎn)性。
范例4
下面是一個(gè)交易描述應(yīng)用程序的部分領(lǐng)域類圖:
開(kāi)始時(shí),項(xiàng)目只有GasDeal和少數(shù)用戶,它使用“每個(gè)類層次一張表”。
OilDeal和ElectricityDeal是后期產(chǎn)生更多業(yè)務(wù)需求后加入的。沒(méi)有改變映射策略。但是ElectricityDeal有太多自己的屬性,因此有很多電相關(guān)的可空字段加入了Deal表。因?yàn)橛脩袅恳苍谠鲩L(zhǎng),數(shù)據(jù)修改變得越來(lái)越慢。
重新設(shè)計(jì)時(shí)我們使用了兩張單獨(dú)的表,分別針對(duì)氣/油和電相關(guān)的屬性。新的映射混合了“每個(gè)類層次一張表”和“每個(gè)子類一張表”。我們還重新設(shè)計(jì)了查詢,以便允許針對(duì)具體交易子類進(jìn)行選擇,消除不必要的列和表連接。
4.3 領(lǐng)域?qū)ο笳{(diào)優(yōu)
基于4.1節(jié)中對(duì)業(yè)務(wù)規(guī)則和設(shè)計(jì)的調(diào)優(yōu),你得到了一個(gè)用POJO來(lái)表示的領(lǐng)域?qū)ο蟮念悎D。我們建議:
4.3.1 POJO調(diào)優(yōu)
- 從讀寫數(shù)據(jù)中將類似引用這樣的只讀數(shù)據(jù)和以讀為主的數(shù)據(jù)分離出來(lái)。
只讀數(shù)據(jù)的二級(jí)緩存是最有效的,其次是以讀為主的數(shù)據(jù)的非嚴(yán)格讀寫。將只讀POJO標(biāo)識(shí)為不可更改的(immutable)也是一個(gè)調(diào)優(yōu)點(diǎn)。如果一個(gè)服務(wù)層方法只處理只讀數(shù)據(jù),可以將它的事務(wù)標(biāo)為只讀,這是優(yōu)化Hibernate和底層JDBC驅(qū)動(dòng)的一個(gè)方法。 - 細(xì)粒度的POJO和粗粒度的數(shù)據(jù)表。
基于數(shù)據(jù)的修改并發(fā)量和頻率等內(nèi)容來(lái)分解大的POJO。盡管你可以定義一個(gè)粒度非常細(xì)的對(duì)象模型,但粒度過(guò)細(xì)的表會(huì)導(dǎo)致大量表連接,這對(duì)數(shù)據(jù)倉(cāng)庫(kù)來(lái)說(shuō)是不能接受的。 - 優(yōu)先使用非final的類。
Hibernate只會(huì)針對(duì)非final的類使用CGLIB代理來(lái)實(shí)現(xiàn)延時(shí)關(guān)聯(lián)獲取。如果被關(guān)聯(lián)的類是final的,Hibernate會(huì)一次加載所有內(nèi)容,這對(duì)性能會(huì)有影響。 - 使用業(yè)務(wù)鍵為分離(detached)實(shí)例實(shí)現(xiàn)equals()和hashCode()方法。
在多層系統(tǒng)中,經(jīng)常可以在分離對(duì)象上使用樂(lè)觀鎖來(lái)提升系統(tǒng)并發(fā)性,達(dá)到更高的性能。 - 定義一個(gè)版本或時(shí)間戳屬性。
樂(lè)觀鎖需要這個(gè)字段來(lái)實(shí)現(xiàn)長(zhǎng)對(duì)話(應(yīng)用程序事務(wù))[譯注:session譯為會(huì)話,conversion譯為對(duì)話,以示區(qū)別]。 - 優(yōu)先使用組合POJO。
你的前端UI經(jīng)常需要來(lái)自多個(gè)不同POJO的數(shù)據(jù)。你應(yīng)該向UI傳遞一個(gè)組合POJO而不是獨(dú)立的POJO以獲得更好的網(wǎng)絡(luò)性能。
有兩種方式在服務(wù)層構(gòu)建組合POJO。一種是在開(kāi)始時(shí)加3.2載所有需要的獨(dú)立POJO,隨后抽取需要的屬性放入組合POJO;另一種是使用HQL投影,直接從數(shù)據(jù)庫(kù)中選擇需要的屬性。
如果其他地方也要查找這些獨(dú)立POJO,可以把它們放進(jìn)二級(jí)緩存以便共享,這時(shí)第一種方式更好;其他情況下第二種方式更好。
4.3.2 POJO之間關(guān)聯(lián)的調(diào)優(yōu)
- 如果可以用one-to-one、one-to-many或many-to-one的關(guān)聯(lián),就不要使用many-to-many。
- many-to-many關(guān)聯(lián)需要額外的映射表。
盡管你的Java代碼只需要處理兩端的POJO,但查詢時(shí),數(shù)據(jù)庫(kù)需要額外地關(guān)聯(lián)映射表,修改時(shí)需要額外的刪除和插入。 - 單向關(guān)聯(lián)優(yōu)先于雙向關(guān)聯(lián)。
由于many-to-many的特性,在雙向關(guān)聯(lián)的一端加載對(duì)象會(huì)觸發(fā)另一端的加載,這會(huì)進(jìn)一步觸發(fā)原始端加載更多的數(shù)據(jù),等等。
one-to-many和many-to-one的雙向關(guān)聯(lián)也是類似的,當(dāng)你從多端(子實(shí)體)定位到一端(父實(shí)體)。
這樣的來(lái)回加載很耗時(shí),而且可能也不是你所期望的。 - 不要為了關(guān)聯(lián)而定義關(guān)聯(lián);只在你需要一起加載它們時(shí)才這么做,這應(yīng)該由你的業(yè)務(wù)規(guī)則和設(shè)計(jì)來(lái)決定(見(jiàn)范例5)。
另外,你要么不定義任何關(guān)聯(lián),要么在子POJO中定義一個(gè)值類型的屬性來(lái)表示父POJO的ID(另一個(gè)方向也是類似的)。 - 集合調(diào)優(yōu)
如果集合排序邏輯能由底層數(shù)據(jù)庫(kù)實(shí)現(xiàn),就使用“order-by”屬性來(lái)代替“sort”,因?yàn)橥ǔ?shù)據(jù)庫(kù)在這方面做得比你好。
集合可以是值類型的(元素或組合元素),也可以是實(shí)體引用類型的(one-to-many或many-to-many關(guān)聯(lián))。對(duì)引用類型集合的調(diào)優(yōu)主要是調(diào)優(yōu)獲取策略。對(duì)于值類型集合的調(diào)優(yōu),HRD [1]中的20.5節(jié)“理解集合性能”已經(jīng)做了很好的闡述。 - 獲取策略調(diào)優(yōu)。請(qǐng)見(jiàn)4.7節(jié)的范例5。
范例5
我們有一個(gè)名為ElectricityDeals的核心POJO用于描述電的交易。從業(yè)務(wù)角度來(lái)看,它有很多many-to-one關(guān)聯(lián),例如和Portfolio、Strategy和Trader等的關(guān)聯(lián)。因?yàn)橐脭?shù)據(jù)十分穩(wěn)定,它們被緩存在前端,能基于其ID屬性快速定位到它們。
為了有好的加載性能,ElectricityDeal只映射元數(shù)據(jù),即那些引用POJO的值類型ID屬性,因?yàn)樵谛枰獣r(shí),可以在前端通過(guò)portfolioKey從緩存中快速查找Portfolio:
<property name="portfolioKey" column="PORTFOLIO_ID" type="integer"/>這種隱式關(guān)聯(lián)避免了數(shù)據(jù)庫(kù)表連接和額外的字段選擇,降低了數(shù)據(jù)傳輸?shù)拇笮 ?/p>
4.4 連接池調(diào)優(yōu)
由于創(chuàng)建物理數(shù)據(jù)庫(kù)連接非常耗時(shí),你應(yīng)該始終使用連接池,而且應(yīng)該始終使用生產(chǎn)級(jí)連接池而非Hibernate內(nèi)置的基本連接池算法。
通常會(huì)為Hibernate提供一個(gè)有連接池功能的數(shù)據(jù)源。Apache DBCP的BasicDataSource[13]是一個(gè)流行的開(kāi)源生產(chǎn)級(jí)數(shù)據(jù)源。大多數(shù)數(shù)據(jù)庫(kù)廠商也實(shí)現(xiàn)了自己的兼容JDBC 3.0的連接池。舉例來(lái)說(shuō),你也可以使用Oracle ReaApplication Cluster [15]提供的JDBC連接池[14]以獲得連接的負(fù)載均衡和失敗轉(zhuǎn)移。
不用多說(shuō),你在網(wǎng)上能找到很多關(guān)于連接池調(diào)優(yōu)的技術(shù),因此我們只討論那些大多數(shù)連接池所共有的通用調(diào)優(yōu)參數(shù):
- 最小池大小:連接池中可保持的最小連接數(shù)。
- 最大池大小:連接池中可以分配的最大連接數(shù)。
如果應(yīng)用程序有高并發(fā),而最大池大小又太小,連接池就會(huì)經(jīng)常等待。相反,如果最小池大小太大,又會(huì)分配不需要的連接。 - 最大空閑時(shí)間:連接池中的連接被物理關(guān)閉前能保持空閑的最大時(shí)間。
- 最大等待時(shí)間:連接池等待連接返回的最大時(shí)間。該參數(shù)可以預(yù)防失控事務(wù)(runaway transaction)。
- 驗(yàn)證查詢:在將連接返回給調(diào)用方前用于驗(yàn)證連接的SQL查詢。這是因?yàn)橐恍?shù)據(jù)庫(kù)被配置為會(huì)殺掉長(zhǎng)時(shí)間空閑的連接,網(wǎng)絡(luò)或數(shù)據(jù)庫(kù)相關(guān)的異常也可能會(huì)殺死連接。為了減少此類開(kāi)銷,連接池在空閑時(shí)會(huì)運(yùn)行該驗(yàn)證。
4.5事務(wù)和并發(fā)的調(diào)優(yōu)
短數(shù)據(jù)庫(kù)事務(wù)對(duì)任何高性能、高可擴(kuò)展性的應(yīng)用程序來(lái)說(shuō)都是必不可少的。你使用表示對(duì)話請(qǐng)求的會(huì)話來(lái)處理單個(gè)工作單元,以此來(lái)處理事務(wù)。
考慮到工作單元的范圍和事務(wù)邊界的劃分,有3中模式:
- 每次操作一個(gè)會(huì)話。每次數(shù)據(jù)庫(kù)調(diào)用需要一個(gè)新會(huì)話和事務(wù)。因?yàn)檎鎸?shí)的業(yè)務(wù)事務(wù)通常包含多個(gè)此類操作和大量小事務(wù),這一般會(huì)引起更多數(shù)據(jù)庫(kù)活動(dòng)(主要是數(shù)據(jù)庫(kù)每次提交需要將變更刷新到磁盤上),影響應(yīng)用程序性能。這是一種反模式,不該使用它。
- 使用分離對(duì)象,每次請(qǐng)求一個(gè)會(huì)話。每次客戶端請(qǐng)求有一個(gè)新會(huì)話和一個(gè)事務(wù),使用Hibernate的“當(dāng)前會(huì)話”特性將兩者關(guān)聯(lián)起來(lái)。
在一個(gè)多層系統(tǒng)中,用戶通常會(huì)發(fā)起長(zhǎng)對(duì)話(或應(yīng)用程序事務(wù))。大多數(shù)時(shí)間我們使用Hibernate的自動(dòng)版本和分離對(duì)象來(lái)實(shí)現(xiàn)樂(lè)觀并發(fā)控制和高性能。 - 帶擴(kuò)展(或長(zhǎng))會(huì)話的每次對(duì)話一會(huì)話。在一個(gè)也許會(huì)跨多個(gè)事務(wù)的長(zhǎng)對(duì)話中保持會(huì)話開(kāi)啟。盡管這能把你從重新關(guān)聯(lián)中解脫出來(lái),但會(huì)話可能會(huì)內(nèi)存溢出,在高并發(fā)系統(tǒng)中可能會(huì)有舊數(shù)據(jù)。
你還應(yīng)該注意以下幾點(diǎn)。
- 如果不需要JTA就用本地事務(wù),因?yàn)镴TA需要更多資源,比本地事務(wù)更慢。就算你有多個(gè)數(shù)據(jù)源,除非有跨多個(gè)數(shù)據(jù)庫(kù)的事務(wù),否則也不需要JTA。在最后的一個(gè)場(chǎng)景下,可以考慮在每個(gè)數(shù)據(jù)源中使用本地事務(wù),使用一種類似“Last Resource Commit Optimization”[16]的技術(shù)(見(jiàn)下面的范例6)。
- 如果不涉及數(shù)據(jù)變更,將事務(wù)標(biāo)記為只讀的,就像4.3.1節(jié)提到的那樣。
- 總是設(shè)置默認(rèn)事務(wù)超時(shí)。保證在沒(méi)有響應(yīng)返回給用戶時(shí),沒(méi)有行為不當(dāng)?shù)氖聞?wù)會(huì)完全占有資源。這對(duì)本地事務(wù)也同樣有效。
- 如果Hibernate不是獨(dú)占數(shù)據(jù)庫(kù)用戶,樂(lè)觀鎖會(huì)失效,除非創(chuàng)建數(shù)據(jù)庫(kù)觸發(fā)器為其他應(yīng)用程序?qū)ο嗤瑪?shù)據(jù)的變更增加版本字段值。
范例6
我們的應(yīng)用程序有多個(gè)在大多數(shù)情況下只和數(shù)據(jù)庫(kù)“A”打交道的服務(wù)層方法;它們偶爾也會(huì)從數(shù)據(jù)庫(kù)“B”中獲取只讀數(shù)據(jù)。因?yàn)閿?shù)據(jù)庫(kù)“B”只提供只讀數(shù)據(jù),我們對(duì)這些方法在這兩個(gè)數(shù)據(jù)庫(kù)上仍然使用本地事務(wù)。
服務(wù)層上有一個(gè)方法設(shè)計(jì)在兩個(gè)數(shù)據(jù)庫(kù)上執(zhí)行數(shù)據(jù)變更。以下是偽代碼:
//Make sure a local transaction on database A exists @Transactional (readOnly=false, propagation=Propagation.REQUIRED) public void saveIsoBids() { //it participates in the above annotated local transaction insertBidsInDatabaseA(); //it runs in its own local transaction on database B insertBidRequestsInDatabaseB(); //must be the last operation因?yàn)?strong>insertBidRequestsInDatabaseB()是saveIsoBids ()中的最后一個(gè)方法,所以只有下面的場(chǎng)景會(huì)造成數(shù)據(jù)不一致:
在saveIsoBids()執(zhí)行返回時(shí),數(shù)據(jù)庫(kù)“A”的本地事務(wù)提交失敗。
但是,就算saveIsoBids()使用JTA,在兩階段提交(2PC)的第二個(gè)提交階段失敗的時(shí)候,你還是會(huì)碰到數(shù)據(jù)不一致。因此如果你能處理好上述的數(shù)據(jù)不一致性,而且不想為了一個(gè)或少數(shù)幾個(gè)方法引入JTA的復(fù)雜性,你應(yīng)該使用本地事務(wù)。
(未完待續(xù))
關(guān)于作者
Yongjun Jiao是SunGard Consulting Services的技術(shù)主管。過(guò)去10年中他一直是專業(yè)軟件開(kāi)發(fā)者,他的專長(zhǎng)包括Java SE、Java EE、Oracle和應(yīng)用程序調(diào)優(yōu)。他最近的關(guān)注點(diǎn)是高性能計(jì)算,包括內(nèi)存數(shù)據(jù)網(wǎng)格、并行計(jì)算和網(wǎng)格計(jì)算。
Stewart Clark是SunGard Consulting Services的負(fù)責(zé)人。過(guò)去15年中他一直是專業(yè)軟件開(kāi)發(fā)者和項(xiàng)目經(jīng)理,他的專長(zhǎng)包括Java核心編程、Oracle和能源交易。
轉(zhuǎn)載自infoq