[譯] Spring 的分佈式事務實現 — 使用和不使用 XA — 第二部分

一個共享的數據庫資源有時能夠從現有的單獨資源中被合成,特別是若是它們都在相同的 RDBMS 平臺上。企業級別的數據庫供應商都支持同義詞(或等價物)的概念,其中一個模式(Oracle 術語)中的表在另外一個模式內被定義爲同義詞。這樣的話,在平臺中的物理數據能夠被 JDBC 客戶端中的相同的 Connection 進行事務處理。例如,在真實系統中(做爲對照)在 ActiveMQ 中實現共享事務資源模式,將會常常爲涉及消息傳遞和業務數據建立同義詞。html

性能和 JDBCPersistenceAdapter

在 ActiveMQ 社區中的某些人聲稱 JDBCPersistenceAdapter 會形成性能問題。然而,許多項目和實時系統將 ActiveMQ 和關係型數據庫一同使用。在這些狀況下,收到的明智的建議是使用日誌版本用於提升性能。這不適用於共享事務資源模式(由於日誌本事是一個新的事務資源)。儘管如此,陪審團仍然在關注 JDBCPersistenceAdapter。而且事實上有理由認爲共享事務資源可能會提升。性能在日誌方面。這是 Spring 和 ActiveMQ 工程團隊之間積極研究的領域。前端

非消息方案(多數據庫)的另外一種共享資源的技術是使用 Oracle 數據的連接功能在 RDBMS 平臺將兩個數據庫模式連接在一塊兒(請參閱資料)。這可能須要修改應用程序的代碼,或者建立同義詞,由於引用連接數據庫的表名的別名包含了連接的名稱。java

最大努力單階段提交模式

最大努力單階段提交模式是至關廣泛的,但在開發人員必須注意的某些狀況下可能會失敗。這是一種非 XA 模式,涉及了許多資源的同步單階段提交。由於沒有使用二階段提交,它毫不會像 XA 事務那樣安全,可是若是參與者意識到妥協,一般就足夠了。許多高容量,高吞吐量的事務處理系統經過設置這種方式以達到提升性能的目的。android

基本思想是在事務中儘量晚地延遲全部資源的提交,以便惟一可能出錯的是基礎設施故障(而不是業務處理錯誤)。系統依賴於最大努力單階段提交模式的緣由是基礎設施故障很是罕見,以致於他們可以承擔風險以換取更高的吞吐量。若是業務處理服務也被設計成冪等,那麼在實戰中幾乎不可能出現錯誤。ios

爲了幫助你更好地理解模式並分析失敗的後果,我將使用消息驅動的數據庫更新做爲示例。git

此事務中的兩個資源計入並計算在內。消息事務在數據庫以前啓動,並以相反的順序結束(提交或回滾)。所以,成功案例中的順序可能與本文開頭的順序相同:github

  1. 開啓消息事務
  2. 接受消息
  3. 開始數據庫事務
  4. 更新數據庫
  5. 提交數據庫事務
  6. 提交消息事務

實際上,前四個步驟的順序並不關鍵,除了必須在更新數據庫以前接收消息,而且每一個事務必須在使用其相應資源以前開始。因此這個序列一樣有效:spring

  1. 開啓消息事務
  2. 開始數據庫事務
  3. 接受消息
  4. 更新數據庫
  5. 提交數據庫事務
  6. 提交消息事務

關鍵在於最後兩個步驟很重要:它們必須按此順序排在最後。順序很重要的緣由是由於技術性,可是業務需求也決定了順序本事。這個順序告訴你在這種狀況下的事務資源是特殊的。它包含了關於如何去執行另外一項工做的說明。這是一個業務排序:系統沒法自動的判斷如何排序(儘管若是消息和數據是兩個資源,那麼它一般按照如此順序)。排序很重要的緣由是由於它和失敗狀況相關。最多見的故障狀況(到目前爲止)是業務處理失敗(錯誤數據,編程錯誤等)。在這種狀況下,能夠輕鬆地操縱這兩個事務以響應異常和回滾。在這種狀況下,業務數據的完整性得以保留,時間線相似於本文開頭概述的理想故障狀況。數據庫

觸發回滾的確切機制並不重要,有幾個可用。重要的是,提交或回滾的發生方式與資源中業務排序的順序相反。在示例應用程序中,消息傳遞事務必須最後提交,由於業務流程的指令被包含在該資源中。這很重要,由於會發生第一次提交成功而且第二次提交失敗的(罕見)故障狀況。由於經過設計,此時全部業務處理已經完成,因此這種部分故障的惟一緣由將是消息傳遞中間件的基礎設施問題。apache

請注意,若是數據庫資源的提交失敗,則淨效果仍然是回滾。所以,惟一的非原子失敗模式是第一個事務提交而第二個事務回滾。更廣泛的狀況下,若是事務中存在 n 個資源,存在 n-1 這樣的失敗模式,在回滾以後會使資源存在不一致(已提交)狀態。在消息數據庫的用例中,此失敗模式的結果是消息被回滾並返回到另外一個事務中,即便它已經成功處理。所以,您能夠推測到可能發生的更糟糕的事情是能夠傳遞重複的消息。在更廣泛的狀況下,由於事務中較早的資源被認爲可能攜帶有關如何對後來的資源進行處理的信息,因此失敗模式的最終結果一般能夠稱爲消息重複

有些人承擔了重複消息不常常發生的風險,以致於他們不會費心去預測它們。可是,爲了對業務數據的正確性和一致性更有信心,您須要在業務邏輯中瞭解它們。若是你在業務處理中意識到重複的消息可能會發生,那麼全部必須作的事情(一般須要一些額外的成本,但不如 2PC 那麼多)是檢查它是否已經處理過該數據,若是有,則不執行任何操做。此專業化有時稱爲冪等業務服務模式。

示例代碼包括使用此模式同步事務資源的兩個示例。我將依次討論每個,而後測試一些其餘選項。

Spring 和消息驅動的 POJO

示例代碼best-jms-db project, 參與者使用主流配置選項進行設置,以便遵循最大努力單階段提交模式。這個想法是發送到隊列的消息由異步監聽器收集並用於將數據插入數據庫的表中。

這個 TransactionAwareConnectionFactoryProxy — Spring 中的一個組件,旨在用於這種模式 — 是關鍵因素。使用配置將 ConnectionFactory 包裝在處理事務同步的裝飾器中,而不是使用原始供應商提供的 ConnectionFactory。這發生在 jms-context.xml, 如示例 6 所示:

示例 6. 配置一個TransactionAwareConnectionFactoryProxy 來包裝供應商提供的 ConnectionFactory

<bean id="connectionFactory" class="org.springframework.jms.connection.TransactionAwareConnectionFactoryProxy">
  <property>
    <bean class="org.apache.activemq.ActiveMQConnectionFactory" depends-on="brokerService">
      <property/>
    </bean>
  </property>
  <property/>
</bean>
複製代碼

ConnectionFactory 不須要知道要與哪一個事務管理器同步,由於在須要時只有一個事務處於活動狀態,而 Spring 能夠在內部處理它。驅動事務由 data-source-context.xml 中配置的普通 DataSourceTransactionManager 處理。須要瞭解的是事務管理器的組件是將輪詢和接收消息的JMS監聽器容器:

<jms:listener-container transaction-manager="transactionManager">
  <jms:listener destination="async" ref="fooHandler" method="handle"/>
</jms:listener-container>
複製代碼

fooHandlermethod 告訴監聽器容器當消息到達 async 隊列時,哪一個組件要調用哪一個方法。處理程序是這樣實現的,接受一個 String 做爲傳入消息,並使用它來插入記錄:

public void handle(String msg) {

  jdbcTemplate.update(
      "INSERT INTO T_FOOS (ID, name, foo_date) values (?, ?,?)", count.getAndIncrement(), msg, new Date());

}
複製代碼

爲了模擬失敗的狀況,代碼使用了 FailureSimulator 切面。它檢查消息內容以查看它是否應該失敗,以及以何種方式。示例 7 中所示的 maybeFail() 方法在 FooHandler 處理消息以後調用,但在事務結束以前調用,以便它能夠影響事務的結果:

示例 7. maybeFail() 方法

@AfterReturning("execution(* *..*Handler+.handle(String)) && args(msg)")
public void maybeFail(String msg) {
  if (msg.contains("fail")) {
    if (msg.contains("partial")) {
      simulateMessageSystemFailure();
    } else {
      simulateBusinessProcessingFailure();
    }
  }    
}
複製代碼

simulateBusinessProcessingFailure() 方法只拋出一個 DataAccessException,好像數據庫訪問失敗同樣。當觸發此方法時,您指望徹底回滾全部數據庫和消息事務。此方案在示例項目的 AsynchronousMessageTriggerAndRollbackTests 單元測試中進行了測試。

simulateMessageSystemFailure() 方法經過削弱底層 JMS Session 來模擬消息傳遞系統中的失敗。這裏的預期結果是部分提交:數據庫工做保持提交但消息回滾。這是在 AsynchronousMessageTriggerAndPartialRollbackTests 單元測試中測試的。

示例包還包括在 AsynchronousMessageTriggerSunnyDayTests 類中成功提交全部事務工做的單元測試。

相同的JMS配置和相同的業務邏輯也能夠在同步設置中使用,其中消息在業務邏輯內的阻塞調用中接收,而不是委託給偵聽器容器。這種方法也在 best-jms-db 示例項目中獲得了證實。sunny-day 案例和完整回滾分別在 SynchronousMessageTriggerSunnyDayTestsSynchronousMessageTriggerAndRollbackTests 中進行測試。

連接事務管理器

在最大努力單階段提交模式的另外一個示例(best-db-db 項目)中,事務管理器的粗略實現只是將其餘事務管理器的列表連接在一塊兒以實現事務同步。若是業務處理成功,他們都會提交,若是不是,他們都會回滾。

實如今 ChainedTransactionManager 中,它接受其餘事務管理器的列表做爲注入屬性,如示例 8 所示:

示例 8. ChainedTransactionManager 的配置

<bean id="transactionManager" class="com.springsource.open.db.ChainedTransactionManager">
  <property>
    <list>
      <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property/>
      </bean>
      <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property/>
      </bean>
    </list>
  </property>
</bean>
複製代碼

對此配置最簡單的測試就是在兩個數據庫中插入內容,回滾並檢查兩個操做是否都沒有留下痕跡。這是做爲 MulipleDataSourceTests 中的單元測試實現的,與 XA 示例的 atomikos-db 項目中的相同。若是回滾未同步但提交失敗,則測試失敗。

請記住,資源的順序很重要。它們是嵌套的,而且提交或回滾的順序與它們被登記的順序相反(這是配置中的順序)。這使得其中一個資源變得特殊:若是出現問題,最外層資源總會回滾,即便惟一的問題是該資源的故障。此外,testInsertWithCheckForDuplicates() 測試方法顯示了一個冪等的業務流程,能夠保護系統免受部分故障的影響。它被實現爲對內部資源(在這種狀況下爲 otherDataSource)的業務操做的防護性檢查:

int count = otherJdbcTemplate.update("UPDATE T_AUDITS ... WHERE id=, ...?");
if (count == 0) {
  count = otherJdbcTemplate.update("INSERT into T_AUDITS ...", ...);
}
複製代碼

首先使用 where 子句嘗試更新。若是沒有任何反應,則插入您但願在更新中找到的數據。在這種狀況下,對冪等過程的額外保護的成本是在 sunny-day 案例中的一個額外查詢(更新)。在更復雜的業務流程中,此成本將很是低,其中每一個事務執行許多查詢。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索