J2EE規范現在作為同時期企業項目的標準被廣為接受。但是J2EE規范的一個重要部分即EJB持久性由于它的開發模型復雜并且實體bean的性能很差而長期受到批評。人們相信這樣一個事實:如果實體bean(尤其是容器受控持久性實體bean,或者CMP)用于應用程序中,那么性能將受到影響。
事實并非如此。
本文中我不打算解釋EJB的復雜性。即將推出的EJB 3規范專門針對目標和開發模型,使得它更容易;該規范還提供依賴注入以及在實體bean容器之外的更容易的測試。相反,本文的目標在于提供BEA WebLogic Server 8.1和9.0中可用的高級選項的深度分析,使開發人員改善CMP bean的性能——在很多情況下可極大地改善。該主題很寬泛,不可能在一篇文章中一一涉及;因此,我只重點討論CMP實體bean的并發以及長期緩存策略。我還簡要說明了最新版本BEA WebLogic Server 9.0中的改進。
并發策略
J2EE開發人員知道EJB容器維護了一個實體bean緩存或者池,通??稍诓渴鹈枋龇信渲?。令人驚奇的是,相當多的J2EE開發人員不知道這并不意味著一旦J2EE服務器從數據庫中加載一個特定的bean實例,它就不再去數據庫中尋找該實例,因為該實例已經保存在緩存池中了。相反,默認情況下J2EE服務器執行ejbLoad()在每次事務的開始從數據庫中同步該實例的狀態?;旧?,CMP bean每運行一次(即使該bean在前一個事務中已經被加載),服務器就執行一次SQL select語句來刷新它。只有在一個事務中操作多個實體bean實例時,服務器才會緩存它們。
顯然,在每次事務中都重新從數據庫中加載狀態會造成很大的性能影響!這個默認行為很容易理解:如果數據庫被多個進程共享,并且每個進程都可以改變數據庫中持久對象的狀態,那么這將是最安全的方法。但是可以通過告訴J2EE服務器保留事務間實體bean的緩存實例,從而避免大部分時間里從數據庫中刷新數據來略微改善這種情況。為了解決這個問題并生成一個最優的解決方案,首先我將討論BEA WebLogic Server中可用的不同的并發策略。
對于EJB開發人員來說很重要的一點是要知道實體bean中可用的不同并發策略。令人驚奇的是,有的開發人員甚至不知道并發選項的存在。那么適用于實體bean的并發策略是什么呢?EJB容器是一個高度多線程的應用程序,同時響應來自多個客戶端的請求,這些請求通常會訪問同一資源,比如數據表中的一行。因此,EJB容器應該管理對實體bean實例的并發訪問;更加技術性地講,并發策略決定了容器如何以及何時將實體bean的每個實例與底層數據庫同步。
目前WebLogic Server中有四種可用的并發策略:排他、數據庫、樂觀和只讀。默認情況下,從7.0版本開始,WebLogic Server就使用的是數據庫并發。上面四種策略按性能從低到高依次排列。我將討論每種策略的優缺點。
排他性并發
排他性并發意味著容器最多為每個主要鍵值創建一個實體bean實例(比如,表中的一行映射到容器中的一個EJB實例)。對指定實例的訪問是串行的,并且請求是按照順序逐個執行的。這種策略有一些嚴重的問題。首先,性能由于多個客戶端對bean的串行訪問受到明顯影響,并且您不能再考慮應用程序的伸縮性。其次,EJB的單個實例(以及容器持有的關聯鎖)對于一個JVM(一個服務器實例)來說是本地的,不能在集群中工作。該策略只是用于后向兼容(早期版本的WebLogic Server默認使用它),應該盡量不用。
數據庫并發
數據庫并發策略是目前WebLogic Server版本中的默認并發策略。它提供了數據持久性和性能間的折中考慮。原理很簡單:WebLogic Server并不自己管理鎖,而是為每個試圖訪問該bean的每個事務創建一個新的bean實例,并將并發控制和死鎖檢測委派給底層數據庫。這就像多個客戶端對單個數據庫進行并行數據庫操作;數據庫的隔離水平和鎖定策略將規定哪些更新、選擇和插入會進行,按照何種順序,以及哪些(如果有的話)會失敗。直接好處是該策略在集群環境中的良好適用性——只要集群中的所有節點共享一個數據庫,EJB容器就不需要為數據同步細節而煩惱。
該策略明顯比排他性策略更具伸縮性,并且對于某些應用程序效果尤為出眾,但是也無法擺脫一些嚴重的性能限制。即使這樣,容器仍保持了一個實體bean實例池,并且這些實例不包含事務間的任何中間狀態。這是實例池化而不是緩存數據。池化無狀態實例的整體思想可能來自于早期的JVM實現,那時對象創建還是一項很昂貴的操作,并且從性能的角度來看緩存對象實例是有好處的。在現代的JVM中情況并非如此,因為大部分情況下對象的創建非???,但是由于該行為是EJB規范中描述的,所有供應商都應支持它。然而,當使用數據庫并發策略時,容器從緩存中取出“無狀態的”bean實例,并且必須執行一條SQL選擇操作以獲得最新數據并填充實例字段。
這種方法可能還不錯,因為我們不用擔心“不新鮮的”bean實例(當數據庫中的數據被從同一集群中的另一個節點或者從不同應用程序中更新時),但是性能也同樣受到明顯影響。您總是在每次事務的開始以一個額外的select操作結束,即使您只是打算更新bean中的數據而對之前的值并不感興趣。因此,在主要或僅是執行更新或插入操作的應用程序中使用實體bean意義不大——容器可能花大量時間做不必要的選擇操作,然后再拋棄數據。
排他性和數據庫并發策略至少存在一個共同問題:更新丟失的可能性??梢韵胂髢蓚€客戶端幾乎同時更新映射到一個實體bean的表中的同一條記錄。如果數據庫中沒有鎖,先完成的更新操作的結果會被其次完成的更新所覆蓋。這是否是可接受的結果取決于您的業務需求。更新丟失通常是不可接受或者不想要的;因此,應用程序需要某種機制來避免或檢測更新丟失的情況,并且有機會恢復。當應用程序部署再多個節點上時使用排他性策略將不能控制更新丟失問題。但是如我之前所述,您不應再考慮該該策略。
數據庫策略通過將并發控制委派給數據庫,提供了進行讀數據操作時在數據庫中使用排他性鎖的選擇。這是通過將weblogic-cmp-jar.xml中的use-select-for-update元素設置為true(默認為false)來實現的。顧名思義,該動作告訴WebLogic Server在加載實體bean數據時使用“select for update”。生成的數據庫鎖一致存在,直到事務完成,因此其他事務不可能在第一個事務運行期間讀取或更改數據。該項技術也許在“select for update”上組合了“no wait”選項,可能解決更新丟失問題以及任何可能的死鎖——只不過代價很高。
該方法的一個缺點是性能會降低,因為一旦一個事務鎖住一個實體實例,其他事務就不能訪問同樣的實體數據,即使它們需要的是只讀訪問。這基本上就是另一種排他性策略,唯一的區別是這次它可用于集群環境,因為所有的鎖定都發生在(共享)數據庫中而不是服務器實例上。第二個缺點是bean的行為在某種程度上依賴于底層數據庫的鎖策略實現。比如,有些數據庫支持細粒度、行級的鎖定,而有些則不然。在后一種情況中,對整個記錄頁的訪問可能被作為單個實體加載操作的結果而被阻止。
樂觀并發
樂觀并發策略通過消除讀取和更新操作之間數據庫中的所有鎖定解決了性能問題,同時還提供了檢測更新丟失的機制。和數據庫并發一樣,樂觀并發給每個事務一個自己的bean實例,但是它沒有在事務進行過程中在EJB容器或數據庫中包含任何鎖。對于進行讀鎖定的數據庫(非Oracle數據庫),容器在一個單獨的事務(該事務在數據讀取操作一完成就立即進行)中讀取bean數據,從而避免了讀取鎖,并且提供了更高的可伸縮性。
這種更新丟失檢測機制并不新鮮,在Java創建之前就已經存在好長時間了。該模式的基本思想簡單而強大:在更新時,首先進行檢查,以確定表中的數據在被第一個進程讀取之后是否被其他進程修改。實際中,這通常是通過在update語句的where子句中包含一個或多個額外的“版本”列來實現的。下面給出了一個簡單的SQL的例子,其中version列是一個數值,在每次更新操作之后會遞增:
-- at Entity load time"container executes select of bean fields,
-- plus 'version' field
select name, address, version from person where person_id = 1;
-- container remember value read from version column,
-- say it was 123
-- at update time, value of version which was read previously
-- added to the where clause and version incremented
update person set name='Joe', version = version + 1 where person_id = 1 and version = 123;
在進行更新時,容器通過執行以下代碼能檢測出數據庫中實際上有多少行被更新:
...
PreparedStatement ps =
connection.prepareStatement('update person...');
int rowsUpdated = ps.executeUpdate();
if (rowsUpdatated != 1) {
throw new OptimisticConcurrencyException('Data was modified by other process');
}
如您所見,該方法可使服務器避免更新丟失問題,并且不需要在容器或數據庫中進行長期的鎖定。該策略尤其適用于數據庫中讀取次數遠多于更新次數,因此更新碰撞的幾率很小時。通常,對于大部分應用程序來說是這樣的。
一個用數字表示的版本列不是在WebLogic Server中實現樂觀并發策略的唯一途徑。相反可以使用一個時間戳列。(注意,數據庫應該允許您存放足夠精確的時間戳值)。時間戳的使用給您帶來了額外好處,那就是知道記錄的最后一次更新是什么時候。有時候在使用遺留數據庫模式時,會不愿意或不可能更改數據表以添加一個版本列。這種情況下,WebLogic Server可以檢查表中事務期間被讀取的所有列(實體bean中的所有字段),或者只檢查被當前事務更新的列。樂觀并發可用于遺留模式,無需對數據表進行任何修改,但是開銷略微增加(意味著更復雜的SQL更新)。
只讀并發
最后一個并發策略是read-only并發。如果數據庫中的某些表包含很少或從未被更改過的數據,那么就很有必要將CMP bean聲明為只讀。服務器仍然為每個事務激活一個新的bean實例,所以請求是并行處理的;它不會每次都調用ejbLoad(),但是根據read-timeout-seconds參數的值周期性地調用。這為您的應用程序帶來了顯著的性能提升,因為SQL選擇只進行一次(第一次訪問實體bean時),然后就被緩存起來,在后面的事務中重用。
WebLogic Server 8.1 SP2之前版本中的一個特殊功能是,即使bean被部署為只讀,開發人員仍然可以對該bean的實例進行create和remove操作。從8.1 SP2開始,這個功能在默認情況下已被禁用,但是如果您需要,可以通過在weblogic-cmp.jsr.xml中將allow-readonly-create-and-remove元素設置為true來打開它。
也有一種明確禁用只讀實體bean的方法。該禁用操作強制在下一次事務開始時從數據庫中刷新bean實例,即使讀取時限還沒過去。您可以將其看作沖洗實例緩存??梢允固囟╞ean的一個特定實例,任何實例子集,或者給定bean的所有實例無效。要想調用invalidate()方法,您可能需要將bean的home或local home分別轉換為CachingHome或CachingLocalHome。下面的代碼說明了如何實現這一點:
Object o = context.lookup("MyBeanCMP");
// cast to CachinHome for remote interface or CachingLocalHome
// for local
CachingHome cachingHome = (CachingHome)o;
// invalidate particular instance by passing primary key value
cachingHome.invalidate(pk);
// invalidate any subset of bean instances by passing collections
// of primary keys
Collection pks = Array.asList(new Object[]{pk1, pk2, ...., pkN});
cachingHome.invalidate(pks);
// or invalidate all instances currently cached
cachingHome.invalidateAll();
當表中的數據準靜態時(比如,如果它按照批處理過程每天更改一次)進行顯式的無效操作很有用。這種情況下,您可以將相應的bean部署為只讀,并且設置一個較大的讀取超時值,然后,當批處理過程結束時,為這些實體bean調用invalidateAll()。
您可以通過在weblogic-ejb-jar.xml部署描述符中entity-cache一節中設置concurrency-strategy元素,為每個CMP bean指定一種并發策略。如果沒有指定并發策略,WebLogic Server默認使用數據庫并發。
性能改善策略
既然大家已經熟悉WebLogic Server中的各種并發策略,現在就讓我演示一下如何應用這些知識應用來提高CMP bean的性能。CMP bean通常由于性能不出眾而遭到批評。從某種程度上說確實如此,因為如上所述,對于按照默認并發策略設置部署的bean,WebLogic Server會在每次新事務的一開始從數據庫中讀取數據。換言之,bean實例狀態并沒有在各個事務之間緩存,并且數據庫收到大量的選擇請求。有人爭辯說,這沒問題,因為現代的數據庫本身就有有效的緩存機制。因此,一旦第一次被選擇,數據就可能留在數據庫緩存中,等待后面被調用,需要很少或者不需要磁盤活動來進行數據庫選擇。雖然這樣,但是我們不應該忘記,大多數情況下應用服務器和數據庫之間的調用要在網絡間傳輸,調用的延遲要比JVM內部的本地調用高幾個數量級。另一個要關心的是,多個應用服務器實例可以共同訪問同一個數據庫(通常在集群配置中)。這種情況下,數據可能會充滿應用服務器和數據庫之間的甚至是最快速的網絡鏈接。
簡而言之,如果我們需要好的性能和伸縮性,那么我們可用的最有效的策略就是如何在可能的情況下在本地緩存數據,以避免代價高昂的對數據庫的遠程調用。從CMP bean的角度來說,這意味著我們需要一種機制在來自各個不同事務的調用之間保存(或緩存)bean的狀態數據。當然,這會給我們帶來性能收益,只要同一個bean實例在它生命周期中被調用一次以上的幾率大于零。換言之,您的應用程序讀數據的機會要多于寫數據,并且存在在每次更新之間多次訪問同一數據的可能。比如,如果您的應用程序只向數據庫中寫數據(OLTP),緩存那些數據根本不能提高性能。同樣,如果您有特別大的表,并且要隨機選擇某些行,那么就存在這樣的可能:緩存數據要存在足夠長的時間,以便在下次需要時可用。幸運的是,仍然有很多類型的應用程序滿足我們的緩存標準。您需要針對具體任務評估緩存的有效性。
觀察一下CMP bean的可用并發策略,您會注意到,至少有兩種方法可以實現長期緩存。第一種方法是盡可能使用只讀bean。不幸的是,通常數據庫中的數據不是完全靜態的,并且/或者按照不可預知的時間間隔更新。如前所述,有一種機制可以顯式地使任何只讀CMP bean的緩存無效。這種方法雖然可行,但是卻很不理想并且易發生錯誤,因為開發人員必須記得在每次更新操作之后調用invalidate()。幸運的是,WebLogic Server提供了方便的read-mostly模式實現,這在稍后將會詳細討論。更強大的方法是充分利用使用樂觀并發策略部署的bean可用的事務間緩存機制。
使用read-mostly模式
WebLogic Server通過將一個只讀CMP bean和一個讀寫CMP bean映射到同一數據中為您提供了一個實現read-mostly模式的機制。您的應用程序應該用只讀bean來讀取數據,用讀寫bean來修改數據。只讀bean按照上述部署描述符中read-timeout-seconds元素所指定的間隔從數據庫中加載數據。為了保證只讀bean總是返回當前數據,應該在讀寫bean更改數據時使只讀bean無效。您可以通過在weblogic-ejb-jar.xml中的entity-descriptor小節的invalidation-target元素中指定該bean,來配置服務器,使其自動讓相應的只讀bean無效。這只能用于CMP 2.0實體bean。雖然該模式提供了緩存方面的好處,但是也有嚴重的不足。當使用該模式時,您應用程序中的大量實體bean將被有效地加倍,對應用程序的啟動時間和內存造成影響。同樣,開發人員需要記住,只讀和讀寫操作應該使用不同的bean,這經常會令人混淆。
值得一提的是,在舊版本的WebLogic Server中(沒有對只讀模式的通過invalidation-target的內在支持),仍可以使用它?;貞浺幌虑懊嬷v過的,根據EJB規范,如果EJB拋出一個RuntimeException或者它的子類物,容器就應該銷毀bean實例。因此,可以在實體bean的只讀版本上暴露這樣的destroyMe()方法,并從讀寫bean的ejbStore()方法中調用它。這就是著名的sepukku模式。
在讀/寫CMP bean的事務間緩存
另一種更先進的長期緩存的方法是通過將weblogic-ejb-jar.xml中的cache-between-transactions元素設置為true來配置bean。這種情況下,只有客戶端首先引用該bean或者事務被回滾時WebLogic Server才會調用ejbLoad()來從數據庫中加載數據。
盡管從理論上說,您可以對除數據庫并發之外的所有并發策略使用該方法,但是在實踐中,只有對樂觀并發使用該方法才有意義。當應用于只讀并發時,該設置被忽略,因為bean數據已經被緩存;當應用于排他性并發時,只有EJB具有對底層數據庫的排他性訪問時才起作用,而這是極少出現的情況。此外,當具有排他性并發的bean被部署在集群中時,WebLogic Server自動禁用事務間的緩存,因為集群中的任何服務器都可能更新bean數據,并且沒有用來在節點間同步或禁用緩存值的機制。這使得我們在進行長期緩存時只有一種可行的并發策略:樂觀并發。
如前所述,對于利用樂觀并發策略部署的bean,WebLogic Server有一種內在機制,用來通過verify-columns檢測底層數據變更。雖然樂觀并發本身只能為數據庫并發帶來少量性能改善,但可利用事務功能間的緩存提供更大的改善。如果在EJB緩存中bean實例已經可用的話,將cache-between-transactions設置為true將使WebLogic Server忽略對數據庫的調用。對于某些類型的應用程序(其中同一對象在短期內被不同事務多次訪問),這可能導致顯著的性能改善(在某些環境下,最多百分之30到40)。自然地,既然我們使用的是樂觀并發,您的應用程序必須做好在檢測到并發沖突時處理OptimisticConcurrencyException的準備。當OptimisticConcurrencyException(RuntimeException的子類型)被拋出時,WebLogic Server 從緩存中丟棄一個EJB實例。注意,如果您將delay-updates-until-end-of-tx設置為true(默認),除非事務承諾否則就得不到樂觀異常,并且如果使用的是容器受控事務這將在應用程序代碼之外。
與read-mostly模式(不提供通知集群中其他節點其中一個節點的數據發生變更的機制)相比,當具有樂觀并發的bean被更新時,一個通知將被廣播給其他集群成員,并且緩存bean實例將被丟棄,以避免樂觀沖突。由于WebLogic Server不廣播數據變更本身,而是只廣播某些種類的bean標識符,這種跨集群的緩存無效措施在提高性能和網絡帶寬利用率方面很有效。WebLogic Server自動完成這種緩存無效工作,bean開發人員不需要再做其他配置。當對同一個bean發出下一個請求時,新鮮的數據將被從數據庫中加載。
If the data in the database is updated by processes external to the server (or if you're using direct JDBC aclearcase/" target="_blank" >ccess from the server to modify the same tables as mapped to CMPs with long-term caching), then these processes should honor the contract on version columns.換言之,如果實體bean被配置為使用數值版本列,那么外部進程應該在行數據更新時增加該值;如果使用了一個時間戳列,那么這個值應該被更新為當前時間戳。如果不這樣做會導致WebLogic Server覆蓋數據,因為它的樂觀檢測機制不會觸發異常,如果版本數據沒有被更改的話。如果不可能通過修改外部進程來更新版本列,可用數據庫觸發器來實現同樣效果。如果不允許修改數據庫模式,可對WebLogic Server進行配置,使其檢查事務期間被讀取的所有列或者只讀取更新過的列(通過分別將verify-columns元素設置為Read或Modified來實現)。注意,這種情況下,可能存在性能問題,因為生成的更新SQL語句更復雜。我建議進行測試,以確定這會對您具體環境中的更新造成多大影響。
在事務間進行緩存提供了比上面討論過的read-mostly模式更好的緩存數據模型。首先,并沒有增加復雜度,比如部署同一bean的兩個版本。另外,對啟動時間,以及當集群間bean發生變更時的自動緩存禁用等也沒有造成影響。直到最近,WebLogic Server中與事務bean間緩存相關的一項特性還被忽視。沒有公開的機制來有計劃地使bean緩存無效。如果您對服務器上數據庫進行排他性訪問,這沒有什么大問題,但是在很多項目中,很少有這種情況,并且為了清空緩存,必須重啟實例;同樣,這也并不總是可能的。
讓我們看一下如果一個bean用樂觀并發部署并且當數據庫中的數據被外部進程更新時被在事務間緩存,這時會發生什么。如果一個外部進程更新當前被容器緩存的記錄,然后應用程序通過CMP更新同樣的列,那么會有兩種可能的結果:如果外部進程在更新verify-columns時不遵守協定,那么就會出現更新丟失的情況(來自CMP的更新覆蓋外部進程對記錄進行的修改)。另一方面,如果外部進程更新了版本列,或者bean被配置,以便用Read/Modified列進行樂觀控制,您就可能有一個OptimisticConcurrencyException。
在WebLogic Server中,OptimisticConcurrencyException是RuntimeException的一個子類,并且如果應用程序沒有捕獲它,實體實例(以及調用同一事務中該實體的所有會話bean)就被丟棄,事務就被回滾;下一次,WebLogic Server會從數據庫中重新加載數據,并且事務會成功完成。盡管缺乏“美感”,但這種方法對于使用隊列(MOM)的應用程序來說還是很有效的;在事務回滾時,該消息將保留在隊列中,接著下一個重新交付嘗試(如果有的話)會成功。值得再次一提的是,除非您的應用程序使用bean受控事務,否則您將捕獲不到OptimisticConcurrencyException,除非將delay-updates-until-end-of-tx設為false(非默認值)。利用默認設置,WebLogic Server不能在數據庫中執行實際的DML操作,并且操作會以RollbackException(它內部就是提到過的真正的OptimisticConcurrencyException)異常而失敗。
下面基于Cactus的測試驗證了該行為。假定一個Person CMP bean是利用樂觀并發部署的,并且cache-between-transaction被設置為true:
...
public class OptimisticLockingTest extends ServletTestCase {
private UserTransaction tx;
protected void setUp() throws Exception {
super.setUp();
Context context = new InitialContext();
this.tx = (UserTransaction)context
.lookup("javax/transaction/UserTransaction");
}
public void testCacheBetweenTransactions() throws Exception {
PersonLocalHome localHome = (PersonLocalHome)Locator
.getLocalHome(PersonLocalHome.JNDI_NAME);
// create record via CMP in first transaction
this.tx.begin();
PersonLocal local = localHome.create();
local.setName("John");
Long pk = local.getPrimaryKey();
this.tx.commit();
// update some field(s) via direct JDBC call in another
// transaction. Assume that updatePersonNameViaJdbc()
// method will update version column as well
String newName = "Paul";
this.tx.begin();
updatePersonNameViaJdbc(pk, newName);
this.tx.commit();
// find CMP again and try to update name in yet
// another transaction
this.tx.begin();
local = localHome.findByPrimaryKey(pk);
// code doesn't see changes made directly
// because bean instance was cached between transactions
assertFalse(newName.equals(local.getName());
try {
// this should throw optimistic concurrency
// exception (on commit)
local.setName("George");
this.tx.commit();
fail("expected OptimisticConcurrencyException not thrown");
}
catch (RollbackException expected) {
// unfortunately there seems to be no better way to
// assert that underlying exception was
// an OptimisticConcurrencyException
assertTrue("Unexpected exception type: "+expected,
expected.getMessage()
.indexOf("Optimistic concurrency violation") > 0);
}
}
}
...
我希望您同意對長期緩存的失效進行控制是有益的。隨著它的出現,甚至出現了WebLogic 7.0和8.1的解決方案。與可用于為只讀bean清空緩存的CachingHome/CachingLocalHome接口類似,一個EntityEJBHome和一個EntityEJBLocalHome,再加上同系列的invalidate()方法,使應用程序能讓特定實體bean的所有緩存或者一部分緩存無效。WebLogic Server中的任何CMP本地接口都可轉換為EntityEJBLocalHome。利用前面的例子,我們可以在updatePersonNameViaJdbc()方法調用后插入下面的代碼:
...
// flush cache
assertTrue("PersonLocalHome not instance of EntityEJBLocalHome: "+ localHome, localHome instanceof EntityEJBLocalHome);
EntityEJBLocalHome entityEJBLocalHome = (EntityEJBLocalHome)localHome;
entityEJBLocalHome.invalidate(pk);
...
現在當下一次調用findByPrimaryKey()時,bean實例將被從數據庫中重新加載,并且所有一切變得更好。除了invalidate()方法,還有invalidateAll()和invalidate(Collection)方法。
WebLogic Server 9.0中的改進
在WebLogic Server 9.0中,對使用樂觀并發的緩存bean進行的顯式緩存禁用被歸檔,并且與只讀bean一致。(比如,bean home或遠程主接口可被緩存到上面調用的CachingHome或CachingLocalHome和invalidate()方法中)。此外,read-timeout-seconds參數適用于用樂觀并發部署的bean。開發人員還對集群中的bean實例無效化有更多的控制。默認情況下,當在一個集群中部署具有樂觀并發策略的bean,并且該集群的一個成員更新該bean時,WebLogic Server會試圖使該集群的所有節點中的bean的所有副本無效。該無效化使您避免了樂觀并發故障,但是會影響性能,因為它是一項資源密集型操作??赏ㄟ^在weblogic-cmp-jar.xml中將cluster-invalidation-disabled設置為true來防止EJB容器使集群中的bean副本無效。
為實體Bean選擇最佳緩存大小
既然您理解了事務間的緩存是如何工作的,下面就讓我們討論一下選擇最佳緩存大小方面的重要話題。默認情況下,每個實體bean都定義有一個大小為1000的緩存。緩存大小由weblogic-ejb-jar.xml部署描述符中的max-beans-in-cache元素控制。我發現該名稱有些令人誤解,因為根據并發策略的不同,WebLogic Server保存無狀態的實體bean實例池(采用數據庫并發策略和排他性并發策略,且事務間緩存被禁止的情況下)或者(只讀、樂觀或排他性并發策略,且事務間緩存啟用的情況下)保存具有保留字段值的bean的真正緩存,從而無需從數據庫中重新加載bean的狀態就可使用。后一種情況更有趣。有人可能會想,更改緩存大小只影響具有實體bean的操作的性能;緩存越大,在緩存中發現需要的特定實體bean實例的機會就越大?;旧鲜沁@樣的,但是如我下面會講到的那樣,另一個重要因素會影響緩存大小的選擇。
多版本化和事務的因素
確定實體bean緩存大小的推動因素之一(可能不太明顯)是當事務使用實體bean時,它們在事務的執行期間被實際加載和“固定”在實體緩存中,即使調用者不在修改實體bean實例而僅是從中讀取值。比如,想象一下,一個會話或MDB bean中的代碼在一個實體bean “Person”上執行一個finder方法,然后在返回的集合上迭代。
...
Collection persons = personLocalHome.findSomething();
for (Iterator iter = persons.iterator(); iter.hasNext();) {
PersonLocal person = (PersonLocal)iter.next();
Long id = person.getId();
// do something: log, send email, etc
...
}
...
如果findSomething()方法返回比max-beans-in-cache中規定的值更多的對象,您的應用程序將在迭代器得到N+1個對象時(N為當前實體緩存大?。┇@得一個令人不愉快的(并且很可能是不想要的)CacheFullException。這可能看上去很重要,因為一般大家都認為finder不應返回很大的集合。但是不要忘記默認情況下WebLogic實體緩存是多版本的,這意味著如果多個事務請求同一個實體bean實例,那么會在緩存中創建多個版本(每個事務一個);從唯一對象的角度來看這可能極大地降低了緩存容量。
由于有多個事務同時運行在一個容器上很正常,所以可以想象如果上面的代碼被從一個會話或MDB bean中調用,其中該bean是利用一個較高的max-beans-in-free-pool參數值(默認1000)部署的,并且同時有50個客戶端請求。這使得每個事務在實體緩存中只有1000/50 = 20個可用的槽,如果一個finder返回的對象超過20個,有的事務就會失敗。
在設計具有大量實體bean的操作時要時刻牢記這一點。開發人員通常使用小型數據庫這一事實使情況變得更糟,并且該問題可能并不表現出來,直到代碼部署到生產規模的數據庫中時。作為保護措施,我建議在開發過程中不要使用默認的緩存大小,而是將其較低值(10-100),這樣緩存相關的問題就能在開發早期發現并解決。
如您所見,為實體緩存選擇正確的大小非常重要,并且不只是從性能的角度來看。如果緩存過大,您的應用程序將消耗很多不必要的內存,但是如果您走到另一個極端,配置過小的緩存空間,會有收到CacheFullException的風險。那么該如何為所有的實體bean選擇最佳的緩存大小呢?
如果您沒有明確為實體bean指定緩存大小,WebLogic Server將使用默認大小1000。這對于預先知道實例數不會太多的某些bean來說足夠了——比如,如果一個bean表示數據庫中的一個查詢表,比如“country”或“state”,其中bean實例的上限是已知的。這種情況下,不指定緩存大小而讓服務器使用默認值是完全可接受的,因為如果緩存沒有被充分利用不會對內存造成影響。順便指出,為不變化或不頻繁變化的bean使用只讀并發策略是一個不錯的注意;這不但消除了不必要的數據庫調用,還限制了與該bean的實體緩存中的實例具有同樣PK的實例數(多版本是不是必要的),從而節省了內存,提高了性能。
對于其中可同時訪問的最大實例數未知或不能可靠地估計出來的bean,情況略微復雜些。您需要分析和估計從finder方法返回以及從一個事務內訪問的最大bean數,然后乘以可同時發生的最大事務數(這通常受您應用程序入口點的最大實例數限制——會話bean和/或MDB)。這能粗略地估計出特定實體bean所需的最小緩存容量。
應用程序級緩存
如果您的應用程序使用了很多實體bean,那么分析和配置各個bean的緩存會很麻煩。估計從“master-detail”關系的“detail”端返回的bean實例數尤為困難——比如,如果您的應用程序在“訂單”表上執行一個finder操作,每個訂單都有一個“項目”數可變的集合。另一個問題是由于每個實體bean都有一個單獨的緩存,內存沒有得到最有效的利用。認識到“每個bean一個緩存”模型的限制,WebLogic Server(從版本7開始)開始支持實體bean的應用程序級緩存。這使得同一個J2EE應用程序中的多個實體bean共享一個運行時緩存。
應用程序級緩存提供了下列好處:
為了定義應用程序級緩存,首先配置weblogic-application.xml中的entity-cache元素。然后在weblogic-ejb-jar.xml中引用entity-descriptor元素中entity-cache-ref元素中的應用程序級緩存。您可以定義一個緩存,然會將其用于應用程序中的所有實體bean,或者為bean組定義不同的緩存。也可以將應用程序級緩存與每個bean一個緩存混合使用,這樣您就有很大的試驗空間。我建議首先從被所有實體bean共享的應用程序級緩存開始,除非您有某些特殊需求。
使用應用程序級緩存是為每個bean指定一個緩存的可行的替代方法??梢脝蝹€緩存的不同實體bean的數目或者已定義緩存的數目沒有限制??筛鶕ean實例數(與每個bean一個緩存類似的方法)或者最大內存空間來指定緩存大小。從管理角度來看,使用內存的大小很有吸引力,但是要知道WebLogic Server不計算緩存中bean消耗的實際內存數(這可能是一項代價很高的操作);它只是根據weblogic-ejb-jar.xml部署描述符中規定的bean平均大小來分割內存。如果沒有指定大小,則假定每個bean平均有100字節。我認為根據bean實例數指定緩存大小會更透明。
選擇哪項策略?
本文已經討論了很多內容,但是還未提及可應用于CMP bean的優化技術。比如,CMR緩存和字段組在某些環境下也很有用。選擇最佳并發策略并充分利用長期緩存會給您的應用程序帶來直接的性能提升。由于現在各個WebLogic Server版本中可用的選項有諸多不同(對于其他J2EE服務器也是如此),所以有時候很難在某一具體情況下做出選擇,尤其是如果開發人員沒有調整這些參數的經驗的話。如果根本沒有指定并發策略和緩存參數,WebLogic Server使用的默認設置從數據一致性方面考慮當然是不錯的選擇,但是從CMP bean的性能方面來看卻不是最佳選擇。沒有放之四海皆準的東西,所以如果您不確定的話,應該分析您的用例,然后利用不同的并發設置進行測試,然后再作出最佳選擇。接著,我會討論一些基本用例以及它們的推薦設置。
靜態只讀數據
最可能的場景是,當數據庫是靜態(不隨時間變化)、準靜態(變化不頻繁),或者從應用程序的角度來看可當作靜態或準靜態,并且您的應用程序不修改這些數據時。比如,數據可能被外部進程頻繁更新,但是如果您的應用程序只是每分鐘/小時/天看到這些更新,那么就沒問題。這種情況下,使用帶有適當read-timeout-seconds值的只讀并發策略是合乎邏輯的。如果您的應用程序需要按照某種預定時間間隔看到更新,或者有一個批處理過程來加載數據,而你需要馬上看到新數據,那么就可以像前面描述的那樣,顯式地禁用緩存。比如,您可以在應用程序的正面暴露一個“CMP緩存失效服務”,然后在批處理的最后或者從調度程序中調用。這種情況下緩存大小很容易計算,因為需要該緩存的所有事務共享同一個CMP實例,所以不需要考慮多版本及其對緩存大小的影響。要根據表大小、單個對象大小以及可用內存選擇合理的緩存大小。
Read-mostly數據
也許最常見的情形是,數據被讀取的頻率大于其被更改的頻率。這正是緩存可成功應用的場合。我建議使用啟用了事務間緩存的樂觀鎖定。如果數據庫模式可更改,我通常指定一個verify-columns的整型值,如果數據庫模式不可更改,就指定一個Modified值。如果您決定使用一個版本列,那么要保證外部進程(如果有的話)在數據發生變更時遵守版本列更新的協定;否則,面臨更新丟失的風險。
從選擇適當的緩存大小方面來看,應該考慮多版本化,以及從finder方法返回的最大bean數目。一個不錯的上限估計方法是將應用程序需要同時處理的最大事務數乘以單個事務可以處理的最大bean數。我通常建議使用更加靈活的應用程序級緩存,因為通常不太可能所有的CMP都同時被使用。應用程序緩存對于所有CMP來說是全局的,會自適應不同bean的活動。如果您定義了一個過大的應用程序級緩存,可能會損害性能,因為所有事務都會串行訪問這個唯一的緩存。對于大小適當的緩存來說極少出現這個問題,但是同樣,在您不確定緩存大小如何影響性能時應該進行性能測試。順便說一下,良好的設計實踐是,避免創建返回任意多實體bean的finder方法(比如,大型表上的findAll()),因為這使得估計出適當的緩存大小變得幾乎不可能。
帶有事務間緩存的樂觀并發最適合有緩存碰撞“保護”的用例。比如,在一個項目用例中,應用程序需要處理傳入消息(來自JMS)。每個消息記錄需要在數據庫表中創建,然后必須發送另一個消息作為響應;對于第二個消息來說,應用程序希望在一分鐘內收到響應,并且在收到該響應時,同一條數據庫記錄得到更新。這時在該場景中應用緩存會帶來巨大和直接的性能收益。我們“保證”每個被緩存的項目至少有一個碰撞,如果緩存對于保存CMP實例來說過大的話。
另一個極端是目標表太大,以至于不可能對同一數據作出重復請求。從實踐角度來看,這是不可行的,并且緩存這樣的數據不能提高性能。
在上述的read-mostly模式中,樂觀并發模式應該是更好的選擇。read-mostly模式不能用于集群中,不能防止出現更新丟失,并且一般來說不便于使用。本文講述它是為了提供關于所有可用策略的整體情況,但是我不鼓勵在現代應用程序中使用它。
Read-mostly數據
如果您的應用程序主要是插入或更新記錄,那么緩存數據意義不大,因為幾乎不會再次訪問它們。在只進行插入操作(OLTP)的極端情況下,緩存反而會減慢處理速度。非重復性更新(對表中遠超過緩存大小的隨機行的更新)也很少從CMP緩存中受益。此外,隨著更新數目相對于讀取次數的增加,樂觀并發策略的表現越來越差,因為會出現大量的樂觀并發異常。實際上,如果您的應用程序只在數據庫中更新和插入記錄,就根本沒有必要使用實體bean。
結束語
從本文的長度就可以看出,調整CMP 2.0 EJB有很多內容。我首先講述了可用的各種并發策略。然后討論了一些重要的性能策略:read-mostly模式,事務間緩存,以及選擇最佳緩存大小。最后,我提供了關于在何種情況下使用何種策略的指南。我希望這些分析能幫助你更好地理解EJB。
原文轉自:http://www.anti-gravitydesign.com