Spring分佈式事務實現概覽

分佈式事務,一直是實現分佈式系統過程中最大的挑戰。在只有單個數據源的單服務系統當中,只要這個數據源支持事務,例如大部分關係型數據庫,和一些MQ服務,如activeMQ等,我們就可以很容易的實現事務。

本地事物

大家可能都知道什麼是事務,但是我們還是再來看一下它的定義。事務的概念來自於數據庫事務,在數據庫事務定義中,事務是一個執行的邏輯單元,它需要提供一個一致、可靠的數據操作。它主要包括下面兩個目標:

  1. 當出現任何錯誤,包括系統宕機、部分失敗等,都能保證左右的數據修改都恢復到未修改的狀態。
  2. 不同的事務併發放完相同的數據時,提供適當的隔離機制。

我們常說的ACID其實,其實是某些數據庫特有的事務的實現方式,也就是實現了原子性、一致性、隔離性和持久性。

分佈式系統的實現原則

那麼在分佈式系統當中,我們應該怎麼樣去實現事務呢?這就需要從分佈式系統的原則說起。分佈式系統的實現原則有幾種說法,如BASE原理、ACP原理。

其中ACP是:

  • A: 可用性(Availability)
  • C: 一致性(Consistency)
  • P: 分區容錯性(Tolerance of network Partition)

A和P沒什麼好說的,就是分佈式系統的基本特性,C(一致性)就是指在分佈式系統當中,多個節點之間數據的一致性,包括一個節點修改的數據,通過另一個節點訪問的時候也能看到;以及當一個操作需要修改多個數據源的數據的時候,多個修改要都能夠完成,或者都不完成。

這裡的一致性,我們可以看做是上面說的數據庫事務的ACID特性中,原子性、一致性,甚至是隔離性的統一。如果以ACID這4個特性為要求來實現分佈式系統,在現實當中是不可能的,其中原子性就沒有辦法實現。如果一個業務請求,要修改多個數據庫中的數據,那麼這多個數據庫的操作,就無法實現原子性,勢必會有一個先後,在第一個數據庫上完成以後,再在第二個數據庫上完成,那麼這期間的一點點時間,就違反了原子性。

所以,我們往往無法在分佈式系統中實現完全的一致性,所以就有了BASE理論。BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)三個短語的縮寫。BASE理論是對CAP中一致性和可用性權衡的結果,要求實現最終一致性即可。

其中,Soft state(軟狀態)是指,在一個業務操作過程中,允許出現一箇中間狀態,也就是軟狀態,而不要求原子性那樣,要麼都完成,要麼都不完成。例如在下單的時候,出現一個“正在處理”的狀態。由於有這個軟狀態,那我的一致性,就不要求是強一致性,而是最終一致性,也就是說,只要最終這個請求能處理完,所有的數據狀態都是處理完的狀態;如果期間出錯了,所有的數據也都一致,該失敗的失敗、該退錢的退錢、該重置的重置。

分佈式事務的實現

所以,確定了分佈式系統的實現原則是最終一致性以後,同時也明確了我們實現分佈式事務的原則,也是最終一致性。

其實,不管是數據庫事務的ACID特性,還是分佈式事務的最終一致性,其實,都是根據事務的定義和它的兩個目標,所採取的不同的實現方式。

那麼我們應該怎麼實現這個最終一致性呢?

單服務的分佈式事務

首先,任何一個分佈式系統,總是由一個個的系統組成,也就是一個個的服務,這些服務又可以部署多個。同時,我們的整個系統也需要一定的方式相互作用、相關通信。有時候,我們可以讓一個服務直接調用另一個服務的接口(如果有提供的話);還有時候,我們可以讓兩個服務通過一個MQ之類的消息中間件通信,共同完成一些業務。但是,無論如何,大部分情況下,分佈式系統的一個服務總是會訪問多個數據源。最典型的例子就是通過MQ接受一個事件,然後出發一些操作,再把結果發送到另一個隊列裡。

對於這種每個服務訪問多個數據源的情況,其實就是一個最簡單的分佈式事務的場景。如果大家在網上搜“Spring分佈式事務實現”,搜到的結果也都是在說這個場景下的分佈式事務實現過程。

要實現這個事務,首先需要對Spring的事物機制有一定了解。對於這種情況,最簡單的就是使用Spring的JTA事務管理。但是,我們知道,JTA事務管理是通過兩階段提交實現的,在很多情況下,它的效率是很低的。因為它在多個數據源修改數據的時候,這些數據一直都處在被鎖的狀態,知道多個數據源的事務都提交完成,才會釋放。

如果不用JTA,Spring也給我們提供了幾種方式,來近似的實現分佈式事務(注意這裡說的近似)。例如:

  1. 事務同步,也就是提交一個事物的時候,通過Listener等方式通知另一個事務也提交。但是這種情況下,如果第二個事務提交的時候出錯了,第一個事物就無法回滾,因為他已經提交完成了。
  2. 鏈式事務,就是將多個事務,包裝在一個鏈式事務管理器當中,在提交事務的時候,一次提交裡面的事務。對於這種實現,也存在上面說的問題。
  3. 還有其他的一些方式,就不過多說明。

所以,使用Spring在單服務多數據源的情況下,實現分佈式事務,實際上沒辦法完全實現事務的,因為出錯的時候不能保證都會滾。那麼這時候,就需要再通過其他機制來補充。

  1. 首先就是重試,也就是在出錯的時候,重試之前的操作。這在有MQ的時候比較常用,因為一般的MQ服務器,在你讀消息以後,處理的時候如果出錯了,那麼這個讀消息的操作不會被提交。那這個消息就會被重新讀到,重新出發剛才的操作。這時候,我們就需要考慮這個方法的冪等性,保證在重複消息的時候不會重複處理數據。
  2. 其次,我們需要自己處理一些錯誤。例如上面的情況,重試幾次以後,一直沒有成功,那麼這時候就需要走失敗邏輯。有時候,我們也可以通過一個定時器來檢查一定時間內沒有完成的失敗操作。
  3. 有些情況下,我們還需要考慮其他各種錯誤,如網絡錯誤、超時,系統宕機等等。

大家可以試想一下,分佈式系統越複雜,它的各種出錯的情況就越多,我們需要考慮的補救措施就越多。那這種修修補補的實現分佈式事務的最終一致性的做法,始終不是一個好的辦法。但是,使用Spring解決單服務的分佈式系統,始終是分佈式事務實現的基礎。我們可以用其他的模式來方便我們解決分佈式事務,但是在每個服務當中,我們還是要經常使用事務同步、鏈式事務等,來實現事務。我們用Spring來保證絕大多數情況下的事務問題,而對於特殊的錯誤情況,就採用其他的模式來解決。

分佈式事務實現的模式

剛才說了我們用其他模式來覺得分佈式事務問題,那麼都有什麼模式呢?

消息驅動(Event Driven)模式

消息驅動模式是,當某個業務請求需要由多個服務參與完成的時候,這些服務之前不直接通信,而是通過一個MQ中間件來通信。比如對於一個訂單支付的請求,接收到支付完成的請求後,通過MQ,通知訂單服務去完成訂單,訂單服務再去通知商品服務去減庫存,再通知物流服務去發起物流流程。

那麼,對於每一個服務來說,都需要先從一個隊列讀取一個消息,完成自己的業務操作,再往另一個隊列發送一個消息,這就需要操作一個數據和一個MQ服務器。這也就是上面說的單服務的分佈式事務實現。對於這種模式而言,我們用事務同步保證在每個服務中,在大部分情況下都能保證事務。即使偶爾出現網絡錯誤、系統錯誤等,通過重試就能解決大部分問題。如果重試一直不能解決,那就再處理失敗邏輯。

我們使用這種方式,最重要的,就是對這個消息、和他的處理流程的編排,其次,它也是一種響應式的編程思維。

事件溯源(Event Sourcing)模式

Event Sourcing在上面說的消息驅動的基礎上,進一步提升事件(也就是之前的消息)的地位,讓它成為系統的一等公民。也就是說,怎麼的系統不是基於原先那些實體的,而是基於事件的,一個事件就代表一個業務操作和業務數據狀態的更改。至於業務數據,我們不需要把它保存在數據庫中,即使保存,也只是為了查詢數據方便而保存。

在Event Sourcing模式中,每個服務完成某個邏輯的方式,跟上面說的消息驅動模式差不多,就是對於用戶的每個操作,會產生一個事件(可能多個),這個事件會被某個服務的某個處理方法處理,它也有可能再產生其他的事件,再由其他服務處理,直到完成整個業務流程。但是,它跟消息驅動的最大區別就是,在Event Sourcing的服務裡,業務狀態數據不一定要保存在數據庫中,就算保存,出錯了也沒關係,反正它可以根據Event事件重新生成。所以這個地方的事務,我們只需要保證Event保存成功即可。當然,我們需要其他的機制,方便我們能夠重新生成業務數據,而這,一般都是實現Event Sourcing的框架來提供。

TCC(Try-Confirm-Cancel)模式

除了上面說的通過一箇中間價關聯不同的服務,在有些分佈式系統當中,我們的不同的服務可以直接通信,例如Spring Cloud微服務框架就提供Rest方式訪問別的服務。那麼這時候,就相當於,我的一個服務除了訪問自己的數據庫以外,還要訪問別的服務,這裡的這個服務就可以當做是一個數據庫。我們可能要調用別的服務的某個接口完成一些業務。

在這種情況下,一個服務提供的接口,不可能實現事務,也就是先操作數據,再Commit,如果出錯了再Rollback。但是,我們可以借鑑事務的這種處理思路,來自己提供類似事務的方法,這就是TCC模式。一個事物是通過Do-Commit/Rollback來實現的,在TCC模式中,是通過給每一個服務間調用的操作接口,提供一套Try-Confirm/Cancel接口。

還是舉一個例子,就是用戶下單以後支付完成。支付完成的時候由先訂單服務處理,然後調用商品服務去減庫存。大家用Spring Cloud的話,可能就在商品服務裡寫一個接口直接做減庫存的操作,但是在TCC模式下,我們需要3個接口。首先是減庫存的Try接口,在這裡,我們要檢查業務數據的狀態、檢查商品庫存夠不夠,然後做資源的預留,也就是在某個字段上設置預留的狀態。然後在Confirm接口裡,完成庫存減1的操作。在Cancel接口裡,把之前預留的字段重置。

這可能聽著有點繁瑣,感覺可以一次完成的事情,為什麼要分成2步,首先這麼做是為了能夠在出錯的時候正確的重置庫存數據,其次這個預留操作跟Confirm操作是兩個請求,中間可能會有其他併發請求。從理論上說,只要我們在Try接口裡面預留資源的邏輯是正確的,那麼,即使Confirm的時候出錯了,我也可以通過重試Confirm請求來完成

使用數據庫保存事務狀態

這其實不是一種模式,只是一種方式。例如在TCC模式下,在準備調用Confirm接口的時候,目標服務突然宕機了,或者發起請求的服務突然宕機或出錯了,導致這個Confirm請求一直沒有被調用。那麼,在系統恢復以後,我該怎麼完成之前的事務呢?除了上面說的用定時器定期檢查未完成的操作以外(需要能夠通過某種數據狀態判斷業務沒有執行完成後),我們還可以用數據庫來記錄事務的運行狀態。

例如在TCC模式中,每當一個服務A要使用TCC模式調用另一個服務B的時候,服務A將這個TCC的事務狀態寫到數據庫中,根據具體實現,可能是在調用前記錄當前事務的狀態,調用完成再保存該調用的參數和結果狀態,這個事務完成以後(也就是調用完Confirm,或Cancel以後),再更新成完成的狀態。那麼,通過合理的設計,我們就能在各種出錯情況下,保證能繼續完成這個事務,或取消這個事務。

總結

總之,對於分佈式事務來說,沒有一個簡單的像本地事物一樣的實現方式,我們總是需要根據分佈式系統的設計,根據業務需求,選擇某種方式來保證數據的一致性。而且,在實現分佈式事務的過程中,業務流程的設計也至關重要,不管是用TCC、消息驅動、還是EventSourcing。分佈式系統的業務流程,實際上就是一個完備的狀態機,這個狀態機是否包含了所有的事件,是否包含了所有的業務路徑,包括正常的、異常的,合理的設計業務流程,才能更好地實現分佈式事務。

,謝謝合作!


分享到:


相關文章: