第14章 事務
Dale Green著
JSP WU 譯
一個典型的企業應用程序在一個或多個數據庫中訪問和存儲信息。因為這些信息對于商業操作非常重要,它必須精確、實時、可靠。如果允許多個程序同時更新相同的數據,就會破壞數據的完整性。如果在一個商業交易處理過程中,部分數據被更新后系統崩潰也將破壞數據完整性。事務通過預防以上情況的發生確保數據的完整性。事務控制多個應用程序對數據庫的并發操作。如果發生系統崩潰,事務確保恢復的數據崩潰前將保持一致。
本章內容:
什么是事務
容器管理事務
事務的屬性
回滾容器管理事務
同步會話bean實例變量
容器管理事務中不允許使用的方法
Bean 管理事務
JDBC事務
JTA? 事務
非提交返回事務
在Bean管理事務中不允許使用的方法
企業Bean事務摘要
事務超時
隔離級別
更新多個數據庫
Web 組件事務
?
一.什么是事務
模擬一個商業交易,應用程序需要完成幾個步驟。例如,一個財物應用程序,可能會將資金從經常性帳戶(checking account)轉到儲蓄性賬戶(saving account),該交易的偽碼表示如下:
begin transaction
debit checking account
credit savings account
update history log
commit transaction
三個步驟要么全部完成,要么一個都不做。否則數據完整性將被破壞。因為事務中的所有步驟被看作一個統一的整體,所以事務一般被定義為一個不可分割的工作單元。
結束事務有兩種方法:提交或者回滾。當一個事務提交,數據修改被保存。如果事務中有一個步驟失敗,事務就回滾,這個事務中的已經執行的動作被撤銷。例如在上面的偽碼中,如果在處理第二步的時候硬盤驅動器崩潰,事務的第一步將被撤銷。盡管事務失敗,數據的完整性不會被破壞,因為帳目仍然保持平衡。
前面偽碼中,begin和commit標明了事務的界限。當設計一個企業Bean的時候,你要決定怎樣通過容器管理或bean管理事務來指定事務界限。
二.容器管理事務
在容器管理事務的企業Bean中,EJB容器來設定事務界線。你能夠在任何企業Bean中使用容器管理事務:會話Bean、實體Bean或者 Message-driven Bean。容器管理事務簡化了開發,因為企業Bean不用編碼來顯式制定事務界限。代碼不包括開始結束事務的語句。典型的,容器會在一個企業Bean的方法被調用前立即開始一個事務,在這個方法退出以前提交這個事務。每個方法都關聯一個事務。在一個方法中不允許嵌套或多個的事務存在。容器管理事務不需要所有的方法都關聯事務。當部署一個Bean時,通過設定部署描述符中的事務屬性來決定方法是否關聯事務和如何關聯事務。
事務的屬性
一個事務的屬性控制了事務的使用范圍。圖 14-1說明了為什么控制事務的范圍很重要。圖中,method-A開始一個事務然后調用Bean-2中的method-B.它運行在method-A開始的事務中還是重新執行一個新的事務?結果要看method-B中的事務屬性。
圖 14-1 Transaction Scope
一個事務屬性可能有下面的屬性之一:
☆ Required
☆ RequiresNew
☆ Mandatory
☆ NotSupported
☆ Supports
☆ Never
Required
如果客戶端正在一個運行的事務中調用一個企業Bean的方法,這個方法就在這個客戶端的事務中執行。如果客戶端不關聯一個事務,這個容器在運行該方法前開始一個新的事務。
Required屬性在許多事務環境中可以很好的工作,因此你可以把它作為一個默認值,至少可以在早期開發中使用。因為事務的屬性是在部署描述符中聲明的,在以后的任何時候修改它們都很容易。
RequiresNew
如果客戶端在一個運行的事務中調用企業Bean的方法,容器的步驟是:
1.掛起客戶端的事務
2.開始一個新的事務
3.代理方法的調用
4.方法完成后重新開始客戶端的事務
如果客戶端不關聯一個事務,容器運行這個方法以前同樣開始一個新的事務。如果你想保證該方法在任何時候都在一個新事物中運行,使用RequiresNew屬性。
Mandatory
如果客戶端在一個運行的事務中調用企業Bean的方法,這個方法就在客戶端的事務中執行。如果客戶端不關聯事務,容器就拋出TransactionRequiredException 異常。
如果企業Bean的方法必須使用客戶端的事務,那么就使用Mandatory屬性。
NotSupported
如果客戶端在一個運行的事務中調用企業Bean的方法,這個容器在調用該方法以前掛起客戶端事務。方法執行完后,容器重新開始客戶端的事務。
如果客戶端不關聯事務,容器在方法運行以前不會開始一個新的事務。為不需要事務的方法使用NotSupported屬性。因為事務包括整個過程,這個屬性可以提高性能。
Supports
如果客戶端在一個運行的事務中調用企業Bean的方法,這個方法在客戶端的事務中執行,如果這個客戶端不關聯一個事務,容器運行該方法前也不會開始一個新的事務。因為該屬性使方法的事務行為不確定,你應該謹慎使用Supports屬性。
Never
如果客戶端在一個運行的事務中調用企業Bean的方法,容器將拋出RemoteException異常。如果這個客戶端不關聯一個事務,容器運行該方法以前不會開始一個新的事務。
Summary of Transaction Attributes(事務屬性概要)
表 14-1 列出了事務屬性的影響。事務T1和T2都被容器控制。T1是調用企業Bean方法的客戶端的事務環境。在大多數情況下,客戶端是其它的企業Bean。T2是在方法執行以前容器啟動的事務。在表 14-1中,“None”的意思是這個商業方法不在容器控制事務中執行。然而,該商業方法中的數據庫操作可能在DBMS管理控制的事務中執行。
Setting Transaction Attributes (設定事務屬性)
因為事務屬性被保存在配置描述符中,他們會在J2EE應用程序開發的幾個階段被改變:創建企業Bean,應用程序裝配和部署。然而, 當創建這個Bean的時候指定它的屬性是企業Bean開發者的責任。只有將該組件裝配到一個更大的應用程序時可以由開發者修改該屬性,而不要期待J2EE應用程序部署者來指定該事務屬性。
表 14-1 事物屬性和范圍? |
||
事務屬性 |
客戶端事務 |
商業方法事務 |
Required |
None |
T2 |
T1 |
T1 |
|
RequiresNew |
None |
T2 |
T1 |
T2 |
|
Mandatory |
None |
error |
T1 |
T1 |
|
NotSupported |
None |
None |
T1 |
None |
|
Supports |
None |
None |
T1 |
T1 |
|
Never |
None |
None |
T1 |
Error |
你可以為整個企業Bean或者單個方法指定事務屬性。如果你為整個企業Bean和它某個方法各指定一個事務屬性,為該方法指定的事務屬性優先。當為單個方法指定事務屬性時,不同類型企業Bean的要求也不同。會話Bean需要為商業方法定義屬性屬性,但create方法不需要定義事務屬性。實體Bean需要為商業方法、create方法、remove方法和查找(finder)方法定義事務屬性。Message-driven Bean需要為onMessage方法指定屬性事務,而且只能是Required和NotSupported其中之一。
容器管理事務的回滾
在以下兩中情況下,事務將回滾。第一,如果產生一個系統異常,容器將自動回滾該事務。第二,通過調用EJBContext接口SetRollbackOnly方法,Bean方法通知容器回滾該事務。如果Bean拋出一個應用異常,事務將不會自動回滾,但可以調用SetRollbackOnly回滾。對于一個系統和應用的異常,參考第5章的處理異常一節。
下面這個例子的代碼在j2eetorial/examples/src/bank目錄下。在命令行窗口下進入j2eetutorial/examples目錄執行ant bank命令編譯這些代碼,執行 ant create-bank-table命令創建要用到的表。一個BankApp.ear樣本文件在j2eetutorial/examples/ears目錄下。通過BnankEJB 實例的transferToSaving方法來說明setRollbackOnly方法的用法。如果余額檢查出現負數,那么transferToSaving調用setRollBackOnly回滾事務并拋出一個應用程序異常(InsufficientBalanceException)。updateChecking和updateSaving 方法更新數據表。如果更新失敗,這兩個方法拋出SQLException異常而transgerToSaving方法拋出EJBException異常。因為EJBException是一個系統異常,它使容器事務自動回滾事務。TransferTuSaving 方法的代碼如下:
public void transferToSaving(double amount) throws
?? InsufficientBalanceException? {
? checkingBalance -= amount;
?? savingBalance += amount;
? try {
???? ?updateChecking(checkingBalance);
????? if (checkingBalance < 0.00) {
???????? context.setRollbackOnly();
???????? throw new InsufficientBalanceException();
????? }
????? updateSaving(savingBalance);
?? } catch (SQLException ex) {
?????? throw new EJBException
??????????("Transaction failed due to SQLException: "
??????????+ ex.getMessage());
?? }
}
當一個容器回滾一個事務,它總是會撤消事務中已執行的SQL語句造成的數據改動。然而,僅僅在實體Bean中容器回滾才會改變Bean的實例變量(與數據庫狀態有關的字段)。(這是因為容器會自動調用實體Bean的ejbLoad方法,該方法從數據庫中讀入實例變量的值。)當發生回滾,會話Bean必須顯式重新設置所有被事務改動過的實例變量。重設會話Bean的實例變量最簡單的方法是實現SessionSynchronization接口。
同步會話Bean的實例變量
SessionSynchronization接口是可選的,它允許你在Bean的實例變量和它們在數據庫中的相應值之間保持同步。容器會在事務的幾個主要階段調用SessionSynchronization接口的對應方法—afterBegin、beforeCompletion和afterCompletion。
AfterBegin方法通知Bean實例一個新的事務已經開始。容器在調用商業方法以前立即調用afterBegin方法。afterBegin方法是從數據庫中讀入實例變量值的最佳位置。例如,在BanBean類中,在afterBegin方法中從讀入了CheckingBalance和savingBalance變量的值:
? public void afterBegin() {
? System.out.println("afterBegin()");
?? try {
????? checkingBalance = selectChecking();
????? savingBalance = selectSaving();
?? } catch (SQLException ex) {
?????? throw new EJBException("afterBegin Exception: " +
?????????? ex.getMessage());
?? }
}
商業方法方法完成以后,容器調用beforeCompletion方法,不過僅僅是在事務提交以前。BeforeCompletion方法是會話Bean回滾事務的最后時機(通過調用setRollbackOnly方法).如果會話Bean還沒有實例變量的值更新數據庫,就在beforCompletion方法里實現。
afterCompletion方法指出事務已經完成。它只有一個布爾型的參數,true表示事務被正確提交false表示事務回滾。如果事務回滾,會話Bean可以在該方法中從數據庫中重新讀取它的實例變量值:
public void afterCompletion(boolean committed) {
? System.out.println("afterCompletion: " + committed);
?? if (committed == false) {
????? try {
???????? checkingBalance = selectChecking();
???????? savingBalance = selectSaving();
????? } catch (SQLException ex) {
????????? throw new EJBException("afterCompletion SQLException:
???????? " + ex.getMessage());
????? }
?? }
}
容器管理事務中不允許使用的方法
你不應該調用可能干擾容器設置的事務界線的方法,下面列出了所有禁止的方法:
☆ java.sql.Connection接口的commit、setAutoCommit和rollback方法
☆ javax.ejb.EJBContext 接口的getUserTransaction方法
☆ javax.transaction.UserTransaction接口的所有方法
然而你可以在Bean管理事務中使用這些方法設置事務界限。
三.Bean管理事務
在一個Bean管理事務中,會話Bean或者Message-driven Bean是用代碼顯式設置事務界線的。實體Bean不能使用Bean管理事務,只能使用容器管理的事務。雖然容器管理事務Bean需要較少的代碼,但它也有一個局限:方法執行時,它只能關聯一個事務或不關聯任何事務。如果這個局限使你Bean編碼困難,你應該考慮使用Bean管理事務。(譯者:實際上J2EE服務器不支持嵌套事物,那么Bean管理事務唯一的優點就是可以在一個方法中一次啟動多個事務)
下面的偽碼很好說明了Bean管理事對商業邏輯的緊密控制。通過檢查各種條件,偽碼決定是否在商業方法中啟動或停止不同的事務。
begin transaction
...
update table-a
...
if (condition-x)
?? commit transaction
else if (condition-y)
?? update table-b
?? commit transaction
else
?? rollback transaction
?? begin transaction
?? update table-c
?? commit transaction
當為會話Bean或Message-driver Bean的Bean管理事務編碼時,你必須決定是使用jdbc或者JTA事務。下面的內容論述了兩種事務類型。
JDBC 事務
JDBC事務通過DBMS事務管理器來控制。你可能會為了使用會話Bean中的原有代碼而采用JDBC事務將這些代碼封裝到一個事務中。使用JDBC事務,要調用java.sql.Connection接口的commit和rollback方法。事務啟動是隱式的。一個事務的從最近的提交、回滾或連接操作后的第一個SQL的語句開始。(這個規則通常是正確的,但可能DBMS廠商的不同而不同)
代碼資源
下面的例子在j2eetutorial/examples/src/ejb/warehouse目錄下。在命令行窗口中進入j2eetutorial/examples目錄執行ant bank命令編譯這些源文件,執行ant create-warehouse-table命令創建要用到的表,一個樣本WarehouseApp.ear文件在j2eetutorial/example/ears 目錄下。
下面的代碼來自WarehouseEJB例子,一個會話Bean通過使用Connection接口的方法來劃定Bean管理事務界限。ship方法以調用名為con的連接對象的setAutoCommit方法開始,該方法通知DBMS不要自動提交每個SQL語句。接下來ship 方法更新order_item和inventory數據表。如果更新成功,這個事務就會被提交。如果出現異常,事務就回滾。
public void ship (String productId, String orderId, int
quantity) {
? try {
????? con.setAutoCommit(false);
????? updateOrderItem(productId, orderId);
????? updateInventory(productId, quantity);
????? con.commit();
?? } catch (Exception ex) {
?????? try {
????????? con.rollback();
????????? throw new EJBException("Transaction failed: " +
???????????? ex.getMessage());
?????? } catch (SQLException sqx) {
?????????? throw new EJBException("Rollback failed: " +
????????????? sqx.getMessage());
?????? }
?? }
}
JTA 事務
JTA是Java Transaction API 的縮寫。這些API 允許你用獨立于具體的事務管理器實現的方法確定事務界限。J2EE SDK 事務管理器通過Java事務服務(Java Transaction Service, JTS)實現。但是你的代碼并不直接調用JTS中的方法,而是調用JTA方法來替代,JTA方法會調用底層的JTS實現。
JTA事務被J2EE?事務管理器管理。你可能需要使用一個JTA事務,因為它能夠統一操作不同廠商的數據庫。一個特定DBMS的事務管理器不能工作在不同種類的數據庫上。然而J2EE事務管理器仍然有一個限制——它不支持嵌套事務。就是說,它不能在前一個事務結束前啟動另一個事務。
下面例子的源代碼在j2eetutorial/examples/src/ejb/teller目錄下,在命令行窗口進入j2eetutorial/examples目錄,執行ant teller命令編譯這些源文件,執行ant create-bank-teller命令創建要用到的表。一個樣本TellerApp.ear文件在j2eetutorial/examples/ears目錄下。
要自己確定事務界限,可以調用javax.transaction.UserTransaction接口的begin、commit和rollback方法來確定事務界限(該接口只能在SessionBean中使用,實體Bean不允許使用用戶自定義的)。下面選自TellerBean類的代碼示范了UserTransaction的用法。begin和commit方法確定了數據庫操作的事務界限,如果操作失敗則調用rollback回滾事務并拋出EJBException異常。
public void withdrawCash(double amount) {
? UserTransaction ut = context.getUserTransaction();
? try {
????? ut.begin();
????? updateChecking(amount);
????? machineBalance -= amount;
????? insertMachine(machineBalance);
????? ut.commit();
?? } catch (Exception ex) {
?????? try {
????? ????ut.rollback();
?????? } catch (SystemException syex) {
?????????? throw new EJBException
????????????? ("Rollback failed: " + syex.getMessage());
?????? }
?????? throw new EJBException
????????? ("Transaction failed: " + ex.getMessage());
??? }
}
非提交返回事務
使用Bean管理事務的無狀態會話Bean在事務返回前必須提交或者返回事務,而有狀態的會話Bean沒有這個限制。
對于使用JTA事務的有狀態會話Bean,Bean實例和事務的關聯越過大量用戶調用被保持,甚至被調用的每個商業方法都打開和關閉數據庫連接,該市無關聯也不斷開,直到事務完成(或回滾)。
對于使用JDBC事務的有狀態會話Bean,JDBC連接越過用戶調用保持Bean和事務之間的關聯。連接關閉,事務關聯將被釋放。
在Bean管理事務中不允許使用的方法
在Bean管理的事務中不能調用EJBContext接口的getRollbackOnly和setRollbackOnly方法,這兩個方法只能在容器管理事務中被調用。在Bean管理事務中,應調用UserTransaction接口的getStatus和rollback方法。
四.企業Bean事務摘要
如果你不能確定怎么在企業Bean中使用事務,可以用這個小技巧:在Bean的部署描述符中,制定事務類型為容器管理,把整個Bean(所有方法)的事務屬性設置為Required。大多數情況下,這個配置可以滿足你的事務需求。
表14-2列出了不同類型的企業Bean所允許使用的事務類型。實體Bean只能使用容器管理事務,但可以在部署描述符中配置事務屬性,并可以調用EJBContext接口的setRollbackOnly方法來回滾事務。
表 14-2 企業Bean 允許的事務類型 |
||||
企業Bean 類型 |
容器管理事務 |
Bean管理事務 |
? | |
JTA |
JDBC |
? | ||
實體Bean |
Y |
N |
N |
? |
會話Bean |
Y |
Y |
Y |
? |
Message-driven |
Y |
Y |
Y |
? |
會話Bean既可以使用容器管理事務也可以使用Bean管理事務。Bean管理事務又有兩種類型:JDBC事務和JTA事務。JDBC事務使用Connection接口的commit和rollback方法來劃分事務界限。JTA事務使用UserTransaction接口的begin、commit和rollback方法來劃分事務界限。
在Bean管理事務的會話Bean中,混合使用JTA事務和JDBC事務是可能的。但是我不推薦這樣使用,因為這樣會造成代碼的調試和維護都很困難。
Message-driver Bean和會話Bean一樣既可以使用容器管理事務也可以使用Bean管理事務。
五.事務超時
對于容器管理事務,事務超時間隔是通過設置default.properties文件中ransaction.timeout屬性的值來確定的,該文件在J2EE SDK安裝目錄的config子目錄下。如下例將事務超時間隔設置為5秒鐘:
transaction.timeout=5
這樣,當事務在5秒鐘內還沒有完成,容器將回滾該事務。
J2EE SDK安裝后,超時間隔的缺省值為0,表示不計算超時,無論事務執行多長時間,除非異常出錯回滾,一直等待事務完成。
只有使用容器管理事務的企業Bean才會受到transaction.timeout屬性值的影響。Bean管理的JTA事務使用UserTransaction接口的setTransactionTimeout方法來設置事務超時間隔。
六.隔離級別
事務不僅保證事務界限內的數據庫操作全部完成(或回滾)同時還隔離數據庫更新語句。隔離級別描述被修改的數據對其他事物的可見度。
假如一個應用程序在事務中修改一個顧客的電話號碼,在事務結束前另一個應用程序要讀取該條記錄的電話號碼。那么第二個應用程序是讀取修改過但還沒提交的數據,還是讀取未修改前的老數據呢?答案就取決于事務的隔離級別。如果事務允許其他程序讀取未提交的數據,會因為不用等待事務結束而提高性能,同時也有一個缺點,如果事務回滾,其他應用程序讀取的將是錯誤的數據。
容器管理持久性(CMP)的實體Bean的事務級別無法修改,它們使用DBMS的默認個理解別,通常是READ_COMMITTED。
Bean管理持久性(BMP)的實體Bean和兩種會話Bean都可以通過在程序中調用底層DBMS提供的API來設置事務級別。例如,一個DBMS可能允許你如下調用setTransactionIsolation方法將隔離級別設置成可讀取未提交數據:
Connection con;
...
con.setTransactionIsolation(TRANSACTION_READ_UNCOMMITTED);
不要在事務執行期間更改隔離級別,通常隔離級別的更改會引起DBMS產生一次隱式提交。因為隔離級別的控制會跟具體的DBMS廠商不同而不同,具體的信息請參考DBMS的文檔。J2EE平臺規范不包括隔離級別標準。
七.更新多個數據庫
J2EE事務管理器控制著除了Bean管理的JDBC事務以外的所有企業Bean事務,它允許企業Bean在同一個事務中更新多個數據庫。下面示范在單個事務中更新多個數據庫的兩個應用。
圖14-2中,客戶端調用Bean-A的商業方法,商業方法啟動一個事務,更新數據庫X和Y,Bean-A的商業方法有調用Bean-B的商業方法,Bean-B的商業方法更新數據庫Z然后返回事務的控制權給Bean-A的商業方法,由Bean-A提交該事務。三個數據庫的更新都在同一個事務中發生。
圖 14-2 更新多個數據庫
圖14-3中,客戶端調用Bean-A的商業方法,該商業方法啟動一個事務并更新數據庫X,然后調用另一個J2EE服務器中的Bean-B的方法,該方法更新數據庫Y。J2EE服務器保證兩個數據庫的更新都在同一個事務中進行(筆者認為應該是第一個J2EE服務器的事務管理器管理整個事物)。
圖 14-3 跨越J2EE服務器更新多個數據庫
八.Web 組件事務
Web組件中劃分事務界限可以使用java.sql.Connection接口和javax.transaction.UserTransaction接口中的任意一個。跟Bean管理事務的會話Bean使用一樣的兩個接口。這兩個接口的使用方法參考前面幾節的內容。Web組件事務的例子在第10章Servlet技術第四節共享信息的訪問數據庫小節講述過。