西沙璞玉
          愛不容易
          posts - 0,comments - 4,trackbacks - 0

          在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代碼

          1. public void processT(String orders) {  
          2. Context initCtx = new InitialContext();  
          3. javax.sql.DataSource ds = javax.sql.DataSource)initCtx.lookup  
          4. (“java:comp/env/jdbc/OrdersDB”);  
          5. java.sql.Connection conn = ds.getConnection();  
          6. try {  
          7. conn.setAutoCommit( false ); //更改JDBC事務(wù)的默認(rèn)提交方式  
          8. orderNo = createOrder( orders );  
          9. updateOrderStatus(orderNo, “orders created”);  
          10. conn.commit(); //提交JDBC事務(wù)  
          11. } catch ( Exception e ){  
          12. try {  
          13. conn.rollback(); //回滾sJDBC事務(wù)  
          14. throw new EJBException(“事務(wù)回滾: “ + e.getMessage());  
          15. } catch ( SQLException sqle ){  
          16. throw new EJBException(“出現(xiàn)SQL操作錯(cuò)誤: “ + sqle.getMessage());  
          17. }  
          18. }  

           

          下面給出了JTA事務(wù)代碼示例:

          Java代碼
          1. public void processOrder(String orderMessage) {  
          2. UserTransaction transaction = mySessionContext.getUserTransaction(); //獲得JTA事務(wù)  
          3. try {  
          4. transaction.begin(); //開始JTA事務(wù)  
          5. orderNo = sendOrder(orderMessage);  
          6. updateOrderStatus(orderNo, “order sent”);  
          7. transaction.commit(); //提交JTA事務(wù)  
          8. } catch (Exception e){  
          9. try {  
          10. transaction.rollback(); //回滾JTA事務(wù)  
          11. } catch (SystemException se){  
          12. se.printStackTrace();  
          13. }  
          14. throw new EJBException(“事務(wù)回滾: “ + e.getMessage());  
          15. }  

           

           

          在整個(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 readsnon-repeatable readsphantom 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ù)。

           






           


          posted on 2012-04-12 10:10 @趙 閱讀(130) 評(píng)論(0)  編輯  收藏

          只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          哥哥最近不是很忙
          主站蜘蛛池模板: 郑州市| 东山县| 织金县| 仙居县| 资中县| 鄂伦春自治旗| 盐池县| 贵溪市| 云林县| 宣汉县| 南投县| 达拉特旗| 田阳县| 台南市| 奈曼旗| 微山县| 北京市| 彰化县| 内丘县| 平定县| 化隆| 屯留县| 开鲁县| 山丹县| 盈江县| 丰顺县| 章丘市| 定远县| 汽车| 渭南市| 桂阳县| 临泉县| 富民县| 平湖市| 西贡区| 建湖县| 驻马店市| 石河子市| 梁山县| 潢川县| 伊金霍洛旗|