Java反應式事件溯源之第5部分:事件儲存

banq發表於2022-01-23

選擇事件儲存資料庫需要大量研究。這可能是一個單獨的部落格系列的主題(可能在將來),所以我的計劃是從不同的角度來解決它。不要試圖找到最佳解決方案(因為恕我直言不存在)。相反,嘗試找到一個最佳的折衷方案併為改變做好準備。

使用Akka Persistence有一個巨大的優勢,即永續性只是一個外掛。我們可以將此庫與任何能夠履行AsyncWriteJournal合同的事件儲存一起使用。我們只需要實現 4 種方法來使用我們的自定義事件儲存解決方案:

@Override
public Future<Iterable<Optional<Exception>>> doAsyncWriteMessages(Iterable<AtomicWrite> iterable) {...}

@Override
public Future<Void> doAsyncDeleteMessagesTo(String s, long l) {...}

@Override
public Future<Void> doAsyncReplayMessages(String s, long l, long l1, long l2, Consumer<PersistentRepr> consumer) {...}

@Override
public Future<Long> doAsyncReadHighestSequenceNr(String s, long l) {...}

SnapshotStoreReadJournal的讀取模型投影的情況類似。

我並不是說實現自定義 Akka Persistence 外掛是一項微不足道的任務。這將是一個相當大的挑戰,但至少是可能的(這是我同事的例子)。我們在未來不受今天在當前背景下做出的選擇的限制。此外,我們可以推遲一些困難的決定,例如使用分散式事件儲存(例如 Cassandra)還是使用單個主機(例如 PostgreSQL、MariaDB)。

對於初學者,有可用的外掛,例如:

 

JDBC

經典的關聯式資料庫適用於很多專案。我想大多數開發人員對這種資料庫都有一些經驗。要為我們的事件源行為啟用Akka Persistence JDBC 外掛,您所要做的就是新增適當的依賴項和一些配置:

akka{
  persistence.journal.plugin = "jdbc-journal"
}

jdbc-journal {
  slick = {
    profile = "slick.jdbc.PostgresProfile$"
    db {
      host = "localhost"
      port = "5432"
      dbName = "postgres"
      url = "jdbc:postgresql://"${slick-akka.db.host}":"${slick-akka.db.port}"/"${slick-akka.db.dbName}"?reWriteBatchedInserts=true"
      user = "admin"
      password = "admin"
      driver = "org.postgresql.Driver"
    }
  }
}

在我們執行docker-compose -f development/docker-compose-jdbc.yml起來之後,我們可以啟動我們的應用程式,提出一些請求,並驗證我們的關係型事件儲存在event_journal表中包含一些事件。不要對event_payload列感到驚訝,因為我們的事件是以二進位制blob的形式儲存的。對於原型設計,我們使用的是Java序列化機制,當然這對於生產部署來說是一個非常糟糕的主意,應該改變。在這一部分,我們將不關注序列化本身(這需要一個單獨的帖子),但如果你想閱讀更多關於可能的選擇,只需閱讀我關於事件源的序列化策略的帖子。

這樣一個解決方案的反應性如何?與事件儲存進行通訊的JDBC驅動是一個阻塞式的。的確,在寫這篇文章的時候,R2DBC(反應式JDBC)驅動還不被Akka Persistence支援。我希望這在未來會有所改變。幸運的是,JDBC驅動被一個Slick庫包裹著,它給了我們一個相當不錯的、可管理的(一個獨立的執行緒池)反應式門面。不要忘了,Actor不會因為與底層事件儲存的互動而被阻斷。

 

Cassandra

在某種程度上,單個主機資料庫是不夠的。垂直縮放將達到其極限並處理更多負載,我們將需要切換到分散式的東西。雖然,並非所有分散式資料庫都會提高吞吐量。使用leader-follower 架構(例如MongoDB),我們仍然會受到單個主機(leader)的限制。我們應該關注像 Apache Cassandra 這樣的無領導者解決方案。從寫的角度來看,Cassandra 是事件儲存的絕配,我們會得到:

  • 分割槽(資料分佈在叢集中的所有節點上),
  • 複製(如果節點發生故障,我們可以使用副本),
  • 針對寫入進行了優化(吞吐量非常好),
  • 近線性水平縮放。

如果你曾經使用過 Cassandra,你應該知道模式建模是這個資料庫的關鍵。Akka 永續性 Cassandra外掛意識到了這一點,用於儲存事件的表模式就像一個魅力。分割槽不會太小,但也不會太大(實際上,您可以配置單個分割槽應該有多少事件)。使用這樣的模式,讀取給定聚合的事件將非常有效(對於恢復階段很重要)。通訊的反應式驅動器將非常適合我們的設計。任何問題?當然 :) 在生產環境中維護分散式事件儲存與單個主機資料庫完全不同。這將需要大量的 DevOps 能力和知識(或者在託管解決方案的情況下需要資金)。此外,請注意,從所有聚合中讀取所有事件可能會帶來一些挑戰。我想在某種程度上,我們根本沒有選擇。如果我們想進一步擴充套件,

好訊息是我們不必在實現中進行任何更改即可使用 Cassandra。和以前一樣,我們只需要一些依賴項和配置:

akka {
  persistence.journal.plugin = "akka.persistence.cassandra.journal"
}
akka.persistence.cassandra.journal {
  keyspace-autocreate = true //not recommended for production
  tables-autocreate = true //not recommended for production
}

使用part_5原始碼中的 Cassandra 外掛執行我們的應用程式,我們需要-Dconfig.resource=/application-cassandra.conf在 Intellij 啟動配置中新增 VM 選項或直接從命令列執行它:

./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="--enable-preview -Dconfig.resource=/application-cassandra.conf"

 

概括

我希望你能感受到 Akka Persistence 的這一無可置疑的優勢,我們可以使用記憶體中的事件儲存進行原型設計,然後在生產環境中切換到 JDBC 事件儲存,一旦你遇到一些重大負載,就切換到像 Apache Cassandra 這樣的分散式事件儲存. 對我來說,這改變了遊戲規則。我可以把非常困難的決定推遲到最後一刻。我還應該提到,我們可以為每個聚合型別配置事件儲存。對於會產生數千個事件的長期聚合,例如交易所市場、物聯網,我可以使用 Apache Cassandra,但對於具有少量事件的聚合,我可以使用 JDBC 外掛。

在使用完全不同的資料庫時,肯定會有一些特定的陷阱和陷阱。事件遷移將需要一些工作,這將是一個過程。關係事件儲存上的模式與 Apache Cassandra(或任何其他分散式資料庫)上的模式完全不同,但至少是可能的。我們將來不會被阻止,從一個資料庫到另一個資料庫的神話般的切換可能是真正的交易。這是可能的,因為使用事件溯源,我們不必太關心資料之間的關係。我們需要儲存一個事件流並讀取事件流,這可以在許多資料庫引擎上進行模擬。使用 Actor 模型,我們可以使用(分散式)單寫原則,並安全地使用不提供 ACID 事務保證的資料庫。

如果您想將 Akka 用於 CRUD 功能,還有一個具有持久狀態永續性的選項。

像往常一樣 - 檢視part_5原始碼並使用該應用程式。

 

相關文章