使用kafka消息隊列解決分佈式事務(可靠消息最終一致性方案-本地消息服務)

微服務框架Spring Cloud介紹 Part1: 使用事件和消息隊列實現分佈式事務

本文轉自:http://skaka.me/blog/2016/04/21/springcloud1/spring

不一樣於單一架構應用(Monolith), 分佈式環境下, 進行事務操做將變得困難, 由於分佈式環境一般會有多個數據源, 只用本地數據庫事務難以保證多個數據源數據的一致性. 這種狀況下, 可使用兩階段或者三階段提交協議來完成分佈式事務.可是使用這種方式通常來講性能較差, 由於事務管理器須要在多個數據源之間進行屢次等待. 有一種方法一樣能夠解決分佈式事務問題, 而且性能較好, 這就是我這篇文章要介紹的使用事件,本地事務以及消息隊列來實現分佈式事務.數據庫

咱們從一個簡單的實例入手. 基本全部互聯網應用都會有用戶註冊的功能. 在這個例子中, 咱們對於用戶註冊有兩步操做: 
1. 註冊成功, 保存用戶信息.
2. 須要給用戶發放一張代金券, 目的是鼓勵用戶進行消費.
若是是一個單一架構應用, 實現這個功能很是簡單: 在一個本地事務裏, 往用戶表插一條記錄, 而且在代金券表裏插一條記錄, 提交事務就完成了. 可是若是咱們的應用是用微服務實現的, 可能用戶和代金券是兩個獨立的服務, 他們有各自的應用和數據庫, 那麼就沒有辦法簡單的使用本地事務來保證操做的原子性了. 如今來看看如何使用事件機制和消息隊列來實現這個需求.(我在這裏使用的消息隊列是kafka, 原理一樣適用於ActiveMQ/RabbitMQ等其餘隊列)編程

咱們會爲用戶註冊這個操做建立一個事件, 該事件就叫作用戶建立事件(USER_CREATED). 用戶服務成功保存用戶記錄後, 會發送用戶建立事件到消息隊列, 代金券服務會監聽用戶建立事件, 一旦接收到該事件, 代金券服務就會在本身的數據庫中爲該用戶建立一張代金券. 好了, 這些步驟看起來都至關的簡單直觀, 可是怎麼保證事務的原子性呢? 考慮下面這兩個場景:
1. 用戶服務在保存用戶記錄, 還沒來得及向消息隊列發送消息以前就宕機了. 怎麼保證用戶建立事件必定發送到消息隊列了?
2. 代金券服務接收到用戶建立事件, 還沒來得及處理事件就宕機了. 從新啓動以後如何消費以前的用戶建立事件?
這兩個問題的本質是: 如何讓操做數據庫和操做消息隊列這兩個操做成爲一個原子操做. 不考慮2PC, 這裏咱們能夠經過事件表來解決這個問題. 下面是類圖. json

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會從新將事件發送給對應服務.框架

經過這種方式, 咱們不用2PC, 也保證了多個數據源之間狀態的最終一致性.
和2PC/3PC這種同步事務處理的方式相比, 這種異步事務處理方式具備異步系統一般都有的優勢:
1. 事務吞吐量大. 由於不須要等待其餘數據源響應.
2. 容錯性好. A服務在發佈事件的時候, B服務甚至能夠不在線.
缺點:
1. 編程與調試較複雜.
2. 容易出現較多的中間狀態. 好比上面的例子, 在用戶服務已經保存了用戶併發布了事件, 可是代金券服務還沒來得及處理以前, 用戶若是登陸系統, 會發現本身是沒有代金券的. 這種狀況可能在有些業務中是可以容忍的, 可是有些業務卻不行. 因此開發以前要考慮好.異步

三、與具體業務場景綁定,偶爾性強,不能夠共用分佈式

四、消息數據和業務數據同一個庫,佔用業務數據庫資源

另外, 上面的流程在實現的過程當中還有一些能夠改進的地方:1. 定時器在更新EventPublish狀態爲PUBLISHED的時候, 能夠一次批量更新多個EventProcess的狀態.2. 定時器查詢EventProcess並交給事件回調處理器處理的時候, 可使用線程池異步處理, 加快EventProcess處理週期.3. 在保存EventPublish和EventProcess的時候同時保存到Redis, 以後的操做能夠對Redis中的數據進行, 可是要當心處理緩存和數據庫可能狀態不一致問題.4. 針對Kafka, 由於Kafka的特色是可能重發消息, 因此在接收事件而且保存到EventProcess的時候可能報主鍵衝突的錯誤(由於重複消息id是相同的), 這個時候能夠直接丟棄該消息.

相關文章
相關標籤/搜索