你們好,今天是第二次在這裏給你們分享數據一致性的話題,在第一篇分享中咱們介紹了微服務架構下應該知足數據最終一致性原則,並介紹實現最終一致性3種模式。 本文是系列分享的第二篇,講述可靠事件模式的實現方法。 圖片描述 在第一篇分享中咱們介紹了可靠事件模式屬於事件驅動架構,微服務完成業務操做後向消息代理髮布事件,關聯的微服務從消息代理訂閱到該事件從而完成相應的業務操做。 咱們還強調了實現可靠事件模式的關鍵在於:可靠事件投遞和避免事件重複消費。 圖片描述 可靠事件投遞定義爲(a)每一個服務原子性的完成業務操做和發佈事件(b)消息代理確保事件投遞至少一次。 避免重複消費要求消費事件的服務實現冪等性。 由於如今流行的消息隊列都實現了事件的持久化和at leastonce的投遞模式,(b)特性(消息代理確保事件投遞至少一次)已經知足,今天不作展開。 下面分享的內容主要從可靠事件投遞和實現冪等性兩方面來討論,咱們先來看可靠事件投遞。 首先咱們來看一個實現的代碼片斷,這是從某生產系統上截取下來的。 圖片描述 根據上述代碼及註釋,初看可能出現3種狀況: 1.操做數據庫成功,向消息代理投遞事件也成功 2.操做數據庫失敗,不會向消息代理中投遞事件了 3.操做數據庫成功,可是向消息代理中投遞事件時失敗,向外拋出了異常,剛剛執行的更新數據庫的操做將被回滾。 從上面分析的幾種狀況來看,貌似沒有問題。可是仔細分析不難發現缺陷所在,在上面的處理過程當中存在一段隱患時間窗口。 圖片描述 1) 微服務A投遞事件的時候可能消息代理已經處理成功,可是返回響應的時候網絡異常, 致使append操做拋出異常。最終結果是事件被投遞,數據庫確被回滾。 圖片描述 2) 在投遞完成後到數據庫commit操做之間若是微服務A宕機也將形成數據庫操做由於鏈接異常關閉而被回滾。最終結果仍是事件被投遞,數據庫卻被回滾。 這個實現每每運行很長時間都沒有出過問題,可是一旦出現了將會讓人感受莫名很難發現問題所在。 · · · 下面給出兩種可靠事件投遞的實現方式 本地事件表 圖片描述 本地事件表方法將事件和業務數據保存在同一個數據庫中,使用一個額外的「事件恢復」服務來恢復事件,由本地事務保證更新業務和發佈事件的原子性。考慮到事件恢復可能會有必定的延時,服務在完成本地事務後可當即向消息代理髮佈一個事件。 圖片描述 1.微服務在同一個本地事務中記錄業務數據和事件。 2.微服務實時發佈一個事件當即通知關聯的業務服務,若是事件發佈成功當即刪除記錄的事件。 3.事件恢復服務定時從事件表中恢復未發佈成功的事件,從新發布,從新發布成功才刪除記錄的事件。 其中第2條的操做主要是爲了增長髮布事件的實時性,由第三條保證事件必定被髮布。 本地事件表方式業務系統和事件系統耦合比較緊密,額外的事件數據庫操做也會給數據庫帶來額外的壓力,可能成爲瓶頸。 外部事件表 圖片描述 外部事件表方法將事件持久化到外部的事件系統,事件系統需提供實時事件服務以接受微服務發佈事件,同時事件系統還須要提供事件恢復服務來確認和恢復事件。 圖片描述 1.業務服務在事務提交前,經過實時事件服務向事件系統請求發送事件,事件系統只記錄事件並不真正發送。 2.業務服務在提交後,經過實時事件服務向事件系統確認發送,事件獲得確認後事件系統才真正發佈事件到消息代理。 3.業務服務在業務回滾時,經過實時事件向事件系統取消事件。 4.若是業務服務在發送確認或取消以前中止服務了怎麼辦呢?事件系統的事件恢復服務會按期找到未確認發送的事件向業務服務查詢狀態,根據業務服務返回的狀態決定事件是要發佈仍是取消。 該方式將業務系統和事件系統獨立解耦,均可以獨立伸縮。可是這種方式須要一次額外的發送操做,而且須要發佈者提供額外的查詢接口。 · · · 介紹完了可靠事件投遞再來講一說冪等性的實現,有些事件自己是冪等的,有些事件卻不是。 自己具備冪等性的事件須要考慮執行順序 圖片描述 若是事件自己描述的是某個時間點的固定值(如帳戶餘額爲100),而不是描述一條轉換指令(如餘額增長10),那麼這個事件是冪等的。 咱們要意識到事件可能出現的次數和順序是不可預測的,須要保證冪等事件的順序執行,不然結果每每不是咱們想要的。 若是咱們前後收到兩條事件,(1)帳戶餘額更新爲100,(2)帳戶餘額更新爲120。 1.微服務收到事件(1) 圖片描述 2. 微服務收到事件(2) 圖片描述 3. 微服務再次收到事件1 圖片描述 顯然結果是錯誤的,因此咱們須要保證事件(2)一旦執行事件(1)就不能再處理,不然帳戶餘額仍不是咱們想要的結果。 爲保證事件的順序一個簡單的作法是在事件中添加時間戳,微服務記錄每類型的事件最後處理的時間戳,若是收到的事件的時間戳早於咱們記錄的,丟棄該事件。 若是事件不是在同一個服務器上發出的,那麼服務器之間的時間同步是個難題,更穩妥的作法是使用一個全局遞增序列號替換時間戳。 對於自己不具備冪等性的操做,主要思想是爲每條事件存儲執行結果,當收到一條事件時咱們須要根據事件的id查詢該事件是否已經執行過,若是執行過直接返回上一次的執行結果,不然調度執行事件。 圖片描述 圖片描述 在這個思想下咱們須要考慮重複執行一條事件和查詢存儲結果的開銷。 · · · 重複處理開銷小的事件重複處理 圖片描述 若是重複處理一條事件的開銷相比額外一次查詢的開銷要高不少,使用一個過濾服務來過濾重複的事件,過濾服務使用事件存儲存儲已經處理過的事件和結果。 當收到一條事件時,過濾服務首先查詢事件存儲,肯定該條事件是否已經被處理過,若是事件已經被處理過,直接返回存儲的結果;不然調度業務服務執行處理,並將處理完的結果存儲到事件存儲中。 通常狀況下上面的方法可以運行得很好,若是咱們的微服務是RPC類的服務咱們須要更加當心,可能出現的問題在於,(1)過濾服務在業務處理完成後纔將事件結果存儲到事件存儲中,可是在業務處理完成前有可能就已經收到重複事件,因爲是RPC服務也不能依賴數據庫的惟一性約束;(2)業務服務的處理結果可能出現位置狀態,通常出如今正常提交請求可是沒有收到響應的時候。 對於問題(1)能夠按步驟記錄事件處理過程,好比事件的記錄事件的處理過程爲「接收」、「發送請求」、「收到應答」、「處理完成」。好處是過濾服務能及時的發現重複事件,進一步還能根據事件狀態做不一樣的處理。 對於問題(2)能夠經過一次額外的查詢請求來肯定事件的實際處理狀態,要注意額外的查詢會帶來更長時間的延時,更進一步可能某些RPC服務根本不提供查詢接口。此時只能選擇接收暫時的不一致,時候採用對帳和人工接入的方式來保證一致性。 須要注意的是上面的冪等處理方法要求事件必須有惟一的ID(這個ID通常是業務相關的),好比用ID來保證數據庫的惟一性約束;使用事件ID來確認事件是否已經被處理;使用事 件ID來查詢RPC服務的事件處理結果。