使用事件和訊息佇列實現分布式事務

2022-05-29 01:15:20 字數 2970 閱讀 7437

原文:

不同於單一架構應用(monolith), 分布式環境下, 進行事務操作將變得困難, 因為分布式環境通常會有多個資料來源, 只用本地資料庫事務難以保證多個資料來源資料的一致性. 這種情況下, 可以使用兩階段或者三階段提交協議來完成分布式事務.但是使用這種方式一般來說效能較差, 因為事務管理器需要在多個資料來源之間進行多次等待. 有一種方法同樣可以解決分布式事務問題, 並且效能較好, 這就是我這篇文章要介紹的使用事件,本地事務以及訊息佇列來實現分布式事務.

我們從乙個簡單的例項入手. 基本所有網際網路應用都會有使用者註冊的功能. 在這個例子中, 我們對於使用者註冊有兩步操作:

1. 註冊成功, 儲存使用者資訊.

2. 需要給使用者發放一張代金券, 目的是鼓勵使用者進行消費.

如果是乙個單一架構應用, 實現這個功能非常簡單: 在乙個本地事務裡, 往使用者表插一條記錄, 並且在代金券表裡插一條記錄, 提交事務就完成了.

但是如果我們的應用是用微服務實現的, 可能使用者和代金券是兩個獨立的服務, 他們有各自的應用和資料庫,

那麼就沒有辦法簡單的使用本地事務來保證操作的原子性了.

現在來看看如何使用事件機制和訊息佇列來實現這個需求.(我在這裡使用的訊息佇列是kafka, 原理同樣適用於activemq/rabbitmq等其他佇列)

我們會為使用者註冊這個操作建立乙個事件, 該事件就叫做使用者建立事件(user_created). 使用者服務成功儲存使用者記錄後, 會傳送使用者建立事件到訊息佇列,

代金券服務會監聽使用者建立事件, 一旦接收到該事件, 代金券服務就會在自己的資料庫中為該使用者建立一張代金券.

好了, 這些步驟看起來都相當的簡單直觀, 但是怎麼保證事務的原子性呢? 考慮下面這兩個場景:

1. 使用者服務在儲存使用者記錄, 還沒來得及向訊息佇列傳送訊息之前就宕機了. 怎麼保證使用者建立事件一定傳送到訊息佇列了?

2. 代金券服務接收到使用者建立事件, 還沒來得及處理事件就宕機了. 重新啟動之後如何消費之前的使用者建立事件?

這兩個問題的本質是: 如何讓運算元據庫和操作訊息佇列這兩個操作成為乙個原子操作.

不考慮2pc, 這裡我們可以通過事件表來解決這個問題. 下面是類圖.

eventpublish是記錄待發布事件的表. 其中:

id: 每個事件在建立的時候都會生成乙個全域性唯一id, 例如uuid.

status: 事件狀態, 列舉型別. 現在只有兩個狀態: 待發布(new), 已發布(published).

payload: 事件內容. 這裡我們會將事件內容轉成json存到這個欄位裡.

eventtype: 事件型別, 列舉型別. 每個事件都會有乙個型別, 比如我們之前提到的建立使用者user_created就是乙個事件型別.

eventprocess是用來記錄待處理的事件. 欄位與eventpublish基本相同.

我們首先看看事件的發布過程. 下面是使用者服務發布使用者建立事件的順序圖.

1. 使用者服務在接收到使用者請求後開啟事務, 在使用者表建立一條使用者記錄, 並且在eventpublish表建立一條status為new的記錄, payload記錄的是事件內容, 提交事務.

2. 使用者服務中的定時器首先開啟事務, 然後查詢eventpublish是否有status為new的記錄, 查詢到記錄之後, 拿到payload資訊, 將訊息發布到kafka中對應的topic.

傳送成功之後, 修改資料庫中eventpublish的status為published, 提交事務.

下面是代金券服務處理使用者建立事件的順序圖.

1. 代金券服務接收到kafka傳來的使用者建立事件(實際上是代金券服務主動拉取的訊息, 先忽略訊息佇列的實現),

在eventprocess表建立一條status為new的記錄, payload記錄的是事件內容, 如果儲存成功, 向kafka返回接收成功的訊息.

2. 代金券服務中的定時器首先開啟事務, 然後查詢eventprocess是否有status為new的記錄, 查詢到記錄之後,

拿到payload資訊, 交給事件**處理器處理, 這裡是直接建立代金券記錄.

處理成功之後修改資料庫中eventprocess的status為processed, 最後提交事務.

回過頭來看我們之前提出的兩個問題:

1. 使用者服務在儲存使用者記錄, 還沒來得及向訊息佇列傳送訊息之前就宕機了. 怎麼保證使用者建立事件一定傳送到訊息佇列了?

根據事件發布的順序圖, 我們把建立事件和發布事件分成了兩步操作.

如果事件建立成功, 但是在發布的時候宕機了. 啟動之後定時器會重新對之前沒有發布成功的事件進行發布.

如果事件在建立的時候就宕機了, 因為事件建立和業務操作在乙個資料庫事務裡, 所以對應的業務操作也失敗了, 資料庫狀態的一致性得到了保證.

2. 代金券服務接收到使用者建立事件, 還沒來得及處理事件就宕機了. 重新啟動之後如何消費之前的使用者建立事件?

根據事件處理的順序圖, 我們把接收事件和處理事件分成了兩步操作.

如果事件接收成功, 但是在處理的時候宕機了. 啟動之後定時器會重新對之前沒有處理成功的事件進行處理.

如果事件在接收的時候就宕機了, kafka會重新將事件傳送給對應服務.

使用者如果登入系統, 會發現自己是沒有代金券的. 這種情況可能在有些業務中是能夠容忍的, 但是有些業務卻不行. 所以開發之前要考慮好.

另外, 上面的流程在實現的過程中還有一些可以改進的地方:

1. 定時器在更新eventpublish狀態為published的時候, 可以一次批量更新多個eventprocess的狀態.

2. 定時器查詢eventprocess並交給事件**處理器處理的時候, 可以使用執行緒池非同步處理, 加快eventprocess處理週期.

3. 在儲存eventpublish和eventprocess的時候同時儲存到redis, 之後的操作可以對redis中的資料進行, 但是要小心處理快取和資料庫可能狀態不一致問題.

4. 針對kafka, 因為kafka的特點是可能重發訊息, 所以在接收事件並且儲存到eventprocess的時候可能報主鍵衝突的錯誤(因為重複訊息id是相同的), 這個時候可以直接丟棄該訊息.

關於2pc:

分布式訊息佇列

以下是訊息佇列以下的大綱,本文主要介紹訊息佇列概述,訊息佇列應用場景和訊息中介軟體示例 電商,日誌系統 訊息佇列概述 訊息佇列應用場景 訊息中介軟體示例 jms訊息服務 見第二篇 大型 架構系列 分布式訊息佇列 二 常用訊息佇列 見第二篇 大型 架構系列 分布式訊息佇列 二 參考 推薦 資料 見第二...

分布式訊息佇列

訊息佇列中介軟體是分布式系統中重要的元件,主要解決應用耦合,非同步訊息,流量削鋒等問題。實現高效能,高可用,可伸縮和最終一致性架構。是大型分布式系統不可缺少的中介軟體。目前在生產環境,使用較多的訊息佇列有activemq,rabbitmq,zeromq,kafka,metamq,rocketmq等。...

訊息佇列實現分布式事務

訊息佇列中的 事務 主要解決的是訊息生產者和訊息消費者的資料一致性問題。電商下單步驟 1 生成訂單 2 刪除購物車 在分布式系統中,任何乙個步驟都有可能失敗,可能出現訂單資料與購物車資料不一致的情況,比如說 1 建立了訂單,沒有清理購物車 2 訂單沒建立成功,購物車裡面的商品卻被清掉了。訂單系統給訊...