17 June 2006

這一本書是新的 Server Side 網站 infoQ 所出的 短篇精華書藉。短短 100 頁教你 Enterprise Transaction 的概念和 best practice。這一系列的書很適合時間不多的 IT 族 研讀。下面是筆記 :)

  • Transaction 管理分為三種
    • 一是 Local Transaction Model - 依賴最底層的 JDBC/RDBMS or JMS provider 來管理。其實這種做法僅能算是管理 connection,不算是管理 transaction。
    • 二是 Programmatic Transaction Model - 靠程式呼叫 begin(), commit(), rollback() 控制 transaction。
    • 三是 Declarative Transaction Model - 由 container 控管 transaction,透過 configuration 來變更 transaction 的設定。
  • Isolation 是 consistency 和 concurrency 的函數。isolation 越高,consistency 則越高,但是 concurrency 卻下降。
  • 在 Local Tx Model 裡,connection 多半是設為 autoCommit,即每執行一段 SQL 就 commit。這通常會有很多問題,盡量加上 connection.setAutoCommit(false); 再搭配 connection.commit(); 較好。
  • Local Tx Model 只適用單一資料庫並且相當簡單的應用環境裡。
  • 取得 JTA 的 transaction: ctx.lookup("java:comp/UserTransaction") ,但是 JNDI 每個 app server 卻都不同。
  • 相較於 JNDI 的做法,可以改用 TransactionManagerFactory 搭配 reflection 取得 TransactionManager 來達到 Programmatic tx model。這種做法在 Unit Test 環境下也通。
  • Programmatic Tx Model 最常使用在 "Client-initiated transactions" - 即 client 需要多個 remote calls 來完成單一 business 邏輯時,我們必需將 transaction 交到 client 端來啟動。
  • 當需要 performance 高度最佳化的情況下,我們可關閉次要的操作上的 transaction (例如 query/validation),而只針對最重要的部份 (例如 credit card 轉帳) 使用 transaction。這種技巧稱 Localized JTA transactions。而這只有在 Programmatic Tx Model 才能辦到。
  • 七個 Declarative Transaction attributes: Required/Mandatory/RequiresNew/Supports/NotSupported/Never and PROPAGATION_NESTED (spring only)。nested 的只有在底層 transaction 服務有提供時才能支援。
  • Required 是最常用的 attribute。
  • RequiresNew 會另外獨立開一個 transaction。這在 auditing log 的時候特別有用,因為 audit 通常不論成敗都要記錄,當失敗時,主要的 transaction rollback 了,但 auditService 本身則需要自己的新 transaction 將 log 寫入。
  • Supported 會加入現有的 transaction,但不會自已開 transaction。這個對最佳化上很有幫助,例如通常 query 是用不到 transaction 的,所以我們不需要設為 Required,很適合設為 supported,讓一般的查詢不用開 transaction。但有時候會遇到前面的 method 已經開啟了 transaction,並且開始對資料庫做了修改。這時這個 query 因為設為 supported,所以可以加入上一個 transaction,因此 query 會得到 "剛被修改中的資料"。如此,得到的資料較為完整。
  • 反之,如果設為 NotSupported,則會停止上一個 transaction。這通常在要另外操作 DDL 或 store procedured 上時,卻不願意影響原本的 transaction 時採用。
  • EJB 不能完全採用所有的 attribute。各個 bean 有各自的限制
  • Declarative Tx Model 在預設狀況下,遇到 Application Exception 是不會 rollback 的,這要額外設定才行。
  • Best Practice(1): Transaction 管理一定要在開啟 transaction 的那個 method 裡進行。因此,如果要呼叫 setRollbackOnly(),應該要在開啟 transaction 的 method 裡呼叫。
  • Rollback rule 不該設定在某個 Exception 的 class 上,因為裏層的 method 如果 throw 該 Exception,則整個 transaction 便強制變成 rollback only,上一層便無法 "回復 transaciton " 的狀態了。這個狀況也是違反了 Best Practice(1)。
  • Best Practice(2): 如果 method 需要 transaction,但是它不能影響整個 transaction 的 rollback,這個 method 應該要設為 MANDATORY,而不是 Required。
  • Transaction Isolation Level 依不同的 Application Server, Database,實際的設定和效果皆不同。因此這並不是一個可以跨平台的設定。Spring/EJB 都支援四種的 level,但是如果底層的 database 不支援的話,db 會 "直接跳用 default 的 level (通常是 ReadCommitted)",而且不會丟出任何 exception! 所以要設之前還是先得查查 db 的手冊,看看這個版本支援哪些 level。
  • PostgreSQL 和 oracle 都只支援 ReadCommitted 和 Serializable 這兩種 isolation level。
  • Isolation: RepeatableRead 的意思是在 A transaction 裡,無論讀幾次相同的資料,結果都會保持一致 (即使 B transacton 已經 commit 了它的修改)。通常DB 內部的實作是讓 B transacton 等待 A transaction 結束後才進行真的修改。因此效率很差。
  • Isolation: Serializable 的意思是將全部的 transaction 排成一個序列,依序執行 (所以才叫序列化)。這是最高的 isolation 了,因為 transaction 間完全不會互相干擾。一般 DB 的實作是讓排在後面的 transaction 暫停 (suspend),輪到了才繼續。可惜的是該死的 oracle 不是用暫停,而是當其他 transaction 想介入時,oracle 直接 throw ORA-08177 error,所以寫程式的又要苦哈哈的替 oracle 做客製... 還好這個 level 用的機率很低,不然 oracle 臭名再加一條!
  • XA transaction processing 是一個 JTA transaction manager 控管底層數個 "resource managers",resource manager 可能是 jdbc or jms... etc。transaction manager 和 resource manager 之間的互動是雙向的,所以可以做到 two-phase commit。
  • Best Practice(3):當你需要在 同一個 transaction 環境下操作多個 resource 時,才有必要導入 XA。不然請盡量避免。換句話說,即使你會動用到多個 resource,但沒有要一起 commit 的需求,那就用不到 XA,這樣可以減少很多問題。
  • Two Phase Commit: 儀式開始 (begin XA transaction),主婚人問男方是否願意,男方回答: "I do",主婚人轉問女方是否也願意,女方回答: "I do"。喔喔~~ 主婚人終於可以宣告兩方正式結為夫婦,完成儀式 (XA transaction commit)。男女兩方終於永遠在一起了 (persistented -- 關係被持久化了,被套牢永不翻身 -_-)。如果有一方拒絕的話,婚禮就只能黯然的結束了 (rollback)... 兩方都回復成原來的習性,男的變回好人,女的則繼續找下個馱獸。
  • Phase One 是 prepare-phase,transaction manager 會問 resource 是否 READY, READ_ONLY, NOT_READY。如果其中一個 resource 回答 NOT_READY 則 transaction rollback。如果全部 resource 皆回答 READY,那 Phase two (commit-phase) 可以進行 commit。回答 READ_ONLY 的則該 resource 不會進入 Phase Two,因為用不到。所以純 query 的 resource 效能在 XA 環境下和非 XA 環境下是一樣的。
  • 大部份的 app server 都支援 One-Phase-Commit Optimization,即如果只有一個 resource 在 transaction 中,則只用一個 phase 完成 transaction。
  • XA 環境下特有的 HeuristicExcetion - 該翻成 "自做主張例外" 吧?我們若用劍理來比喻,這個大破綻是因為第一招與第二招之間轉換不夠流暢所致。phase1 原先大家都說好 READY,但是由於系統面(網路/IO等等)問題,以致於 resource manager 等不到 phase2 開始,就自做主張先 rollback 或 commit 了 (所以才叫自做主張例外)。等到 phase2 開始後,transaction manager 發現怎麼有人出爾反爾,氣得大叫不幹了!丟出 HeuristicException 草草了事。如果是自做主張 rollback 就算了,整個重來就好,但如果遇到自作主張 commit,那就得查資料,手動一筆一筆修復。
  • 三種 Transaction Design Strategy
    • Client Owner Transaction Design Pattern
    • Domain Service Owner Transaction Design Pattern
    • Server Delegate Owner Transaction Design Pattern
  • 上述三種 pattern 依 architect 的不同而有不同的 transaction 設定,但最終目的都是為了達到 Best Practice(1) -- 讓開啟 transaction 的人負責 commit/rollback。
  • Client Owner Pattern 是... 呃... 最不該用的一種,這個 pattern 是為了解救將 business logic 寫到 client 端的爛程式碼而設計。如果 client 端 (可以想成是 struts 的 Action) 包含 logic,多半會呼叫多個後端的 service。這時 transaction 就不能設定在各個後端的 service 上,因為這樣會各自 commit 各自的,失去了 Unit of Work 的用意。因此 transaction 的控管你被迫只能寫在 client 中來解決。實作方法不想寫了,因為看起來很噁心... 與其套用這個 pattern,倒不如修改 architect 來的實在,不然到後來只會越補越大洞。
  • Domain Service Owner Pattern 是最佳 solution,不管你的 application 為何,都應當先用這個 pattern 為起點來設計。這個做法是讓 client 中只呼叫一個 domain service 一次。然後所有的 domain service 都設定為:
    <bean id="anyDomainService" class="TransactionProxyFacotryBean">
       <property name="transactionAttributes">
          <props>
             <prop key="*">PROPAGATION_REQUIRED, -Exception</prop>
             <prop key="get*">PROPAGATION_SUPPORTS</prop>
          </props>
       </property>
    </bean>
    因為所有的 method 都是 propagation_required,所以 service 間互相呼叫時 transaction 會延續下去,而 client 端所呼叫的最外層 servcie 則會負責開啟 transaction。上面的設計也替 query 的 method 做一些調整,改為 PROPAGATOIN_SUPPROTS,因為 query 通常不需要有 transaction,這可以節省一些資源。這個做法有一個問題是如果裏層的 service 丟出 exception,則整個 transaction 便會 rollback,無法回復。我在上一篇中有探討這個問題。
  • 第三種是 Service Delegate Owner Pattern,這個做法是讓底層的 domain service 完全不負責 transaction,交由一個最外層,一個特製的大 facade 來控管,而 client 端只用到這大 facade,而且一次 request 只限呼叫一次 method。這個 facade 可以設計成 command pattern 也可以設計成 service delegate。這個做法的好處是只需要在 "一個" 大 facade 上面設定 transaction 即可:
    <bean id="commandProcessor" class="TransactionProxyFacotryBean">
       <property name="transactionAttributes">
          <props>
             <prop key="executeUpadte">PROPAGATION_REQUIRED, -Exception</prop>
             <prop key="executeRead">PROPAGATION_SUPPORTS</prop>
          </props>
       </property>
    </bean>
    上面的大 facade 套用 command pattern 來實作,非常簡單。這個作法的缺點是你需要替所有的 request 動作額外設計一個 command class,比起 domain service owner pattern 架構上又多了一層。

這本書真是讚啊,釐清了我很多觀念,這個系列的下一本是 Domain Driven Design Quickly。 WOW! 真是另人期待啊 XD