- 原文地址:Distributed transactions in Spring, with and without XA - Part I
- 原文作者:David Syer
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:JackEggie
- 校對者:fireairforce
Spring 的 7 種事務處理模式
雖然在 Spring 中分散式事務通常使用 Java Transaction API 和 XA 協議實現,但也有其他的實現方式。最好的實現方式取決於應用程式所使用資源的型別,以及你是否願意在效能、安全性、可靠性和資料完整性之間做出權衡。針對這個 Java 中的典型問題,Spring 的開發者 David Syer 將會介紹 7 種 Spring 分散式應用的實現方式,其中 3 種實現使用了 XA 協議,另外 4 種使用了其他的實現方式。(中級知識點)
Spring 框架對 Java Transaction API (JTA) 的支援使應用程式能夠無需在 Java EE 容器中即可使用分散式事務和 XA 協議。然而,即使有了這種支援,XA 的效能開銷仍然很大,而且可能不可靠並且難於管理。不過令人驚喜的是,某種特定型別的應用程式可以完全避免使用 XA 來實現分散式事務。
為了讓你對分散式事務的各種實現方式有充分的理解和思考,我將詳細分析這 7 種事務處理模式,並提供程式碼示例幫助你理解得更具體。我將根據安全性和可靠性來依次介紹這些模式,從通常來說資料完整性和原子性程度最高的模式開始。當你按順序瀏覽時,你會看到越來越多的警示說明和限制條件。這些模式的效能開銷也大致相反(從開銷最大的模式開始)。與編寫業務程式碼完全不同的是,這些模式都是從架構複雜度和技術難度考慮的,所以我不會關心業務用例,只關心使每種模式正常工作的最小程式碼量。
注意,只有前三種模式涉及 XA。而從效能的角度考慮,這些模式可能無法使用或效能差到不可接受。我不會像介紹其他模式那樣對 XA 模式有詳細的討論,因為 XA 在其他地方已經有很多介紹了,不過我提供了第一個模式(基於 XA)的簡單示例。通過閱讀本文,你將瞭解使用分散式事務可以做什麼、不能做什麼,何時使用 XA、何時不使用 XA,以及如何避免使用 XA。
分散式事務及其原子性
一個分散式事務通常包含多個事務資源。事務資源是指關係型資料庫和訊息中介軟體的連線。一個典型的事務資源都會有像 begin()
、rollback()
、commit()
這樣的 API。在 Java 中,一個事務資源通常表現為底層連線工廠提供的例項:對於資料庫來說,就是 Connection
物件(由 DataSource
提供)或是 Java Persistence API(JPA)的 EntityManager
物件;對於 Java Message Service(JMS)來說,則是 Session
物件。
在一個典型的例子中,一個 JMS 訊息觸發了資料庫的更新。根據時間先後順序,一次成功的互動過程如下:
- 啟動訊息事務
- 接收訊息
- 啟動資料庫事務
- 更新資料庫
- 提交資料庫事務
- 提交訊息事務
如果資料庫在更新資料時報錯(如約束衝突),理想的互動順序如下:
- 啟動訊息事務
- 接收訊息
- 啟動資料庫事務
- 更新資料庫失敗!
- 回滾資料庫事務
- 回滾訊息事務
在這個例子中,訊息在最後回滾完成之後回到了中介軟體,在某個時刻將再次提交到另一個事務中。這通常是一件好事,因為如果這樣做的話更新資料時發生的錯誤將會被記錄下來。(自動重試和異常處理的機制超出了本文的討論範圍。)
上述兩個例子中最重要的特點就是原子性,邏輯上來說,一個事務要麼完全成功,要麼完全失敗。
那麼是什麼保證了上面兩個例子在流程上的一致性呢?我們必須在事務資源之間進行一些同步,以便在一個事務提交之後,另一個事務才能提交。否則,整個事務就不是原子性的。因為涉及多個資源,所以事務是分散式的。如果不進行同步,事務就不會是原子性的。分散式事務的理論和實現上的困難都與資源的同步(或缺少資源)有關。
下面討論的前三個模式都是基於 XA 協議的。由於這些模式已經被普及,所以在這裡我不會介紹得很詳細。如果你對 XA 的模式非常熟悉,你可以直接跳到共享事務資源模式。
完整的 XA 協議與兩階段提交(2PC)
如果你需要確保應用程式的事務在伺服器當機(伺服器崩潰或斷電)之後仍能夠恢復,那麼完整的 XA 協議是你唯一的選擇。在下面的例子中,用於同步事務的共享資源是一個特殊的事務管理器,它使用 XA 協議協調了程式的資訊。在 Java 中,從開發者的角度來看,該協議是通過 JTA 的 UserTransaction
物件暴露出來的。
作為一個系統介面,XA 是大多數開發者從未見過的一種底層技術。開發者需要知道 XA 協議的存在、它能做什麼、效能消耗如何,以及它是如何操作事務資源的。效能消耗來自於兩階段提交(2PC)協議,事務管理器使用該協議來確保所有資源能在事務結束前就事務的結果達成一致。
如果應用程式是基於 Spring 構建的,它將使用 Spring 中的 JtaTransactionManager
和 Spring 宣告性事務管理來隱藏底層同步的細節。對於開發者來說,使用 XA 與否取決於工廠資源的配置方式:在應用程式中如何配置 DataSource
例項和事務管理器。本文包含了一個示例應用程式(atomikos-db
專案),它演示了這種配置方式。該應用程式中只有 DataSource
例項和事務管理器是基於 XA 或者 JTA 的。
要檢視示例的執行方式,請執行 com.springsource.open.db
下的單元測試。MulipleDataSourceTests
類向兩個資料來源插入了資料,然後使用 Spring 的整合支援特性將事務回滾,如清單 1 所示:
清單 1. 事務回滾
@Transactional
@Test
public void testInsertIntoTwoDataSources() throws Exception {
int count = getJdbcTemplate().update(
"INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", 0,
"foo");
assertEquals(1, count);
count = getOtherJdbcTemplate()
.update(
"INSERT into T_AUDITS (id,operation,name,audit_date) values (?,?,?,?)",
0, "INSERT", "foo", new Date());
assertEquals(1, count);
// 資料的變更將在此方法退出後回滾
}
複製程式碼
然後 MulipleDataSourceTests
將會驗證這兩個操作都回滾完成,如清單 2 所示:
清單 2. 驗證回滾
@AfterTransaction
public void checkPostConditions() {
int count = getJdbcTemplate().queryForInt("select count(*) from T_FOOS");
// 該資料變更已被測試框架回滾
assertEquals(0, count);
count = getOtherJdbcTemplate().queryForInt("select count(*) from T_AUDITS");
// 由於 XA 的存在,該資料變更也被回滾了
assertEquals(0, count);
}
複製程式碼
為了更好地理解 Spring 事務管理的工作原理以及配置的方式,請參閱 Spring 參考文件。
XA 與 1PC 優化
這種模式通過避免 2PC 的效能開銷對許多隻包含單資源事務的事務管理器進行了優化。你將會希望你的應用程式服務能夠藉此解決這個問題。
XA 與最終資源策略
XA 事務管理器的另一個特性是,當除某一個資源外的所有資源都支援 XA 時,它仍然可以提供與所有資源都支援 XA 時相同的資料恢復保證。通過對資源進行排序,並使非 XA 資源參與決策來實現該特性。如果提交失敗,則回滾所有其他資源。這幾乎是 100% 的完全性保證,但還不夠完美。當提交失敗時,除非採取額外的措施(在一些高階實現中有這樣的實現),否則報錯的跟蹤資訊會很少。
共享事務資源模式
在某些系統中,為了降低複雜性和增加吞吐量,一種較好的模式是通過確保系統中的所有事務資源實際上都是同一個資源的不同形式,從而完全消除對 XA 的依賴。顯然,這在所有的用例中都是不可能的,但這種模式與 XA 一樣可靠,而且通常要快得多。這樣的共享事務資源模式是足夠可靠的,但只限於某些特定的平臺和處理場景。
有一個這種模式的簡單例子對很多人來說都很熟悉,即在物件關係對映(ORM)元件和 JDBC 元件之間共享資料庫的 Connection
。這就是你使用支援 ORM 工具的 Spring 事務管理器時所發生的事情,如 Hibernate、EclipseLink 和 Java Persistence API(JPA)。同一個事務可以安全地跨 ORM 和 JDBC 元件使用,該執行過程通常由控制事務的服務級方法來實現。
該模式的另一個有效用法是單個資料庫的訊息驅動更新(如本文中介紹的簡單例子所示)。訊息中介軟體系統需要將資料儲存在某個地方,通常是關聯式資料庫中。要實現此模式,只需指定訊息傳遞系統的目標資料庫為同一個業務資料庫即可。此模式需要訊息中介軟體的供應商公開其儲存策略的詳細資訊,以便可以將其配置指向相同的資料庫並掛接到相同的事務中。
並不是所有的供應商都能做到這一點。另一種適用於幾乎所有資料庫的方式,是使用 Apache ActiveMQ 進行訊息傳遞並將儲存策略配置到訊息代理伺服器中。瞭解其中的技巧,配置起來就會非常簡單。本文的 shared-jms-db
示例專案展示了這種配置方式。應用程式的程式碼中(在本例中是單元測試)不需要感知這種模式的使用,因為它已經在 Spring 配置中已經以宣告方式被啟用了。
示例中名為 SynchronousMessageTriggerAndRollbackTests
的單元測試驗證了所有同步訊息的接收處理。testReceiveMessageUpdateDatabase
方法接收了兩條訊息,並將這兩條訊息中的資料記錄插入到資料庫中。當退出該方法時,測試框架將會回滾當前的事務,接下來你就可以驗證訊息和資料庫更新都已經回滾,如清單 3 所示:
清單 3. 驗證訊息和資料庫更新的回滾
@AfterTransaction
public void checkPostConditions() {
assertEquals(0, SimpleJdbcTestUtils.countRowsInTable(jdbcTemplate, "T_FOOS"));
List<String> list = getMessages();
assertEquals(2, list.size());
}
複製程式碼
該配置最重要的特性是 ActiveMQ 的持久化策略,它將業務資料來源的訊息系統連線到同一個 DataSource
,用於接收訊息的 Spring JmsTemplate
上的標誌位也同樣重要。配置 ActiveMQ 持久化策略的方式如清單 4 所示:
清單 4. ActiveMQ 的持久化配置
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"
depends-on="brokerService">
<property vm://localhost?async=false" />
</bean>
<bean id="brokerService" class="org.apache.activemq.broker.BrokerService" init-method="start"
destroy-method="stop">
...
<property >
<bean class="org.apache.activemq.store.jdbc.JDBCPersistenceAdapter">
<property >
<bean class="com.springsource.open.jms.JmsTransactionAwareDataSourceProxy">
<property />
<property />
</bean>
</property>
<property />
</bean>
</property>
</bean>
複製程式碼
用於接收訊息的 Spring JmsTemplate
上的標誌位配置如清單 5 所示:
清單 5. 為事務配置 JmsTemplate
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
...
<!-- 這很重要... -->
<property />
</bean>
複製程式碼
如果沒有設定 sessionTransacted=true
,就永遠不會執行 JMS 會話事務的 API 呼叫,並且訊息的接收將無法回滾。這裡重要的一點是嵌入式訊息代理伺服器中的特殊引數 async=false
和對 DataSource
的包裝,他們共同確保了 ActiveMQ 和 Spring 共同使用了同一個 JDBC 事務的 Connection
。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。