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

掘金翻譯計劃發表於2019-04-11

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

效能和 JDBCPersistenceAdapter

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

非訊息方案(多資料庫)的另一種共享資源的技術是使用 Oracle 資料的連結功能在 RDBMS 平臺將兩個資料庫模式連結在一起(請參閱資料)。這可能需要修改應用程式的程式碼,或者建立同義詞,因為引用連結資料庫的表名的別名包含了連結的名稱。

最大努力單階段提交模式

最大努力單階段提交模式是相當普遍的,但在開發人員必須注意的某些情況下可能會失敗。這是一種非 XA 模式,涉及了許多資源的同步單階段提交。因為沒有使用二階段提交,它絕不會像 XA 事務那樣安全,但是如果參與者意識到妥協,通常就足夠了。許多高容量,高吞吐量的事務處理系統通過設定這種方式以達到提高效能的目的。

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

為了幫助你更好地理解模式並分析失敗的後果,我將使用訊息驅動的資料庫更新作為示例。

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

  1. 開啟訊息事務
  2. 接受訊息
  3. 開始資料庫事務
  4. 更新資料庫
  5. 提交資料庫事務
  6. 提交訊息事務

實際上,前四個步驟的順序並不關鍵,除了必須在更新資料庫之前接收訊息,並且每個事務必須在使用其相應資源之前開始。所以這個序列同樣有效:

  1. 開啟訊息事務
  2. 開始資料庫事務
  3. 接受訊息
  4. 更新資料庫
  5. 提交資料庫事務
  6. 提交訊息事務

關鍵在於最後兩個步驟很重要:它們必須按此順序排在最後。順序很重要的原因是因為技術性,但是業務需求也決定了順序本事。這個順序告訴你在這種情況下的事務資源是特殊的。它包含了關於如何去執行另一項工作的說明。這是一個業務排序:系統無法自動的判斷如何排序(儘管如果訊息和資料是兩個資源,那麼它通常按照如此順序)。排序很重要的原因是因為它和失敗情況相關。最常見的故障情況(到目前為止)是業務處理失敗(錯誤資料,程式設計錯誤等)。在這種情況下,可以輕鬆地操縱這兩個事務以響應異常和回滾。在這種情況下,業務資料的完整性得以保留,時間線類似於本文開頭概述的理想故障情況。

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

請注意,如果資料庫資源的提交失敗,則淨效果仍然是回滾。因此,唯一的非原子失敗模式是第一個事務提交而第二個事務回滾。更普遍的情況下,如果事務中存在 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章