使用Datomic實現沒有麻煩的事件溯源

banq發表於2018-11-13

無論使用何種實現技術(EventStore / Kafka /SQL ......),“傳統事件溯源”方法會一些常見問題:

設計事件型別和事件處理程式是一項艱苦的工作
比如:你設計一個問答式的網站應用,那麼更新問題的正確事件型別應該是什麼,你在想是UserUpdatedQuestion?但也許這還不夠精細,它應該是更細粒度的UserUpdatedQuestionTitle嗎?你應該選擇更通用的UserUpdatedFieldOfEntity,但是Log會變得更難理解嗎?
6個月後,該系統進入生產環境,大客戶經理Tom闖入你的辦公室,提了一個新需求:“備受矚目的專家回答了一個流行的問題,應該可以換取500點聲望的特殊禮物;你能否在今晚實現這一目標?” 你想一會兒。沒有事件型別可以對聲譽進行特殊更改哇, “我很抱歉,”你回答。“現在,如果使用者沒有來自投票的事件,就不可能改變使用者的聲譽。我們需要進行具體的開發。”

在“一個資料庫總是反映當前狀態”方法的舊時代,你所要做的就是為你的狀態結構設計合適的表達方式,然後你就能透過查詢語言實現在該結構中導航的所有功能。例如,在關聯式資料庫中,你宣告一組表和列的結構,然後你就能透過SQL的所有功能來更改您儲存的資料。
使用傳統的事件溯源時,生活並不那麼容易,因為你必須預測狀態的每個更改,為其設計事件型別,併為此事件型別實現事件處理程式。
更重要的是,命名,粒度和語義在設計事件型別時很難做到 - 而且你最好在第一時間做到這一點,因為除非你重寫事件日誌,否則你的事件處理程式必須處理任何事件型別。程式碼庫的整個生命週期(因為重新處理整個日誌被認為是一個頻繁的操作)。太多的事件型別可能會導致更多的工作來實現事件處理程式; 另一方面,粗粒度事件型別不太可重用。

我認為這裡的教訓是,應用程式定義的事件型別的列舉是一種描述變化的弱語言。

檢測間接變化仍然很難

回到前面將問題評價與使用者聲譽聯絡起來的案例:
假設你正在為這個專案的DDD聚合根編寫事件處理程式,這樣才能跟蹤每個使用者的信譽得分:事件儲存是一個基本的鍵值儲存,它將每個使用者user_id與一個數字相關聯。 特別是,每次對問題進行投票時,都必須增加問題作者的聲譽。 問題是,在其當前形式中,UserVotedOnQuestion事件型別不包含提出問題的作者的user_id,只包含提出問題的ID。 

你該怎麼辦?

  • 您是否應更改UserVotedOnQuestion事件型別以使其明確包含問題作者的ID?但那會是多餘的,然後誰知道在你製作新的聚合時你還想要在事件型別中新增多少新東西呢?
  • 您是否應該更改聚合以便它還跟蹤問題 - >使用者之間的關係?但這會使它變得更加複雜,並且很可能需要多餘地其他聚合的互動......

事件日誌為您提供有關兩個個時間狀態點之間發生變化的精確運算元據; 但這並不意味著資料就很容易操作。要根據事件更新聚合,您需要預測事件是否以及如何影響聚合?在處理關係資訊模型時,事件可能是關於某個實體A並間接影響另一個實體B,但A和B之間的關係在事件中並不明顯; 在上面的示例中,事件型別UserVotedOnQuestion 會影響使用者實體而不直接引用它。 我們需要查詢功能來確定事件如何影響下游聚合,但事件日誌本身提供的查詢能力非常低。

有幾種策略可以緩解這個問題,所有這些都有重要的限制使用警告:
  1. 您可以對事件型別進行“非規範化”以向其新增更多資料,從而有效地對聚合進行一些預計算。這意味著生成事件的程式碼需要預測事件將被消費的所有方式 - 我們試圖擺脫對事件源的耦合。
  2. 您可以豐富每個聚合以跟蹤所需的關係資訊。這使得事件處理程式的實現更加複雜,並且可能是多餘的。
  3. 您可以新增“中間”聚合,該聚合僅跟蹤關係資訊並生成“豐富”事件流。這可能比上面的兩個解決方案都要好,但它仍然需要開發工作,並且仍然需要了解所有下游聚合的需求。


事務性很難實現
防止重複提交:你正在調查Q&A網站的一個錯誤:一些使用者建立了一個問題的2個答案,這不應該發生......的確,當使用者試圖建立答案時,程式碼透過QuestionsById聚合檢查這個使用者如果沒有建立此問題的答案,就應該沒有事件UserCreatedAnswer發出。
然後您意識到這是由事務的競爭條件引起的:在第一個答案新增到日誌的時間和它進入QuestionsById聚合的時間之間,第二個答案又被新增,而且會透過了檢查......
你自我覺得'很棒'。“我喜歡除錯併發問題。”

在這裡我們看到事務問題:事務幾乎與最終一致的寫入相容,這是在非同步處理事件日誌時預設獲得的。

可以透過使用與事件日誌同步更新的聚合來緩解此問題。這意味著新增事件不再像在佇列末尾附加資料記錄那麼簡單:您必須自動執行此操作,更新當前狀態,以便規則檢查時可查詢(例如,透過關聯式資料庫查詢)。

同樣重要的是要意識到事務不僅僅是允許事件進入日誌,而且還用於計算它們。例如,當您線上訂購演出門票時,票務系統必須查詢庫存併為您選擇一個座位號(即使只是為了將其新增到您的購物車,它必須以事務方式進行)。這導致我們區分命令和事件。

配置命令和事件
在傳統的事件源中,解決上述事務性問題的另一種常見方法是新增另一種事件,它們請求更改,但是不提交確認。例如,您可以新增一個UserWantedToCreateAnswer命令,稍後將由命令處理程式處理,命令處理程式將發出UserCreatedAnwser事件或EventCreationWasRejected事件兩個結果,並新增到日誌中; 這個命令處理程式當然需要維護一個聚合根Aggregate來跟蹤問題的建立。

這種方法的優勢在於可以讓您擺脫某些競爭條件,但卻增加了顯著的複雜性,處理事件現在是副作用的:不能是冪等的(多重複幾次呼叫的結果是不同的,多新增一個UserWantedToCreateAnswer命令會造成破壞性影響)。由於這些特殊的新事件應該只處理一次,因此在重新處理日誌時必須要小心。
最後,這意味著您正在強制對這些事件的生產者進行非同步工作流程,你只能回覆操作使用者:“嘿,感謝您提交此表單,遺憾的是我們不知道您的請求是否以及何時將被處理。請繼續關注!”

對我而言,這種複雜性源於這樣一個事實,即傳統的事件溯源誘使您忘記命令和事件之間的本質區別。關於這些概念的小型複習:

  • 一個命令是一個變更請求。它通常是以命令動詞方式中制定的(例如AddItemToCart)。你通常希望它們是短暫的並且只處理一次。
  • 一個事件,正如我們已經提到,描述了聚合根的狀態的變化的發生。它通常用過去時表達(例如ItemAddedToCart)。您通常希望它們持久儲存,並且可以根據需要進行多次處理。
  • 從這個角度來看,事務引擎是一個將命令轉換為事件的過程。

命令和事件起到非常不同的角色,這是毫不奇怪,他們混為一談導致的複雜性。

Datomic的模型
Datomic將資訊建模為事實的集合。每個事實都由Datom表示: Datom是一個5元組[entity-id attribute value transaction-id added?],其中:

  • entity-id 是一個整數,用於標識事實描述的實體(例如使用者或問題)(類似於關聯式資料庫中的行號)
  • attribute可能是類似:user_first_name或:question_author(類似於列在關聯式資料庫)
  • value是此實體的屬性的“內容”(例如"John")
  • transaction-id識別加入datom的事務(事務本身是一個實體)
  • added?是一個布林值,確定是否新增了資料(我們現在知道這個事實)或回退(我們不再知道這個事實)

例如,Datom datom [42 :question_title "What is Event Sourcing" 213130 true]可以用英語或中文翻譯:“我們從交易或事務編號213130中瞭解到,實體42是一個使用者提出的問題,該問題的標題是'什麼是事件溯源''。
Datomic Database Value表示系統在某個時間點的狀態,或者更準確地說是系統在某個時間點累積的知識。從邏輯角度來看,Database值只是Datoms的集合。例如,這是我們的問答資料庫的摘錄:

  [;; ...
   datom [38 :user_id "jane-hacker3444" 896647 true]
   ;; ...
   datom [234 :question_id "what-is-event-sourcing-3242599" 896770 true]
   datom [234 :question_author 38 896770 true]
   datom [234 :question_title "What is Event Sourcing" 896770 true]
   datom [234 :question_body "I've heard a lot about Event Sourcing but not sure what it's for exactly, could someone explain?" 896770 true]
   ;;
   datom [234 :question_title "What is Event Sourcing" 896773 false]
   datom [234 :question_title "What is Event Sourcing?" 896773 true]
   ;; ...
   datom [456 :answer_id uuid"af1722d5-c9bb-4ac2-928e-cf31e77bb7fa" 896789 true]
   datom [456 :answer_question 234 896789 true]
   datom [456 :answer_author 43 896789 true]
   datom [456 :answer_body "Event Sourcing is about [...]" 896789 true]
   ;; ...
   datom [774 :vote_question 234 896823 true]
   datom [774 :vote_direction :vote_up 896823 true]
   datom [774 :vote_author 41 896823 true]
   ;; ...
   ])


實際上,Datomic Database Value沒有實現為基本列表; 它是一個包含多個索引的複雜資料結構,允許使用Datalog(一種關係資料的查詢語言)進行表達和快速查詢。但從邏輯上講, Database Value只是一個資料列表。令人驚訝的是,這個非常簡單的模型允許查詢資料的效率不低於傳統資料庫(SQL /文件儲存/圖形資料庫/等)。
Datomic部署是一系列(不斷增長的)Database Value。寫入Datomic包括提交交易請求(表示我們想要應用的更改的資料結構); 此事務請求應用於當前Database Value,該值包括計算要新增到其中的一組資料(事務),從而產生下一個Database Value。
例如,更改問題標題的交易請求可能如下所示:
(def tx-request-changing-question-title
  [[:db/add [:question_id "what-is-event-sourcing-3242599"] :question_title "What is Event Sourcing?"]])

這將導致事務:

comment "Writing to Datomic"
  @(d/transact conn tx-request-changing-question-title)
  => {:db-before datomic.Db @3414ae14                       ;; the Database Value to which the Transaction Request was applied
      :db-after datomic.Db @329932cd                        ;; the resulting next Database Value
      :tx-data                                              ;; the Datoms that were added by the Transaction
      [datom [234 :question_title "What is Event Sourcing" 896773 false]
       datom [234 :question_title "What is Event Sourcing?" 896773 true]
       datom [896773 :db/txInstant inst "2018-11-07T15:32:54" 896773 true]]}
  )


現在我們開始看到Datomic與我們迄今為止已經列出的事件溯源概念之間的深刻相似之處:
  • 事務請求對應於命令
  • 事務或交易概念對應於事件
  • Datomic資料庫對應於事件日誌

我們也看到了一些重要的差異:
  • 事件由細粒度的Datoms組合而成; 沒有具有規定結構的事件型別。
  • 事件不是由應用程式程式碼直接生成的; 交易請求(命令)卻是的。


使用Datomic處理事件
首先,我們注意到Datomic Database Value可以被視為Aggregate; 一個同步維護而不需要額外努力的,包含儲存在事件中的所有資料,並且可以被明確地查詢。
這個Aggregate可能會涵蓋你的大部分查詢需求; 從我所看到的,新增下游聚合的最可能的用例是搜尋,低延遲聚合和資料匯出。
值得注意的是,您可以獲取Datomic資料庫的任何過去值,因此您可以開箱即用地重現過去的狀態 - 無需重新處理整個日誌:

(def db-at-last-xmas 
  (d/as-of db inst "2017-12-25"))


您可以使用Log API在2個時間點之間獲取事務:

(comment "Reading the changes between t1 and t2 as a sequence of Transactions:"
  (d/tx-range (d/log conn) t0 t1)
  => [{:tx-data [datom [234 :question_id "what-is-event-sourcing-3242599" 896770 true]
                 datom [234 :question_author 38 896770 true]
                 datom [234 :question_title "What is Event Sourcing" 896770 true]
                 datom [234 :question_body "I've heard a lot about Event Sourcing but not sure what it's for exactly, could someone explain?" 896770 true]
                 datom [896770 :db/txInstant inst "2018-11-07T15:32:09"]]}
      ;; ...
      {:tx-data [datom [234 :question_title "What is Event Sourcing" 896773 false]
                 datom [234 :question_title "What is Event Sourcing?" 896773 true]
                 datom [896773 :db/txInstant inst "2018-11-07T15:32:54"]]}
      ;; ...
      {:tx-data [datom [456 :answer_id uuid"af1722d5-c9bb-4ac2-928e-cf31e77bb7fa" 896789 true]
                 datom [456 :answer_question 234 896789 true]
                 datom [456 :answer_author 43 896789 true]
                 datom [456 :answer_body "Event Sourcing is about [...]" 896789 true]
                 datom [896789 :db/txInstant inst"2018-11-08T14:16:33.825-00:00"]]}
      ;; ...
      {:tx-data [datom [774 :vote_question 234 896823 true]
                 datom [774 :vote_direction :vote_up 896823 true]
                 datom [774 :vote_author 41 896823 true]
                 datom [896823 :db/txInstant inst"2018-11-08T14:19:31.855-00:00"]]}]
  )


請注意,儘管它們以非常小的形式描述了更改,但可以將事務與 Database Value結合使用,以直接的方式計算更改的效果。 您不再需要“豐富”您的事件以使其更容易處理; 它們已經透過Database Value豐富了。

我們仍然遇到傳統事件溯源的困難嗎?
好的,我們等著瞧瞧比較結果:

  • “設計事件型別和事件處理程式很難”:我們不再設計事件型別; 我們只設計我們的資料庫模式(它傾向於自然地對映到我們的領域模型),而Datomic將完成描述Datoms方面的變化的工作,這些變化可以通用地進行處理。對於描述不夠的少數情況,我們可以使用Reified Transactions對其進行擴充套件。關於事件處理程式,由於我們有足夠好的預設聚合(資料庫值),因此不再需要它們。
  • “檢測間接更改很難”:現在可以直接計算每個更改對下游聚合的影響,因為我們擁有具有高查詢能力的每個狀態轉換(事務和資料庫值)的增量檢視和全域性檢視。
  • “交易事務性很難實現”:沒有問題,Datomic完全是ACID,具有表達性的寫作語言。
  • “混淆命令和事件”:這裡沒有真正的混淆空間--Datomic不讓我們發出事件,我們只能用命令編寫。

當然,Datomic有侷限性,為了獲得這些好處,您必須確保這些限制對您的用例要求不會過高:
  • 寫入比例:不要指望在一個Datomic系統上每秒進行數萬次寫入。(讀取比例是可以的。資料庫水平擴充套件讀取,希望本文已經明確表示將讀取解除安裝到專門的狀態儲存很容易。)
  • 資料集大小:如果您需要儲存數PB的資料,則需要用其他方式補充或替換Datomic。
  • 資料模型:您的資料必須非常適合在Datomic中表示。Datomic的Universal Schema受到RDF的啟發,擅長在表格,文件或圖形資料庫中儲存的內容,但有一些想象力,您可能會想出一些難以在Datomic中表示的內容。(順便說一句,與流行的看法相反,Datomic 在表示歷史資料方面並不是特別擅長。)
  • 基礎設施: Datomic適合在大型伺服器上執行,通常在雲中執行 - 而不是在移動裝置或嵌入式系統上執行。
  • 專有: Datomic不是開源的,對某些人而言是需要破解。

結論
除了更改日誌之外,Datomic還提供每個更改產生的整個資料庫的可查詢快照(“狀態”),所有這些都由事務性寫入指示。這是一項重大的技術成就,這解釋了為什麼我們可以比傳統的Event Sourcing實現更少的工作量和限制來獲得Event Sourcing的好處。
在更傳統的CQRS術語中:Datomic為您提供了一種同步的表示式命令語言(Datomic事務請求),可操作事件(作為已新增資料集的事務)和強大的關係預設聚合(Datomic資料庫值)。
希望這表明事件溯源不必像我們已經習慣的那樣苛刻,只要我們願意重新考慮我們應該如何實施它的假設。

HackerNews的討論
 

相關文章