微服務架構(九): 數據管理

工作中使用了微服務架構,接下來的一段時間裏,我會寫一系列的文章來介紹微服務架構,這篇文章主要講述了微服務架構中的數據管理。

翻譯和整理自:

  • http://microservices.io/patterns/data/database-per-service.html
  • http://microservices.io/patterns/data/shared-database.html
  • http://microservices.io/patterns/data/event-driven-architecture.html
  • http://microservices.io/patterns/data/event-sourcing.html
  • http://microservices.io/patterns/data/transaction-log-tailing.html
  • http://microservices.io/patterns/data/database-triggers.html
  • http://microservices.io/patterns/data/application-events.html
  • http://microservices.io/patterns/data/cqrs.html
  • http://www.cnblogs.com/dinglang/p/5679542.html (文中摘抄了這篇博文的大部分內容)

一、事件驅動架構


1.上下文和問題


你已經應用了 Database per Service 模式。每個服務都有它自己的數據庫。但是,有一些業務事務跨越多個服務,所以你需要一種機制來確保多個服務之間的數據一致性。比如說,假設你在構建一個在線商城,應用必須保證一個新訂單的數額不超過客戶的餘額。因爲訂單和客戶在不同的數據庫之間,所以你不能簡單的用本地的ACID事務去達到這個目的。那麼,怎麼保障數據的一致性呢?


強制條件:  不使用兩階段提交協議  (譯者注: 讀者如果不瞭解分佈式事務,可以參考下這篇文章: http://www.cnblogs.com/dinglang/p/5679542.html)

2.解決方案


使用一個數據驅動的,最終一致性的解決方案。每個服務在更新數據時會發佈一個事件,別的服務訂閱這些事件,當收到事件時,服務更新自己的數據。

示例:

  1. Order Service 創建一個處於待定狀態的訂單,然後發佈一個OrderCreated 事件。
  2. Customer Service 收到這個事件,判斷用戶的餘額滿不滿足這個訂單的需求。然後它發佈一個 Credit Reserve 事件或者一個 CreditLimitExceeded 事件。
  3. Order Service 收到了 Customer Service 發佈的時間,把訂單的狀態改成通過或取消。

3.結果


優點:

  • 應用不需要使用分佈式事務就能維持多個服務之間的數據一致性

缺點:

  • 編程模型更復雜了

要解決的問題:


二、事件源(Event sourcing)


1.上下文和問題


你已經應用了上文的事件驅動架構模式。爲了實現可靠性,應用必須原子的更新自己的數據庫和發佈事件。使用分佈式事務來覆蓋數據庫和消息代理層面不可行的,那麼怎麼做呢?

2.解決方案


解決這個問題的一個好方案是使用事件源。事件源把業務實體比如訂單和客戶持久化成一系列狀態變化的事件。當一個業務實體的狀態變化時,新的事件加入到這個事件列表的尾端。因爲保存一個事件是單個操作,它是原子性的。應用可以通過重現這些事件的方式來重新構建一個實體現在的狀態。

應用把事件持久化在一個事件存儲中,它是一個事件的數據庫。這個存儲有一個增加和獲取事件的api。這個事件存儲也類似於一個消息代理,它提供了一個api讓服務可以訂閱事件。當一個服務把事件保存在事件存儲中時,這個事件被傳遞到所有的訂閱者那。

有一些實體,比如說客戶,可能會有很大數量的事件。爲了優化負載量,應用可以週期性地保存一個實體當前狀態的快照。爲了重新構建當前的狀態,應用找到最新的快照和那個快照之後的事件,於是需要重現的事件就少了一點。

示例

Customers and Orders 是一個使用事件源和 CQRS的示例應用。這個應用使用Java語言編寫,用到了Spring Boot。它使用 Eventuate構建, 這是一個基於事件源和CQRS的應用平臺。下圖展示了它是怎麼持久化訂單的:




應用以一系列事件的方式持久化Order ,而不是簡單地把每個order當前的狀態保存在表中。CustomerService可以訂閱order events,然後更新自己的狀態。

Order:

public class Order extends ReflectiveMutableCommandProcessingAggregate<Order, OrderCommand> {

  private OrderState state;
  private String customerId;

  public OrderState getState() {
    return state;
  }

  public List<Event> process(CreateOrderCommand cmd) {
    return EventUtil.events(new OrderCreatedEvent(cmd.getCustomerId(), cmd.getOrderTotal()));
  }

  public List<Event> process(ApproveOrderCommand cmd) {
    return EventUtil.events(new OrderApprovedEvent(customerId));
  }

  public List<Event> process(RejectOrderCommand cmd) {
    return EventUtil.events(new OrderRejectedEvent(customerId));
  }

  public void apply(OrderCreatedEvent event) {
    this.state = OrderState.CREATED;
    this.customerId = event.getCustomerId();
  }

  public void apply(OrderApprovedEvent event) {
    this.state = OrderState.APPROVED;
  }


  public void apply(OrderRejectedEvent event) {
    this.state = OrderState.REJECTED;
  }

這是CustomerService 訂閱order events的一個事件處理器:

@EventSubscriber(id = "customerWorkflow")
public class CustomerWorkflow {

  @EventHandlerMethod
  public CompletableFuture<EntityWithIdAndVersion<Customer>> reserveCredit(
          EventHandlerContext<OrderCreatedEvent> ctx) {
    OrderCreatedEvent event = ctx.getEvent();
    Money orderTotal = event.getOrderTotal();
    String customerId = event.getCustomerId();
    String orderId = ctx.getEntityId();

    return ctx.update(Customer.class, customerId, new ReserveCreditCommand(orderTotal, orderId));
  }

}

好處:

  • 它解決了實現事件驅動架構的一個關鍵的問題,使得在狀態改變時可靠的發佈事件成爲可能
  • 因爲它持久化的是事件,而不是領域對象,避免了對象關係不匹配的問題
  • 提供了一個百分百可靠的改變對象實體的審計日誌
  • 使得實現基於時間的查詢成爲可能

壞處:

  • 是一種不同的、不習慣的編程方式,需要學習曲線
  • event store很難做查詢,因爲它需要把業務實體的狀態重新構建,這有點複雜和低效。因此,這個應用使用Command Query Responsibility Segregation (CQRS) 去實現查詢。


三、應用發佈事件(本地表)


1.上下文和問題


你已經應用了上文的事件驅動架構模式。爲了實現可靠性,應用必須原子的更新自己的數據庫和發佈事件。使用分佈式事務來覆蓋數據庫和消息代理層面不可行的,那麼怎麼做呢?

2.解決方案


應用插入事件到EVENTS表中,作爲本地事務的一部分。一個獨立的進程poll這個EVENTS表,然後把事件發佈到消息代理。


(譯者注:關於這種方法,我從http://www.cnblogs.com/dinglang/p/5679542.html上摘錄了一段更詳細的說明,以方便大家對分佈式事務、2PC、事件驅動有更好的理解:

分佈式事務

提到分佈式系統,必然要提到分佈式事務。要想理解分佈式事務,不得不先介紹一下兩階段提交協議。先舉個簡單但不精準的例子來說明:

第一階段,張老師作爲“協調者”,給小強和小明(參與者、節點)發微信,組織他們倆明天8點在學校門口集合,一起去爬山,然後開始等待小強和小明答覆。

第二階段,如果小強和小明都回答沒問題,那麼大家如約而至。如果小強或者小明其中一人回答說“明天沒空,不行”,那麼張老師會立即通知小強和小明“爬山活動取消”。

細心的讀者會發現,這個過程中可能有很多問題的。如果小強沒看手機,那麼張老師會一直等着答覆,小明可能在家裏把爬山裝備都準備好了卻一直等着張老師確認信息。更嚴重的是,如果到明天8點小強還沒有答覆,那麼就算“超時”了,那小明到底去還是不去集合爬山呢?

這就是兩階段提交協議的弊病,所以後來業界又引入了三階段提交協議來解決該類問題。

兩階段提交協議在主流開發語言平臺,數據庫產品中都有廣泛應用和實現的,下面來介紹一下XOpen組織提供的DTP模型圖:


XA協議指的是TM(事務管理器)和RM(資源管理器)之間的接口。目前主流的關係型數據庫產品都是實現了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的,事務管理器和資源管理器之間也使用了XA協議。 本質上也是藉助兩階段提交協議來實現分佈式事務的,下面分別來看看XA事務成功和失敗的模型圖:


在JavaEE平臺下,WebLogic、Webshare等主流商用的應用服務器提供了JTA的實現和支持。而在Tomcat下是沒有實現的(其實筆者並不認爲Tomcat能算是JavaEE應用服務器),這就需要藉助第三方的框架Jotm、Automikos等來實現,兩者均支持spring事務整合。

而在Windows .NET平臺中,則可以藉助ado.net中的TransactionScop API來編程實現,還必須配置和藉助Windows操作系統中的MSDTC服務。如果你的數據庫使用的mysql,並且mysql是部署在Linux平臺上的,那麼是無法支持分佈式事務的。 由於篇幅關係,這裏不展開,感興趣的讀者可以自行查閱相關資料並實踐。

總結:這種方式實現難度不算太高,比較適合傳統的單體應用,在同一個方法中存在跨庫操作的情況。但分佈式事務對性能的影響會比較大,不適合高併發和高性能要求的場景。

提供回滾接口

在服務化架構中,功能X,需要去協調後端的A、B甚至更多的原子服務。那麼問題來了,假如A和B其中一個調用失敗了,那可怎麼辦呢?

在筆者的工作中經常遇到這類問題,往往提供了一個BFF層來協調調用A、B服務。如果有些是需要同步返回結果的,我會盡量按照“串行”的方式去調用。如果調用A失敗,則不會盲目去調用B。如果調用A成功,而調用B失敗,會嘗試去回滾剛剛對A的調用操作。

當然,有些時候我們不必嚴格提供單獨對應的回滾接口,可以通過傳遞參數巧妙的實現。

這樣的情況,我們會盡量把可提供回滾接口的服務放在前面。舉個例子說明:

我們的某個論壇網站,每天登錄成功後會獎勵用戶5個積分,但是積分和用戶又是兩套獨立的子系統服務,對應不同的DB,這控制起來就比較麻煩了。解決思路:

  1. 把登錄和加積分的服務調用放在BFF層一個本地方法中。
  2. 當用戶請求登錄接口時,先執行加積分操作,加分成功後再執行登錄操作
  3. 如果登錄成功,那當然最好了,積分也加成功了。如果登錄失敗,則調用加積分對應的回滾接口(執行減積分的操作)。

總結:這種方式缺點比較多,通常在複雜場景下是不推薦使用的,除非是非常簡單的場景,非常容易提供回滾,而且依賴的服務也非常少的情況。

 

這種實現方式會造成代碼量龐大,耦合性高。而且非常有侷限性,因爲有很多的業務是無法很簡單的實現回滾的,如果串行的服務很多,回滾的成本實在太高。

本地消息表

這種實現方式的思路,其實是源於ebay,後來通過支付寶等公司的佈道,在業內廣泛使用。其基本的設計思想是將遠程分佈式事務拆分成一系列的本地事務。如果不考慮性能及設計優雅,藉助關係型數據庫中的表即可實現。

舉個經典的跨行轉賬的例子來描述。

第一步僞代碼如下,扣款1W,通過本地事務保證了憑證消息插入到消息表中。


第二步,通知對方銀行賬戶上加1W了。那問題來了,如何通知到對方呢?

通常採用兩種方式:

  1. 採用時效性高的MQ,由對方訂閱消息並監聽,有消息時自動觸發事件
  2. 採用定時輪詢掃描的方式,去檢查消息表的數據。

兩種方式其實各有利弊,僅僅依靠MQ,可能會出現通知失敗的問題。而過於頻繁的定時輪詢,效率也不是最佳的(90%是無用功)。所以,我們一般會把兩種方式結合起來使用。

解決了通知的問題,又有新的問題了。萬一這消息有重複被消費,往用戶帳號上多加了錢,那豈不是後果很嚴重?

仔細思考,其實我們可以消息消費方,也通過一個“消費狀態表”來記錄消費狀態。在執行“加款”操作之前,檢測下該消息(提供標識)是否已經消費過,消費完成後,通過本地事務控制來更新這個“消費狀態表”。這樣子就避免重複消費的問題。

總結:上訴的方式是一種非常經典的實現,基本避免了分佈式事務,實現了“最終一致性”。但是,關係型數據庫的吞吐量和性能方面存在瓶頸,頻繁的讀寫消息會給數據庫造成壓力。所以,在真正的高併發場景下,該方案也會有瓶頸和限制的。

MQ(非事務消息)

通常情況下,在使用非事務消息支持的MQ產品時,我們很難將業務操作與對MQ的操作放在一個本地事務域中管理。通俗點描述,還是以上述提到的“跨行轉賬”爲例,我們很難保證在扣款完成之後對MQ投遞消息的操作就一定能成功。這樣一致性似乎很難保證。

先從消息生產者這端來分析,請看僞代碼:


根據上述代碼及註釋,我們來分析下可能的情況:

  1. 操作數據庫成功,向MQ中投遞消息也成功,皆大歡喜
  2. 操作數據庫失敗,不會向MQ中投遞消息了
  3. 操作數據庫成功,但是向MQ中投遞消息時失敗,向外拋出了異常,剛剛執行的更新數據庫的操作將被回滾

從上面分析的幾種情況來看,貌似問題都不大的。那麼我們來分析下消費者端面臨的問題:

  1. 消息出列後,消費者對應的業務操作要執行成功。如果業務執行失敗,消息不能失效或者丟失。需要保證消息與業務操作一致
  2. 儘量避免消息重複消費。如果重複消費,也不能因此影響業務結果

如何保證消息與業務操作一致,不丟失?

主流的MQ產品都具有持久化消息的功能。如果消費者宕機或者消費失敗,都可以執行重試機制的(有些MQ可以自定義重試次數)。

如何避免消息被重複消費造成的問題?

  1. 保證消費者調用業務的服務接口的冪等性
  2. 通過消費日誌或者類似狀態表來記錄消費狀態,便於判斷(建議在業務上自行實現,而不依賴MQ產品提供該特性)

 

總結:這種方式比較常見,性能和吞吐量是優於使用關係型數據庫消息表的方案。如果MQ自身和業務都具有高可用性,理論上是可以滿足大部分的業務場景的。不過在沒有充分測試的情況下,不建議在交易業務中直接使用。

MQ(事務消息)

舉個例子,Bob向Smith轉賬,那我們到底是先發送消息,還是先執行扣款操作?

好像都可能會出問題。如果先發消息,扣款操作失敗,那麼Smith的賬戶裏面會多出一筆錢。反過來,如果先執行扣款操作,後發送消息,那有可能扣款成功了但是消息沒發出去,Smith收不到錢。除了上面介紹的通過異常捕獲和回滾的方式外,還有沒有其他的思路呢?

下面以阿里巴巴的RocketMQ中間件爲例,分析下其設計和實現思路。

RocketMQ第一階段發送Prepared消息時,會拿到消息的地址,第二階段執行本地事物,第三階段通過第一階段拿到的地址去訪問消息,並修改狀態。細心的讀者可能又發現問題了,如果確認消息發送失敗了怎麼辦?RocketMQ會定期掃描消息集羣中的事物消息,這時候發現了Prepared消息,它會向消息發送者確認,Bob的錢到底是減了還是沒減呢?如果減了是回滾還是繼續發送確認消息呢?RocketMQ會根據發送端設置的策略來決定是回滾還是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。如下圖:


總結:據筆者的瞭解,各大知名的電商平臺和互聯網公司,幾乎都是採用類似的設計思路來實現“最終一致性”的。這種方式適合的業務場景廣泛,而且比較可靠。不過這種方式技術實現的難度比較大。目前主流的開源MQ(ActiveMQ、RabbitMQ、Kafka)均未實現對事務消息的支持,所以需二次開發或者新造輪子。比較遺憾的是,RocketMQ事務消息部分的代碼也並未開源,需要自己去實現。

其他補償方式

做過支付寶交易接口的同學都知道,我們一般會在支付寶的回調頁面和接口裏,解密參數,然後調用系統中更新交易狀態相關的服務,將訂單更新爲付款成功。同時,只有當我們回調頁面中輸出了success字樣或者標識業務處理成功相應狀態碼時,支付寶纔會停止回調請求。否則,支付寶會每間隔一段時間後,再向客戶方發起回調請求,直到輸出成功標識爲止。

其實這就是一個很典型的補償例子,跟一些MQ重試補償機制很類似。

一般成熟的系統中,對於級別較高的服務和接口,整體的可用性通常都會很高。如果有些業務由於瞬時的網絡故障或調用超時等問題,那麼這種重試機制其實是非常有效的。

當然,考慮個比較極端的場景,假如系統自身有bug或者程序邏輯有問題,那麼重試1W次那也是無濟於事的。那豈不是就發生了“明明已經付款,卻顯示未付款不發貨”類似的悲劇?

其實爲了交易系統更可靠,我們一般會在類似交易這種高級別的服務代碼中,加入詳細日誌記錄的,一旦系統內部引發類似致命異常,會有郵件通知。同時,後臺會有定時任務掃描和分析此類日誌,檢查出這種特殊的情況,會嘗試通過程序來補償並郵件通知相關人員。

在某些特殊的情況下,還會有“人工補償”的,這也是最後一道屏障。

譯者注:摘錄到此結束!


發佈了123 篇原創文章 · 獲贊 334 · 訪問量 52萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章