Spring中註解事務@Transactional說明

// 業務方法需要在一個事務中運行,如果方法運行時,已經存在一個事務中,

// 那麼加入該事務,否則爲自己創建一個新事務。

@Transactional(propagation = Propagation.REQUIRED)

public void test1() {

}

// 聲明方法不需要事務,如果方法沒有關聯到一個事務,容器不會爲它開啓事務。

// 如果方法在一個事務中被調用,該事務會被掛起,在方法調用結束後,原先的

// 事務便會恢復執行。

@Transactional(propagation = Propagation.NOT_SUPPORTED)

public void test2() {

}

// 表明不管是否存在事務,業務方法總會爲自己發起一個新事務。

// 如果方法已經運行在一個事務中,則原有事務會被掛起,

// 新的事務會被創建,直到方法執行結束,新事務纔算結束,

// 原先的事務纔會被恢復執行。

@Transactional(propagation = Propagation.REQUIRES_NEW)

public void test3() {

}

// 該屬性指定業務方法只能在一個已經存在的事務中執行,

// 業務方法不能發起自己的事務,如果業務方法在沒有事務的環境

// 下調用,容器就會拋出異常。

@Transactional(propagation = Propagation.MANDATORY)

public void test4() {

}

// 這個事務屬性表明,如果業務方法在某個事務範圍內被調用,則方法成爲該事務的一部分,

// 如果業務方法在事務範圍外被調用,則方法在沒有事務的環境下執行。

@Transactional(propagation = Propagation.SUPPORTS)

public void test5() {

}

// 指定業務方法絕對不能在事務範圍內執行。如果業務方法在某個事務中執行,

// 容器會拋出異常,只有業務方法沒有關聯到任何事務,才能正常執行。

@Transactional(propagation = Propagation.NEVER)

public void test6() {

}

// 如果一個活動的事務存在,則運行在一個嵌套的事務中,如果沒有活動事務,

// 則按REQUIRED屬性執行,它使用了一個單獨的事務,這個事務擁有多個回滾的保存點,

// 內部事務的回滾不會對外事務造成影響,它只對DataSourceTransactionManager

// 事務管理器起效。

@Transactional(propagation = Propagation.NESTED)

public void test7() {

}

@Transactional(isolation = Isolation.DEFAULT)

public void test8() {

}

// 讀已提交數據(會出現不可重複讀和幻讀)

@Transactional(isolation = Isolation.READ_COMMITTED)

public void test9() {

}

// 讀未提交數據(會出現髒讀、不可重複讀和幻讀)

@Transactional(isolation = Isolation.READ_UNCOMMITTED)

public void test10() {

}

// 可重複讀(會出現幻讀)

@Transactional(isolation = Isolation.REPEATABLE_READ)

public void test11() {

}

// 串行化

@Transactional(isolation = Isolation.SERIALIZABLE)

public void test12() {

}

// 拋出Exception異常時,記錄回滾

@Transactional(rollbackFor = Exception.class)

public void test13() throws Exception {

}

// 拋出Exception異常時,記錄不回滾

@Transactional(noRollbackFor = Exception.class)

public void test14() throws Exception {

}

髒讀:一個事務讀取到另一個事務未提交的更新數據。

不可重複讀:在同一事務中,多次讀取同一數據返回的結果有所不同。換句話說就是,後續讀取可以讀到另一事務已提交的更新數據。

可重複讀:在同一事務中多次讀取數據時,能夠保證所讀數據一樣,也就是,後續讀取不能讀到另一事務已提交的更新數據。

幻讀:一個事務讀取到另一事務提交的insert數據。

 

Property Type Description
value String Optional qualifier specifying the transaction manager to be used.
propagation enum: Propagation Optional propagation setting.
isolation enum: Isolation Optional isolation level.
readOnly boolean Read/write vs. read-only transaction
timeout int (in seconds granularity) Transaction timeout.
rollbackFor Array of Class objects, which must be derived from Throwable. Optional array of exception classes thatmust cause rollback.
rollbackForClassname Array of class names. Classes must be derived from Throwable. Optional array of names of exception classes that must cause rollback.
noRollbackFor Array of Class objects, which must be derived from Throwable. Optional array of exception classes thatmust not cause rollback.
noRollbackForClassname Array of String class names, which must be derived from Throwable. Optional array of names of exception classes that must not cause rollback.

 

 


在service類前加上@Transactional,聲明這個service所有方法需要事務管理。每一個業務方法開始時都會打開一個事務。


Spring默認情況下會對運行期例外(RunTimeException)進行事務回滾。這個例外是unchecked


如果遇到checked意外就不回滾。


如何改變默認規則:


1 讓checked例外也回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class)


2 讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class)


3 不需要事務管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)


 

注意: 如果異常被try{}catch{}了,事務就不回滾了,如果想讓事務回滾必須再往外拋try{}catch{throw Exception}。

 

spring——@Transactional事務不管理jdbc,所以要自己把jdbc事務回滾。

@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),這樣就做成一個只讀事務,可以提高效率。


       各種屬性的意義:


       REQUIRED:業務方法需要在一個容器裏運行。如果方法運行時,已經處在一個事務中,那麼加入到這個事務,否則自己新建一個新的事務。


       NOT_SUPPORTED:聲明方法不需要事務。如果方法沒有關聯到一個事務,容器不會爲他開啓事務,如果方法在一個事務中被調用,該事務會被掛起,調用結束後,原先的事務會恢復執行。


       REQUIRESNEW:不管是否存在事務,該方法總彙爲自己發起一個新的事務。如果方法已經運行在一個事務中,則原有事務掛起,新的事務被創建。


       MANDATORY:該方法只能在一個已經存在的事務中執行,業務方法不能發起自己的事務。如果在沒有事務的環境下被調用,容器拋出例外。


       SUPPORTS:該方法在某個事務範圍內被調用,則方法成爲該事務的一部分。如果方法在該事務範圍外被調用,該方法就在沒有事務的環境下執行。


       NEVER:該方法絕對不能在事務範圍內執行。如果在就拋例外。只有該方法沒有關聯到任何事務,才正常執行。


       NESTED:如果一個活動的事務存在,則運行在一個嵌套的事務中。如果沒有活動事務,則按REQUIRED屬性執行。它使用了一個單獨的事務,這個事務 擁有多個可以回滾的保存點。內部事務的回滾不會對外部事務造成影響。它只對DataSourceTransactionManager事務管理器起效。

事務陷阱-1


 


清單 1. 使用 JDBC 的簡單數據庫插入


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 代碼沒有包含任何事務邏輯,它只是在數據庫中保存 TRADE 表中的交易訂單。在本例中,數據庫處理事務邏輯。


在 LUW 中,這是一個不錯的單個數據庫維護操作。但是如果需要在向數據庫插入交易訂單的同時更新帳戶餘款呢?如清單 2 所示:

 

清單 2. 在同一方法中執行多次表更新


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() 方法使用不帶事務的標準 JDBC 代碼。insertTrade() 方法結束後,數據庫保存(並提交了)交易訂單。如果 updateAcct() 方法由於任意原因失敗,交易訂單仍然會在 placeTrade() 方法結束時保存在 TRADE 表內,這會導致數據庫出現不一致的數據。如果 placeTrade() 方法使用了事務,這兩個活動都會包含在一個 LUW 中,如果帳戶更新失敗,交易訂單就會回滾。


 


 


事務陷阱-2


 


隨着 Java 持久性框架的不斷普及,如 Hibernate、TopLink 和 Java 持久性 API(Java Persistence API,JPA),我們很少再會去編寫簡單的 JDBC 代碼。更常見的情況是,我們使用更新的對象關係映射(ORM)框架來減輕工作,即用幾個簡單的方法調用替換所有麻煩的 JDBC 代碼。例如,要插入 清單 1 中 JDBC 代碼示例的交易訂單,使用帶有 JPA 的 Spring Framework,就可以將 TradeData 對象映射到 TRADE 表,並用清單 3 中的 JPA 代碼替換所有 JDBC 代碼:

 

清單 3. 使用 JPA 的簡單插入


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 上調用了 persist() 方法來插入交易訂單。很簡單,是吧?其實不然。這段代碼不會像預期那樣向 TRADE 表插入交易訂單,也不會拋出異常。它只是返回一個值 0 作爲交易訂單的鍵,而不會更改數據庫。這是事務處理的主要陷阱之一:基於 ORM 的框架需要一個事務來觸發對象緩存與數據庫之間的同步。這通過一個事務提交完成,其中會生成 SQL 代碼,數據庫會執行需要的操作(即插入、更新、刪除)。沒有事務,就不會觸發 ORM 去生成 SQL 代碼和保存更改,因此只會終止方法 — 沒有異常,沒有更新。如果使用基於 ORM 的框架,就必須利用事務。您不再依賴數據庫來管理連接和提交工作。


這些簡單的示例應該清楚地說明,爲了維護數據完整性和一致性,必須使用事務。不過對於在 Java 平臺中實現事務的複雜性和陷阱而言,這些示例只是涉及了冰山一角。


 


 


 


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();

   }

}


現在重新測試代碼,您發現上述方法仍然不能工作。問題在於您必須告訴 Spring Framework,您正在對事務管理應用註釋。除非您進行充分的單元測試,否則有時候很難發現這個陷阱。這通常只會導致開發人員在 Spring 配置文件中簡單地添加事務邏輯,而不會使用註釋。


要在 Spring 中使用 @Transactional 註釋,必須在 Spring 配置文件中添加以下代碼行:


view plaincopy to clipboardprint?

<tx:annotation-driven transaction-manager="transactionManager"/>

<tx:annotation-driven transaction-manager="transactionManager"/>


transaction-manager 屬性保存一個對在 Spring 配置文件中定義的事務管理器 bean 的引用。這段代碼告訴 Spring 在應用事務攔截器時使用 @Transaction 註釋。如果沒有它,就會忽略 @Transactional 註釋,導致代碼不會使用任何事務。


讓基本的 @Transactional 註釋在 清單 4 的代碼中工作僅僅是開始。注意,清單 4 使用 @Transactional 註釋時沒有指定任何額外的註釋參數。我發現許多開發人員在使用 @Transactional 註釋時並沒有花時間理解它的作用。例如,像我一樣在清單 4 中單獨使用 @Transactional 註釋時,事務傳播模式被設置成什麼呢?只讀標誌被設置成什麼呢?事務隔離級別的設置是怎樣的?更重要的是,事務應何時回滾工作?理解如何使用這個註釋對於 確保在應用程序中獲得合適的事務支持級別非常重要。回答我剛纔提出的問題:在單獨使用不帶任何參數的 @Transactional 註釋時,傳播模式要設置爲 REQUIRED,只讀標誌設置爲 false,事務隔離級別設置爲 READ_COMMITTED,而且事務不會針對受控異常(checked exception)回滾。


@Transactional 只讀標誌陷阱


我在工作中經常碰到的一個常見陷阱是 Spring @Transactional 註釋中的只讀標誌沒有得到恰當使用。這裏有一個快速測試方法:在使用標準 JDBC 代碼獲得 Java 持久性時,如果只讀標誌設置爲 true,傳播模式設置爲 SUPPORTS,清單 5 中的 @Transactional 註釋的作用是什麼呢?

 

清單 5. 將只讀標誌與 SUPPORTS 傳播模式結合使用 — 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...

}


當執行清單 5 中的 insertTrade() 方法時,猜一猜會得到下面哪一種結果:

拋出一個只讀連接異常

正確插入交易訂單並提交數據

什麼也不做,因爲傳播級別被設置爲 SUPPORTS

是哪一個呢?正確答案是 B。交易訂單會被正確地插入到數據庫中,即使只讀標誌被設置爲 true,且事務傳播模式被設置爲 SUPPORTS。但這是如何做到的呢?由於傳播模式被設置爲 SUPPORTS,所以不會啓動任何事務,因此該方法有效地利用了一個本地(數據庫)事務。只讀標誌只在事務啓動時應用。在本例中,因爲沒有啓動任何事 務,所以只讀標誌被忽略。


 


 


Spring Framework @Transactional 註釋陷阱-4


 


清單 6 中的 @Transactional 註釋在設置了只讀標誌且傳播模式被設置爲 REQUIRED 時,它的作用是什麼呢?

 

清單 6. 將只讀標誌與 REQUIRED 傳播模式結合使用 — 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...

}


執行清單 6 中的 insertTrade() 方法會得到下面哪一種結果呢:


拋出一個只讀連接異常

正確插入交易訂單並提交數據

什麼也不做,因爲只讀標誌被設置爲 true

根據前面的解釋,這個問題應該很好回答。正確的答案是 A。會拋出一個異常,表示您正在試圖對一個只讀連接執行更新。因爲啓動了一個事務(REQUIRED),所以連接被設置爲只讀。毫無疑問,在試圖執行 SQL 語句時,您會得到一個異常,告訴您該連接是一個只讀連接。


關於只讀標誌很奇怪的一點是:要使用它,必須啓動一個事務。如果只是讀取數據,需要事務嗎?答案是根本不需要。啓動一個事務來執行只讀操作會增加處 理線程的開銷,並會導致數據庫發生共享讀取鎖定(具體取決於使用的數據庫類型和設置的隔離級別)。總的來說,在獲取基於 JDBC 的 Java 持久性時,使用只讀標誌有點毫無意義,並會啓動不必要的事務而增加額外的開銷。


使用基於 ORM 的框架會怎樣呢?按照上面的測試,如果在結合使用 JPA 和 Hibernate 時調用 insertTrade() 方法,清單 7 中的 @Transactional 註釋會得到什麼結果?

 

清單 7. 將只讀標誌與 REQUIRED 傳播模式結合使用 — 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() 方法會得到下面哪一種結果:


拋出一個只讀連接異常

正確插入交易訂單並提交數據

什麼也不做,因爲 readOnly 標誌被設置爲 true

正確的答案是 B。交易訂單會被準確無誤地插入數據庫中。請注意,上一示例表明,在使用 REQUIRED 傳播模式時,會拋出一個只讀連接異常。使用 JDBC 時是這樣。使用基於 ORM 的框架時,只讀標誌只是對數據庫的一個提示,並且一條基於 ORM 框架的指令(本例中是 Hibernate)將對象緩存的 flush 模式設置爲 NEVER,表示在這個工作單元中,該對象緩存不應與數據庫同步。不過,REQUIRED 傳播模式會覆蓋所有這些內容,允許事務啓動並工作,就好像沒有設置只讀標誌一樣。


這令我想到了另一個我經常碰到的主要陷阱。閱讀了前面的所有內容後,您認爲如果只對 @Transactional 註釋設置只讀標誌,清單 8 中的代碼會得到什麼結果呢?

 

清單 8. 使用只讀標誌 — 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() 方法會執行以下哪一種操作?


啓動一個事務,獲取交易訂單,然後提交事務

獲取交易訂單,但不啓動事務

正確的答案是 A。一個事務會被啓動並提交。不要忘了,@Transactional 註釋的默認傳播模式是 REQUIRED。這意味着事務會在不必要的情況下啓動。根據使用的數據庫,這會引起不必要的共享鎖,可能會使數據庫中出現死鎖的情況。此外,啓動和停止 事務將消耗不必要的處理時間和資源。總的來說,在使用基於 ORM 的框架時,只讀標誌基本上毫無用處,在大多數情況下會被忽略。但如果您堅持使用它,請記得將傳播模式設置爲 SUPPORTS(如清單 9 所示),這樣就不會啓動事務:

清單 9. 使用只讀標誌和 SUPPORTS 傳播模式進行選擇操作


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);

}


另外,在執行讀取操作時,避免使用 @Transactional 註釋,如清單 10 所示:


清單 10. 刪除 @Transactional 註釋進行選擇操作


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 事務屬性陷阱


不管是使用 Spring Framework,還是使用 EJB,使用 REQUIRES_NEW 事務屬性都會得到不好的結果並導致數據損壞和不一致。REQUIRES_NEW 事務屬性總是會在啓動方法時啓動一個新的事務。許多開發人員都錯誤地使用 REQUIRES_NEW 屬性,認爲它是確保事務啓動的正確方法。


 


 


 


 


 


Spring Framework @Transactional 註釋陷阱-5


 


 


清單 11. 使用 REQUIRES_NEW 事務屬性


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 中的兩個方法都是公共方法,這意味着它們可以單獨調用。當使用 REQUIRES_NEW 屬性的幾個方法通過服務間通信或編排在同一邏輯工作單元內調用時,該屬性就會出現問題。例如,假設在清單 11 中,您可以獨立於一些用例中的任何其他方法來調用 updateAcct() 方法,但也有在 insertTrade() 方法中調用 updateAcct() 方法的情況。現在如果調用 updateAcct() 方法後拋出異常,交易訂單就會回滾,但帳戶更新將會提交給數據庫,如清單 12 所示:

 

清單 12. 使用 REQUIRES_NEW 事務屬性的多次更新


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!

   ...

}


之所以會發生這種情況是因爲 updateAcct() 方法中啓動了一個新事務,所以在 updateAcct() 方法結束後,事務將被提交。使用 REQUIRES_NEW 事務屬性時,如果存在現有事務上下文,當前的事務會被掛起並啓動一個新事務。方法結束後,新的事務被提交,原來的事務繼續執行。


由於這種行爲,只有在被調用方法中的數據庫操作需要保存到數據庫中,而不管覆蓋事務的結果如何時,才應該使用 REQUIRES_NEW 事務屬性。比如,假設嘗試的所有股票交易都必須被記錄在一個審計數據庫中。出於驗證錯誤、資金不足或其他原因,不管交易是否失敗,這條信息都需要被持久 化。如果沒有對審計方法使用 REQUIRES_NEW 屬性,審計記錄就會連同嘗試執行的交易一起回滾。使用 REQUIRES_NEW 屬性可以確保不管初始事務的結果如何,審計數據都會被保存。這裏要注意的一點是,要始終使用 MANDATORY 或 REQUIRED 屬性,而不是 REQUIRES_NEW,除非您有足夠的理由來使用它,類似審計示例中的那些理由。


事務回滾陷阱


我將最常見的事務陷阱留到最後來講。遺憾的是,我在生產代碼中多次遇到這個錯誤。我首先從 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;

   }

}

假設帳戶中沒有足夠的資金來購買需要的股票,或者還沒有準備購買或出售股票,並拋出了一個受控異常(例如 FundsNotAvailableException),那麼交易訂單會保存在數據庫中嗎?還是整個邏輯工作單元將執行回滾?答案出乎意料:根據受控異 常(不管是在 Spring Framework 中還是在 EJB 中),事務會提交它還未提交的所有工作。使用清單 13,這意味着,如果在執行 updateAcct() 方法期間拋出受控異常,就會保存交易訂單,但不會更新帳戶來反映交易情況。


這可能是在使用事務時出現的主要數據完整性和一致性問題了。運行時異常(即非受控異常)自動強制執行整個邏輯工作單元的回滾,但受控異常不會。因此,清單 13 中的代碼從事務角度來說毫無用處;儘管看上去它使用事務來維護原子性和一致性,但事實上並沒有。


儘管這種行爲看起來很奇怪,但這樣做自有它的道理。首先,不是所有受控異常都是不好的;它們可用於事件通知或根據某些條件重定向處理。但更重要的 是,應用程序代碼會對某些類型的受控異常採取糾正操作,從而使事務全部完成。例如,考慮下面一種場景:您正在爲在線書籍零售商編寫代碼。要完成圖書的訂 單,您需要將電子郵件形式的確認函作爲訂單處理的一部分發送。如果電子郵件服務器關閉,您將發送某種形式的 SMTP 受控異常,表示郵件無法發送。如果受控異常引起自動回滾,整個圖書訂單就會由於電子郵件服務器的關閉全部回滾。通過禁止自動回滾受控異常,您可以捕獲該異 常並執行某種糾正操作(如向掛起隊列發送消息),然後提交剩餘的訂單。


 


 


 


 


 


Spring Framework @Transactional 註釋陷阱-6


 


使用 Declarative 事務模式時,必須指定容器或框架應該如何處理受控異常。在 Spring Framework 中,通過 @Transactional 註釋中的 rollbackFor 參數進行指定,如清單 14 所示:

 

清單 14. 添加事務回滾支持 — 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 參數。這個參數接受一個單一異常類或一組異常類,您也可以使用 rollbackForClassName 參數將異常的名稱指定爲 Java String 類型。還可以使用此屬性的相反形式(noRollbackFor)指定除某些異常以外的所有異常應該強制回滾。通常大多數開發人員指定 Exception.class 作爲值,表示該方法中的所有異常應該強制回滾。


在回滾事務這一點上,EJB 的工作方式與 Spring Framework 稍微有點不同。EJB 3.0 規範中的 @TransactionAttribute 註釋不包含指定回滾行爲的指令。必須使用 SessionContext.setRollbackOnly() 方法將事務標記爲執行回滾,如清單 15 所示:

 

清單 15. 添加事務回滾支持 — 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;

   }

}

調用 setRollbackOnly() 方法後,就不能改變主意了;惟一可能的結果是在啓動事務的方法完成後回滾事務。本系列後續文章中描述的事務策略將介紹何時、何處使用回滾指令,以及何時使用 REQUIRED 與 MANDATORY 事務屬性。


 


 


 


 


Isolation Level(事務隔離等級)


 


1、Serializable:最嚴格的級別,事務串行執行,資源消耗最大;

2、REPEATABLE READ:保證了一個事務不會修改已經由另一個事務讀取但未提交(回滾)的數據。避免了“髒讀取”和“不可重複讀取”的情況,但是帶來了更多的性能損失。

3、READ COMMITTED:大多數主流數據庫的默認事務等級,保證了一個事務不會讀到另一個並行事務已修改但未提交的數據,避免了“髒讀取”。該級別適用於大多數系統。

4、Read Uncommitted:保證了讀取過程中不會讀取到非法數據。隔離級別在於處理多事務的併發問題。

我們知道並行可以提高數據庫的吞吐量和效率,但是並不是所有的併發事務都可以併發運行。

我們首先說併發中可能發生的3中不討人喜歡的事情

1: Dirty reads--讀髒數據。也就是說,比如事務A的未提交(還依然緩存)的數據被事務B讀走,如果事務A失敗回滾,會導致事務B所讀取的的數據是錯誤的。

2: non-repeatable reads--數據不可重複讀。比如事務A中兩處讀取數據-total-的值。在第一讀的時候,total是100,然後事務B就把total的數據改成 200,事務A再讀一次,結果就發現,total竟然就變成200了,造成事務A數據混亂。

3: phantom reads--幻象讀數據,這個和non-repeatable reads相似,也是同一個事務中多次讀不一致的問題。但是non-repeatable reads的不一致是因爲他所要取的數據集被改變了(比如total的數據),但是phantom reads所要讀的數據的不一致卻不是他所要讀的數據集改變,而是他的條件數據集改變。比如Select account.id where account.name="ppgogo*",第一次讀去了6個符合條件的id,第二次讀取的時候,由於事務b把一個帳號的名字由"dd"改 成"ppgogo1",結果取出來了7個數據。


  Dirty reads non-repeatable reads phantom reads

Serializable 不會 不會 不會

REPEATABLE READ 不會 不會 會

READ COMMITTED 不會 會 會

Read Uncommitted 會 會 會

readOnly

事務屬性中的readOnly標誌表示對應的事務應該被最優化爲只讀事務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章