在service類前加上@Transactional,聲明這個(gè)service所有方法需要事務(wù)管理。每一個(gè)業(yè)務(wù)方法開始時(shí)都會(huì)打開一個(gè)事務(wù)。
Spring默認(rèn)情況下會(huì)對(duì)運(yùn)行期例外(RunTimeException)進(jìn)行事務(wù)回滾。這個(gè)例外是unchecked
如果遇到checked意外就不回滾。
如何改變默認(rèn)規(guī)則:
1 讓checked例外也回滾:在整個(gè)方法前加上 @Transactional(rollbackFor=Exception.class)
2 讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class)
3 不需要事務(wù)管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)
注意: 如果異常被try{}catch{}了,事務(wù)就不回滾了,如果想讓事務(wù)回滾必須再往外拋try{}catch{throw Exception}。
spring——@Transactional事務(wù)不管理jdbc,所以要自己把jdbc事務(wù)回滾。
下面給出了回滾JDBC事務(wù)的代碼示例:
Java代碼
- public void processT(String orders) {
- Context initCtx = new InitialContext();
- javax.sql.DataSource ds = javax.sql.DataSource)initCtx.lookup
- (“java:comp/env/jdbc/OrdersDB”);
- java.sql.Connection conn = ds.getConnection();
- try {
- conn.setAutoCommit( false ); //更改JDBC事務(wù)的默認(rèn)提交方式
- orderNo = createOrder( orders );
- updateOrderStatus(orderNo, “orders created”);
- conn.commit(); //提交JDBC事務(wù)
- } catch ( Exception e ){
- try {
- conn.rollback(); //回滾sJDBC事務(wù)
- throw new EJBException(“事務(wù)回滾: “ + e.getMessage());
- } catch ( SQLException sqle ){
- throw new EJBException(“出現(xiàn)SQL操作錯(cuò)誤: “ + sqle.getMessage());
- }
- }
- }
下面給出了JTA事務(wù)代碼示例:
- public void processOrder(String orderMessage) {
- UserTransaction transaction = mySessionContext.getUserTransaction(); //獲得JTA事務(wù)
- try {
- transaction.begin(); //開始JTA事務(wù)
- orderNo = sendOrder(orderMessage);
- updateOrderStatus(orderNo, “order sent”);
- transaction.commit(); //提交JTA事務(wù)
- } catch (Exception e){
- try {
- transaction.rollback(); //回滾JTA事務(wù)
- } catch (SystemException se){
- se.printStackTrace();
- }
- throw new EJBException(“事務(wù)回滾: “ + e.getMessage());
- }
- }
在整個(gè)方法運(yùn)行前就不會(huì)開啟事務(wù)
還可以加上:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),這樣就做成一個(gè)只讀事務(wù),可以提高效率。
各種屬性的意義:
REQUIRED:業(yè)務(wù)方法需要在一個(gè)容器里運(yùn)行。如果方法運(yùn)行時(shí),已經(jīng)處在一個(gè)事務(wù)中,那么加入到這個(gè)事務(wù),否則自己新建一個(gè)新的事務(wù)。
NOT_SUPPORTED:聲明方法不需要事務(wù)。如果方法沒有關(guān)聯(lián)到一個(gè)事務(wù),容器不會(huì)為他開啟事務(wù),如果方法在一個(gè)事務(wù)中被調(diào)用,該事務(wù)會(huì)被掛起,調(diào)用結(jié)束后,原先的事務(wù)會(huì)恢復(fù)執(zhí)行。
REQUIRESNEW:不管是否存在事務(wù),該方法總匯為自己發(fā)起一個(gè)新的事務(wù)。如果方法已經(jīng)運(yùn)行在一個(gè)事務(wù)中,則原有事務(wù)掛起,新的事務(wù)被創(chuàng)建。
MANDATORY:該方法只能在一個(gè)已經(jīng)存在的事務(wù)中執(zhí)行,業(yè)務(wù)方法不能發(fā)起自己的事務(wù)。如果在沒有事務(wù)的環(huán)境下被調(diào)用,容器拋出例外。
SUPPORTS:該方法在某個(gè)事務(wù)范圍內(nèi)被調(diào)用,則方法成為該事務(wù)的一部分。如果方法在該事務(wù)范圍外被調(diào)用,該方法就在沒有事務(wù)的環(huán)境下執(zhí)行。
NEVER:該方法絕對(duì)不能在事務(wù)范圍內(nèi)執(zhí)行。如果在就拋例外。只有該方法沒有關(guān)聯(lián)到任何事務(wù),才正常執(zhí)行。
NESTED:如果一個(gè)活動(dòng)的事務(wù)存在,則運(yùn)行在一個(gè)嵌套的事務(wù)中。如果沒有活動(dòng)事務(wù),則按REQUIRED屬性執(zhí)行。它使用了一個(gè)單獨(dú)的事務(wù),這個(gè)事務(wù) 擁有多個(gè)可以回滾的保存點(diǎn)。內(nèi)部事務(wù)的回滾不會(huì)對(duì)外部事務(wù)造成影響。它只對(duì)DataSourceTransactionManager事務(wù)管理器起效。
事務(wù)陷阱-1
清單 1. 使用 JDBC 的簡(jiǎn)單數(shù)據(jù)庫(kù)插入
view plaincopy to clipboardprint?
@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName="java:jdbc/tradingDS") DataSource ds;
public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
try {
Statement sql = dbConnection.createStatement();
String stmt =
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
+ "VALUES ("
+ trade.getAcct() + "','"
+ trade.getAction() + "','"
+ trade.getSymbol() + "',"
+ trade.getShares() + ","
+ trade.getPrice() + ",'"
+ trade.getState() + "')";
sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = sql.getGeneratedKeys();
if (rs.next()) {
return rs.getBigDecimal(1).longValue();
} else {
throw new Exception("Trade Order Insert Failed");
}
} finally {
if (dbConnection != null) dbConnection.close();
}
}
}
@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName="java:jdbc/tradingDS") DataSource ds;
public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
try {
Statement sql = dbConnection.createStatement();
String stmt =
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
+ "VALUES ("
+ trade.getAcct() + "','"
+ trade.getAction() + "','"
+ trade.getSymbol() + "',"
+ trade.getShares() + ","
+ trade.getPrice() + ",'"
+ trade.getState() + "')";
sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = sql.getGeneratedKeys();
if (rs.next()) {
return rs.getBigDecimal(1).longValue();
} else {
throw new Exception("Trade Order Insert Failed");
}
} finally {
if (dbConnection != null) dbConnection.close();
}
}
}
清單 1 中的 JDBC 代碼沒有包含任何事務(wù)邏輯,它只是在數(shù)據(jù)庫(kù)中保存 TRADE 表中的交易訂單。在本例中,數(shù)據(jù)庫(kù)處理事務(wù)邏輯。
在 LUW 中,這是一個(gè)不錯(cuò)的單個(gè)數(shù)據(jù)庫(kù)維護(hù)操作。但是如果需要在向數(shù)據(jù)庫(kù)插入交易訂單的同時(shí)更新帳戶余款呢?如清單 2 所示:
清單 2. 在同一方法中執(zhí)行多次表更新
view plaincopy to clipboardprint?
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
在本例中,insertTrade() 和 updateAcct() 方法使用不帶事務(wù)的標(biāo)準(zhǔn) JDBC 代碼。insertTrade() 方法結(jié)束后,數(shù)據(jù)庫(kù)保存(并提交了)交易訂單。如果 updateAcct() 方法由于任意原因失敗,交易訂單仍然會(huì)在 placeTrade() 方法結(jié)束時(shí)保存在 TRADE 表內(nèi),這會(huì)導(dǎo)致數(shù)據(jù)庫(kù)出現(xiàn)不一致的數(shù)據(jù)。如果 placeTrade() 方法使用了事務(wù),這兩個(gè)活動(dòng)都會(huì)包含在一個(gè) LUW 中,如果帳戶更新失敗,交易訂單就會(huì)回滾。
事務(wù)陷阱-2
隨著 Java 持久性框架的不斷普及,如 Hibernate、TopLink 和 Java 持久性 API(Java Persistence API,JPA),我們很少再會(huì)去編寫簡(jiǎn)單的 JDBC 代碼。更常見的情況是,我們使用更新的對(duì)象關(guān)系映射(ORM)框架來減輕工作,即用幾個(gè)簡(jiǎn)單的方法調(diào)用替換所有麻煩的 JDBC 代碼。例如,要插入 清單 1 中 JDBC 代碼示例的交易訂單,使用帶有 JPA 的 Spring Framework,就可以將 TradeData 對(duì)象映射到 TRADE 表,并用清單 3 中的 JPA 代碼替換所有 JDBC 代碼:
清單 3. 使用 JPA 的簡(jiǎn)單插入
view plaincopy to clipboardprint?
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
注意,清單 3 在 EntityManager 上調(diào)用了 persist() 方法來插入交易訂單。很簡(jiǎn)單,是吧?其實(shí)不然。這段代碼不會(huì)像預(yù)期那樣向 TRADE 表插入交易訂單,也不會(huì)拋出異常。它只是返回一個(gè)值 0 作為交易訂單的鍵,而不會(huì)更改數(shù)據(jù)庫(kù)。這是事務(wù)處理的主要陷阱之一:基于 ORM 的框架需要一個(gè)事務(wù)來觸發(fā)對(duì)象緩存與數(shù)據(jù)庫(kù)之間的同步。這通過一個(gè)事務(wù)提交完成,其中會(huì)生成 SQL 代碼,數(shù)據(jù)庫(kù)會(huì)執(zhí)行需要的操作(即插入、更新、刪除)。沒有事務(wù),就不會(huì)觸發(fā) ORM 去生成 SQL 代碼和保存更改,因此只會(huì)終止方法 — 沒有異常,沒有更新。如果使用基于 ORM 的框架,就必須利用事務(wù)。您不再依賴數(shù)據(jù)庫(kù)來管理連接和提交工作。
這些簡(jiǎn)單的示例應(yīng)該清楚地說明,為了維護(hù)數(shù)據(jù)完整性和一致性,必須使用事務(wù)。不過對(duì)于在 Java 平臺(tái)中實(shí)現(xiàn)事務(wù)的復(fù)雜性和陷阱而言,這些示例只是涉及了冰山一角。
Spring Framework @Transactional 注釋陷阱-3
清單 4. 使用 @Transactional 注釋
view plaincopy to clipboardprint?
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
@Transactional
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
@Transactional
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
現(xiàn)在重新測(cè)試代碼,您發(fā)現(xiàn)上述方法仍然不能工作。問題在于您必須告訴 Spring Framework,您正在對(duì)事務(wù)管理應(yīng)用注釋。除非您進(jìn)行充分的單元測(cè)試,否則有時(shí)候很難發(fā)現(xiàn)這個(gè)陷阱。這通常只會(huì)導(dǎo)致開發(fā)人員在 Spring 配置文件中簡(jiǎn)單地添加事務(wù)邏輯,而不會(huì)使用注釋。
要在 Spring 中使用 @Transactional 注釋,必須在 Spring 配置文件中添加以下代碼行:
view plaincopy to clipboardprint?
< tx:annotation-driven transaction-manager="transactionManager"/>
< tx:annotation-driven transaction-manager="transactionManager"/>
transaction-manager 屬性保存一個(gè)對(duì)在 Spring 配置文件中定義的事務(wù)管理器 bean 的引用。這段代碼告訴 Spring 在應(yīng)用事務(wù)攔截器時(shí)使用 @Transaction 注釋。如果沒有它,就會(huì)忽略 @Transactional 注釋,導(dǎo)致代碼不會(huì)使用任何事務(wù)。
讓基本的 @Transactional 注釋在 清單 4 的代碼中工作僅僅是開始。注意,清單 4 使用 @Transactional 注釋時(shí)沒有指定任何額外的注釋參數(shù)。我發(fā)現(xiàn)許多開發(fā)人員在使用 @Transactional 注釋時(shí)并沒有花時(shí)間理解它的作用。例如,像我一樣在清單 4 中單獨(dú)使用 @Transactional 注釋時(shí),事務(wù)傳播模式被設(shè)置成什么呢?只讀標(biāo)志被設(shè)置成什么呢?事務(wù)隔離級(jí)別的設(shè)置是怎樣的?更重要的是,事務(wù)應(yīng)何時(shí)回滾工作?理解如何使用這個(gè)注釋對(duì)于 確保在應(yīng)用程序中獲得合適的事務(wù)支持級(jí)別非常重要。回答我剛才提出的問題:在單獨(dú)使用不帶任何參數(shù)的 @Transactional 注釋時(shí),傳播模式要設(shè)置為 REQUIRED,只讀標(biāo)志設(shè)置為 false,事務(wù)隔離級(jí)別設(shè)置為 READ_COMMITTED,而且事務(wù)不會(huì)針對(duì)受控異常(checked exception)回滾。
@Transactional 只讀標(biāo)志陷阱
我在工作中經(jīng)常碰到的一個(gè)常見陷阱是 Spring @Transactional 注釋中的只讀標(biāo)志沒有得到恰當(dāng)使用。這里有一個(gè)快速測(cè)試方法:在使用標(biāo)準(zhǔn) JDBC 代碼獲得 Java 持久性時(shí),如果只讀標(biāo)志設(shè)置為 true,傳播模式設(shè)置為 SUPPORTS,清單 5 中的 @Transactional 注釋的作用是什么呢?
清單 5. 將只讀標(biāo)志與 SUPPORTS 傳播模式結(jié)合使用 — JDBC
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
//JDBC Code...
}
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
//JDBC Code...
}
當(dāng)執(zhí)行清單 5 中的 insertTrade() 方法時(shí),猜一猜會(huì)得到下面哪一種結(jié)果:
拋出一個(gè)只讀連接異常
正確插入交易訂單并提交數(shù)據(jù)
什么也不做,因?yàn)閭鞑ゼ?jí)別被設(shè)置為 SUPPORTS
是哪一個(gè)呢?正確答案是 B。交易訂單會(huì)被正確地插入到數(shù)據(jù)庫(kù)中,即使只讀標(biāo)志被設(shè)置為 true,且事務(wù)傳播模式被設(shè)置為 SUPPORTS。但這是如何做到的呢?由于傳播模式被設(shè)置為 SUPPORTS,所以不會(huì)啟動(dòng)任何事物,因此該方法有效地利用了一個(gè)本地(數(shù)據(jù)庫(kù))事務(wù)。只讀標(biāo)志只在事務(wù)啟動(dòng)時(shí)應(yīng)用。在本例中,因?yàn)闆]有啟動(dòng)任何事 務(wù),所以只讀標(biāo)志被忽略。
Spring Framework @Transactional 注釋陷阱-4
清單 6 中的 @Transactional 注釋在設(shè)置了只讀標(biāo)志且傳播模式被設(shè)置為 REQUIRED 時(shí),它的作用是什么呢?
清單 6. 將只讀標(biāo)志與 REQUIRED 傳播模式結(jié)合使用 — JDBC
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
//JDBC code...
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
//JDBC code...
}
執(zhí)行清單 6 中的 insertTrade() 方法會(huì)得到下面哪一種結(jié)果呢:
拋出一個(gè)只讀連接異常
正確插入交易訂單并提交數(shù)據(jù)
什么也不做,因?yàn)橹蛔x標(biāo)志被設(shè)置為 true
根據(jù)前面的解釋,這個(gè)問題應(yīng)該很好回答。正確的答案是 A。會(huì)拋出一個(gè)異常,表示您正在試圖對(duì)一個(gè)只讀連接執(zhí)行更新。因?yàn)閱?dòng)了一個(gè)事務(wù)(REQUIRED),所以連接被設(shè)置為只讀。毫無疑問,在試圖執(zhí)行 SQL 語句時(shí),您會(huì)得到一個(gè)異常,告訴您該連接是一個(gè)只讀連接。
關(guān)于只讀標(biāo)志很奇怪的一點(diǎn)是:要使用它,必須啟動(dòng)一個(gè)事務(wù)。如果只是讀取數(shù)據(jù),需要事務(wù)嗎?答案是根本不需要。啟動(dòng)一個(gè)事務(wù)來執(zhí)行只讀操作會(huì)增加處 理線程的開銷,并會(huì)導(dǎo)致數(shù)據(jù)庫(kù)發(fā)生共享讀取鎖定(具體取決于使用的數(shù)據(jù)庫(kù)類型和設(shè)置的隔離級(jí)別)。總的來說,在獲取基于 JDBC 的 Java 持久性時(shí),使用只讀標(biāo)志有點(diǎn)毫無意義,并會(huì)啟動(dòng)不必要的事務(wù)而增加額外的開銷。
使用基于 ORM 的框架會(huì)怎樣呢?按照上面的測(cè)試,如果在結(jié)合使用 JPA 和 Hibernate 時(shí)調(diào)用 insertTrade() 方法,清單 7 中的 @Transactional 注釋會(huì)得到什么結(jié)果?
清單 7. 將只讀標(biāo)志與 REQUIRED 傳播模式結(jié)合使用 — JPA
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
清單 7 中的 insertTrade() 方法會(huì)得到下面哪一種結(jié)果:
拋出一個(gè)只讀連接異常
正確插入交易訂單并提交數(shù)據(jù)
什么也不做,因?yàn)?readOnly 標(biāo)志被設(shè)置為 true
正確的答案是 B。交易訂單會(huì)被準(zhǔn)確無誤地插入數(shù)據(jù)庫(kù)中。請(qǐng)注意,上一示例表明,在使用 REQUIRED 傳播模式時(shí),會(huì)拋出一個(gè)只讀連接異常。使用 JDBC 時(shí)是這樣。使用基于 ORM 的框架時(shí),只讀標(biāo)志只是對(duì)數(shù)據(jù)庫(kù)的一個(gè)提示,并且一條基于 ORM 框架的指令(本例中是 Hibernate)將對(duì)象緩存的 flush 模式設(shè)置為 NEVER,表示在這個(gè)工作單元中,該對(duì)象緩存不應(yīng)與數(shù)據(jù)庫(kù)同步。不過,REQUIRED 傳播模式會(huì)覆蓋所有這些內(nèi)容,允許事務(wù)啟動(dòng)并工作,就好像沒有設(shè)置只讀標(biāo)志一樣。
這令我想到了另一個(gè)我經(jīng)常碰到的主要陷阱。閱讀了前面的所有內(nèi)容后,您認(rèn)為如果只對(duì) @Transactional 注釋設(shè)置只讀標(biāo)志,清單 8 中的代碼會(huì)得到什么結(jié)果呢?
清單 8. 使用只讀標(biāo)志 — JPA
view plaincopy to clipboardprint?
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
清單 8 中的 getTrade() 方法會(huì)執(zhí)行以下哪一種操作?
啟動(dòng)一個(gè)事務(wù),獲取交易訂單,然后提交事務(wù)
獲取交易訂單,但不啟動(dòng)事務(wù)
正確的答案是 A。一個(gè)事務(wù)會(huì)被啟動(dòng)并提交。不要忘了,@Transactional 注釋的默認(rèn)傳播模式是 REQUIRED。這意味著事務(wù)會(huì)在不必要的情況下啟動(dòng)。根據(jù)使用的數(shù)據(jù)庫(kù),這會(huì)引起不必要的共享鎖,可能會(huì)使數(shù)據(jù)庫(kù)中出現(xiàn)死鎖的情況。此外,啟動(dòng)和停止 事務(wù)將消耗不必要的處理時(shí)間和資源。總的來說,在使用基于 ORM 的框架時(shí),只讀標(biāo)志基本上毫無用處,在大多數(shù)情況下會(huì)被忽略。但如果您堅(jiān)持使用它,請(qǐng)記得將傳播模式設(shè)置為 SUPPORTS(如清單 9 所示),這樣就不會(huì)啟動(dòng)事務(wù):
清單 9. 使用只讀標(biāo)志和 SUPPORTS 傳播模式進(jìn)行選擇操作
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
另外,在執(zhí)行讀取操作時(shí),避免使用 @Transactional 注釋,如清單 10 所示:
清單 10. 刪除 @Transactional 注釋進(jìn)行選擇操作
view plaincopy to clipboardprint?
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
REQUIRES_NEW 事務(wù)屬性陷阱
不管是使用 Spring Framework,還是使用 EJB,使用 REQUIRES_NEW 事務(wù)屬性都會(huì)得到不好的結(jié)果并導(dǎo)致數(shù)據(jù)損壞和不一致。REQUIRES_NEW 事務(wù)屬性總是會(huì)在啟動(dòng)方法時(shí)啟動(dòng)一個(gè)新的事務(wù)。許多開發(fā)人員都錯(cuò)誤地使用 REQUIRES_NEW 屬性,認(rèn)為它是確保事務(wù)啟動(dòng)的正確方法。
Spring Framework @Transactional 注釋陷阱-5
清單 11. 使用 REQUIRES_NEW 事務(wù)屬性
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}
注意,清單 11 中的兩個(gè)方法都是公共方法,這意味著它們可以單獨(dú)調(diào)用。當(dāng)使用 REQUIRES_NEW 屬性的幾個(gè)方法通過服務(wù)間通信或編排在同一邏輯工作單元內(nèi)調(diào)用時(shí),該屬性就會(huì)出現(xiàn)問題。例如,假設(shè)在清單 11 中,您可以獨(dú)立于一些用例中的任何其他方法來調(diào)用 updateAcct() 方法,但也有在 insertTrade() 方法中調(diào)用 updateAcct() 方法的情況。現(xiàn)在如果調(diào)用 updateAcct() 方法后拋出異常,交易訂單就會(huì)回滾,但帳戶更新將會(huì)提交給數(shù)據(jù)庫(kù),如清單 12 所示:
清單 12. 使用 REQUIRES_NEW 事務(wù)屬性的多次更新
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
updateAcct(trade);
//exception occurs here! Trade rolled back but account update is not!
...
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
updateAcct(trade);
//exception occurs here! Trade rolled back but account update is not!
...
}
之所以會(huì)發(fā)生這種情況是因?yàn)?updateAcct() 方法中啟動(dòng)了一個(gè)新事務(wù),所以在 updateAcct() 方法結(jié)束后,事務(wù)將被提交。使用 REQUIRES_NEW 事務(wù)屬性時(shí),如果存在現(xiàn)有事務(wù)上下文,當(dāng)前的事務(wù)會(huì)被掛起并啟動(dòng)一個(gè)新事務(wù)。方法結(jié)束后,新的事務(wù)被提交,原來的事務(wù)繼續(xù)執(zhí)行。
由于這種行為,只有在被調(diào)用方法中的數(shù)據(jù)庫(kù)操作需要保存到數(shù)據(jù)庫(kù)中,而不管覆蓋事務(wù)的結(jié)果如何時(shí),才應(yīng)該使用 REQUIRES_NEW 事務(wù)屬性。比如,假設(shè)嘗試的所有股票交易都必須被記錄在一個(gè)審計(jì)數(shù)據(jù)庫(kù)中。出于驗(yàn)證錯(cuò)誤、資金不足或其他原因,不管交易是否失敗,這條信息都需要被持久 化。如果沒有對(duì)審計(jì)方法使用 REQUIRES_NEW 屬性,審計(jì)記錄就會(huì)連同嘗試執(zhí)行的交易一起回滾。使用 REQUIRES_NEW 屬性可以確保不管初始事務(wù)的結(jié)果如何,審計(jì)數(shù)據(jù)都會(huì)被保存。這里要注意的一點(diǎn)是,要始終使用 MANDATORY 或 REQUIRED 屬性,而不是 REQUIRES_NEW,除非您有足夠的理由來使用它,類似審計(jì)示例中的那些理由。
事務(wù)回滾陷阱
我將最常見的事務(wù)陷阱留到最后來講。遺憾的是,我在生產(chǎn)代碼中多次遇到這個(gè)錯(cuò)誤。我首先從 Spring Framework 開始,然后介紹 EJB 3。
到目前為止,您研究的代碼類似清單 13 所示:
清單 13. 沒有回滾支持
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
假設(shè)帳戶中沒有足夠的資金來購(gòu)買需要的股票,或者還沒有準(zhǔn)備購(gòu)買或出售股票,并拋出了一個(gè)受控異常(例如 FundsNotAvailableException),那么交易訂單會(huì)保存在數(shù)據(jù)庫(kù)中嗎?還是整個(gè)邏輯工作單元將執(zhí)行回滾?答案出乎意料:根據(jù)受控異 常(不管是在 Spring Framework 中還是在 EJB 中),事務(wù)會(huì)提交它還未提交的所有工作。使用清單 13,這意味著,如果在執(zhí)行 updateAcct() 方法期間拋出受控異常,就會(huì)保存交易訂單,但不會(huì)更新帳戶來反映交易情況。
這可能是在使用事務(wù)時(shí)出現(xiàn)的主要數(shù)據(jù)完整性和一致性問題了。運(yùn)行時(shí)異常(即非受控異常)自動(dòng)強(qiáng)制執(zhí)行整個(gè)邏輯工作單元的回滾,但受控異常不會(huì)。因此,清單 13 中的代碼從事務(wù)角度來說毫無用處;盡管看上去它使用事務(wù)來維護(hù)原子性和一致性,但事實(shí)上并沒有。
盡管這種行為看起來很奇怪,但這樣做自有它的道理。首先,不是所有受控異常都是不好的;它們可用于事件通知或根據(jù)某些條件重定向處理。但更重要的 是,應(yīng)用程序代碼會(huì)對(duì)某些類型的受控異常采取糾正操作,從而使事務(wù)全部完成。例如,考慮下面一種場(chǎng)景:您正在為在線書籍零售商編寫代碼。要完成圖書的訂 單,您需要將電子郵件形式的確認(rèn)函作為訂單處理的一部分發(fā)送。如果電子郵件服務(wù)器關(guān)閉,您將發(fā)送某種形式的 SMTP 受控異常,表示郵件無法發(fā)送。如果受控異常引起自動(dòng)回滾,整個(gè)圖書訂單就會(huì)由于電子郵件服務(wù)器的關(guān)閉全部回滾。通過禁止自動(dòng)回滾受控異常,您可以捕獲該異 常并執(zhí)行某種糾正操作(如向掛起隊(duì)列發(fā)送消息),然后提交剩余的訂單。
Spring Framework @Transactional 注釋陷阱-6
使用 Declarative 事務(wù)模式時(shí),必須指定容器或框架應(yīng)該如何處理受控異常。在 Spring Framework 中,通過 @Transactional 注釋中的 rollbackFor 參數(shù)進(jìn)行指定,如清單 14 所示:
清單 14. 添加事務(wù)回滾支持 — Spring
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
注意,@Transactional 注釋中使用了 rollbackFor 參數(shù)。這個(gè)參數(shù)接受一個(gè)單一異常類或一組異常類,您也可以使用 rollbackForClassName 參數(shù)將異常的名稱指定為 Java String 類型。還可以使用此屬性的相反形式(noRollbackFor)指定除某些異常以外的所有異常應(yīng)該強(qiáng)制回滾。通常大多數(shù)開發(fā)人員指定 Exception.class 作為值,表示該方法中的所有異常應(yīng)該強(qiáng)制回滾。
在回滾事務(wù)這一點(diǎn)上,EJB 的工作方式與 Spring Framework 稍微有點(diǎn)不同。EJB 3.0 規(guī)范中的 @TransactionAttribute 注釋不包含指定回滾行為的指令。必須使用 SessionContext.setRollbackOnly() 方法將事務(wù)標(biāo)記為執(zhí)行回滾,如清單 15 所示:
清單 15. 添加事務(wù)回滾支持 — EJB
view plaincopy to clipboardprint?
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
sessionCtx.setRollbackOnly();
throw up;
}
}
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
sessionCtx.setRollbackOnly();
throw up;
}
}
調(diào)用 setRollbackOnly() 方法后,就不能改變主意了;惟一可能的結(jié)果是在啟動(dòng)事務(wù)的方法完成后回滾事務(wù)。本系列后續(xù)文章中描述的事務(wù)策略將介紹何時(shí)、何處使用回滾指令,以及何時(shí)使用 REQUIRED 與 MANDATORY 事務(wù)屬性。
Isolation Level(事務(wù)隔離等級(jí))
1、Serializable:最嚴(yán)格的級(jí)別,事務(wù)串行執(zhí)行,資源消耗最大;
2、REPEATABLE READ:保證了一個(gè)事務(wù)不會(huì)修改已經(jīng)由另一個(gè)事務(wù)讀取但未提交(回滾)的數(shù)據(jù)。避免了“臟讀取”和“不可重復(fù)讀取”的情況,但是帶來了更多的性能損失。
3、READ COMMITTED:大多數(shù)主流數(shù)據(jù)庫(kù)的默認(rèn)事務(wù)等級(jí),保證了一個(gè)事務(wù)不會(huì)讀到另一個(gè)并行事務(wù)已修改但未提交的數(shù)據(jù),避免了“臟讀取”。該級(jí)別適用于大多數(shù)系統(tǒng)。
4、Read Uncommitted:保證了讀取過程中不會(huì)讀取到非法數(shù)據(jù)。隔離級(jí)別在于處理多事務(wù)的并發(fā)問題。
我們知道并行可以提高數(shù)據(jù)庫(kù)的吞吐量和效率,但是并不是所有的并發(fā)事務(wù)都可以并發(fā)運(yùn)行。
我們首先說并發(fā)中可能發(fā)生的3中不討人喜歡的事情
1: Dirty reads--讀臟數(shù)據(jù)。也就是說,比如事務(wù)A的未提交(還依然緩存)的數(shù)據(jù)被事務(wù)B讀走,如果事務(wù)A失敗回滾,會(huì)導(dǎo)致事務(wù)B所讀取的的數(shù)據(jù)是錯(cuò)誤的。
2: non-repeatable reads--數(shù)據(jù)不可重復(fù)讀。比如事務(wù)A中兩處讀取數(shù)據(jù)-total-的值。在第一讀的時(shí)候,total是100,然后事務(wù)B就把total的數(shù)據(jù)改成 200,事務(wù)A再讀一次,結(jié)果就發(fā)現(xiàn),total竟然就變成200了,造成事務(wù)A數(shù)據(jù)混亂。
3: phantom reads--幻象讀數(shù)據(jù),這個(gè)和non-repeatable reads相似,也是同一個(gè)事務(wù)中多次讀不一致的問題。但是non-repeatable reads的不一致是因?yàn)樗〉臄?shù)據(jù)集被改變了(比如total的數(shù)據(jù)),但是phantom reads所要讀的數(shù)據(jù)的不一致卻不是他所要讀的數(shù)據(jù)集改變,而是他的條件數(shù)據(jù)集改變。比如Select account.id where account.name="ppgogo*",第一次讀去了6個(gè)符合條件的id,第二次讀取的時(shí)候,由于事務(wù)b把一個(gè)帳號(hào)的名字由"dd"改 成"ppgogo1",結(jié)果取出來了7個(gè)數(shù)據(jù)。
Dirty reads | non-repeatable reads | phantom reads | |
Serializable | 不會(huì) | 不會(huì) | 不會(huì) |
REPEATABLE READ | 不會(huì) | 不會(huì) | 會(huì) |
READ COMMITTED | 不會(huì) | 會(huì) | 會(huì) |
Read Uncommitted | 會(huì) | 會(huì) | 會(huì) |
readOnly
事務(wù)屬性中的readOnly標(biāo)志表示對(duì)應(yīng)的事務(wù)應(yīng)該被最優(yōu)化為只讀事務(wù)。