天天看點

spring(基礎12) Spring注解@Transactional詳解

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

在整個方法運作前就不會開啟事務還可以寫成:

@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
           

這樣就做成一個隻讀事務,可以提高效率。

各種屬性的意義:

//事務傳播屬性

@Transactional(propagation=Propagation.REQUIRED)//如果有事務,那麼加入事務,沒有的話新建立一個

@Transactional(propagation=Propagation.NOT_SUPPORTED)//這個方法不開啟事務

@Transactional(propagation=Propagation.REQUIREDS_NEW)//不管是否存在事務,都建立一個新的事務,原來的挂起,新的執行完畢,繼續執行老的事務

@Transactional(propagation=Propagation.MANDATORY)//必須在一個已有的事務中執行,否則抛出異常

@Transactional(propagation=Propagation.NEVER)//不能在一個事務中執行,就是目前必須沒有事務,否則抛出異常

@Transactional(propagation=Propagation.SUPPORTS)//其他bean調用這個方法,如果在其他bean中聲明了事務,就是用事務。沒有聲明,就不用事務。

@Transactional(propagation=Propagation.NESTED)//如果一個活動的事務存在,則運作在一個嵌套的事務中,如果沒有活動的事務,則按照REQUIRED屬性執行,它使用一個單獨的事務。這個書屋擁有多個復原的儲存點,内部事務的復原不會對外部事務造成影響,它隻對DataSource TransactionManager事務管理器起效。

@Transactional(propagation=Propagation.REQUIRED,readOnly=true)//隻讀,不能更新,删除

@Transactional(propagation=Propagation.REQUIRED,timeout=30)//逾時30秒

@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT)//資料庫隔離級别

事務傳播行為種類

Spring在TransactionDefinition接口中規定了7種類型的事務傳播行為,

它們規定了事務方法和事務方法發生嵌套調用時事務如何進行傳播:

說明

PROPAGATION_REQUIRED

如果目前沒有事務,就建立一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。

PROPAGATION_SUPPORTS

支援目前事務,如果目前沒有事務,就以非事務方式執行。

PROPAGATION_MANDATORY

使用目前的事務,如果目前沒有事務,就抛出異常。

PROPAGATION_REQUIRES_NEW

建立事務,如果目前存在事務,把目前事務挂起。

PROPAGATION_NOT_SUPPORTED

以非事務方式執行操作,如果目前存在事務,就把目前事務挂起。

PROPAGATION_NEVER

以非事務方式執行,如果目前存在事務,則抛出異常。

PROPAGATION_NESTED

如果目前存在事務,則在嵌套事務内執行。如果目前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

事務陷阱-1

清單 1. 使用 JDBC 的簡單資料庫插入

@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. 在同一方法中執行多次表更新

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 的簡單插入

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 注釋

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 配置檔案中添加以下代碼行:

<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 傳播

@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

@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

@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

@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 傳播模式進行選擇操作 

@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)   
public TradeData getTrade(long tradeId) throws Exception {   
   return em.find(TradeData.class, tradeId);   
} 
           

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

清單 10. 删除 @Transactional 注釋進行選擇操作

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

@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 事務屬性的多次更新

@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. 沒有復原支援 

@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

@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 

@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标志表示對應的事務應該被最優化為隻讀事務。

繼續閱讀