Java反應式事件溯源之第 2 部分:Actor 模型
本節我們將解決併發訪問的問題。. 我們的域程式碼非常優雅,但即使我們使用記錄和一些不可變集合,它在多執行緒環境中也不是完全安全的。比如我們要實現在同時預定同一個座位的情況下,一個請求成功一個失敗的保證。如何實施?在大多數情況下,您將在資料庫級別引入某種樂觀(或悲觀)鎖定。在這種情況下,您本身並沒有處理併發。您寧願將此責任轉移到資料庫。這對於中等負載的標準系統來說很好,但我們正在構建一個應該易於擴充套件的反應式解決方案。此外,您僅限於支援此類鎖定的資料庫,但情況並非總是如此。幸運的是,還有其他選項可以處理併發。它'實現Actor Model 的[url=https://doc.akka.io/docs/akka/current/typed/actors.html]Akka 堆疊[/url]。
Actor
Actor是一個非常簡單但聰明的抽象,將幫助您輕鬆構建非常複雜的併發系統。您可以將其視為您所在州的保護者。如果您想從多個執行緒訪問您的狀態,以一種安全的方式,將其包裝在 actor 中並只與 actor 對話。只有通過傳送訊息才能與參與者進行通訊。每個參與者都有自己的訊息框,它一次會消費一條訊息。因此,通過正確的實現,2 個或更多併發執行緒無法對您的狀態執行某些操作。
此外,來自 Akka的永續性參與者(也稱為事件源參與者)能夠以事件的形式持久化其狀態。使用新的型別化 Actor API,您可能還會遇到entity. 它與sharding一起使用,當你想將你的Actor分佈在不同的節點上時。這可能會讓人感到困惑,所以在這一點上,每當您看到 Akka Entity 時,請記住,這實際上是一個Actor。當然,我現在跳過了很多細節,但是為了這一部分,我認為應該足以瞭解發生了什麼。
將所需的依賴項新增到pom.xml後,您就可以建立您的第一個 actor。我們將只關注型別化 API。使用它更安全,建議將其作為預設選擇。請注意,您仍然可以使用舊的經典 API。在功能方面,基本相同,但如果這是您第一次使用 Akka,請堅持使用型別化 API。犯一些愚蠢的錯誤會更難,編譯器會為你檢查很多東西。
要建立一個參與者或實體,我們需要定義它的行為。行為是封裝參與者契約的抽象。如果您向我傳送此訊息,我會回覆,或者根本不回覆,這取決於您想要實現的目標。讓我們檢查一下並實現一個AdderActor.
record Add(int a, int b, ActorRef<Integer> replyTo) { } public class AdderActor extends AbstractBehavior<Add> { public AdderActor(ActorContext<Add> context) { super(context); } @Override public Receive<Add> createReceive() { return newReceiveBuilder().onMessage(Add.class, add -> { int result = add.a() + add.b(); add.replyTo().tell(result); return Behaviors.same(); }).build(); } } |
我們需要擴充套件AbstractBehavior,並將其與我們的輸入訊息Add一起輸入。輸入的訊息是一條記錄,其中包含ints a和b,以及對需要回復結果的詢問行為體的引用。處理這樣一個訊息是非常簡單的。在訊息的新增上,首先,用結果進行回覆(告訴方法),然後返回相同的行為。所有的東西都是型別化的,所以沒有辦法用例如String來回復,編譯器會為你檢查合同。
Event sourced行為
使用來自 Akka 庫的更高階別的抽象,對於事件溯源,有一種特殊的行為,稱為EventSourcedBehaviour。因為我們想為每條訊息傳送一個回覆,我們可以強制編譯器使用EventSourcedBehaviorWithEnforcedReplies. 事件源行為使用 3 種其他型別引數化:Command、Event、State。看起來很眼熟,你不覺得嗎?但是,這裡有一個小的變化。我們不使用我們的域命令,而是使用一個包含域命令和回覆所需的Actor引用的信封ShowCommandEnvelope。
public sealed interface ShowEntityCommand extends Serializable { record ShowCommandEnvelope(ShowCommand command, ActorRef<ShowEntityResponse> replyTo) implements ShowEntityCommand { } } |
為什麼它有用呢?首先,我們的領域沒有被Akka庫觸及,其次,我們以後會新增一些與我們的領域無關的命令。響應也是用ShowEntityResponse來型別化的。
public sealed interface ShowEntityResponse extends Serializable { final class CommandProcessed implements ShowEntityResponse { } record CommandRejected(ShowCommandError error) implements ShowEntityResponse { } } |
我們可以回覆該命令已被處理或該命令因某些錯誤被拒絕。這就是我們的第一個事件源行為契約。
擴充套件EventSourcedBehavior*類需要實現3個方法。
@Override public Show emptyState() {...} @Override public CommandHandlerWithReply<ShowEntityCommand, ShowEvent, Show> commandHandler() {...} @Override public EventHandler<Show, ShowEvent> eventHandler() {...} |
ReplyEffect
因為我們只有一個命令,所以commandHandler的實現非常簡單。
public CommandHandlerWithReply<ShowEntityCommand, ShowEvent, Show> commandHandler() { return newCommandHandlerWithReplyBuilder().forStateType(Show.class) .onCommand(ShowEntityCommand.ShowCommandEnvelope.class, this::handleShowCommand) .build(); } |
有趣的部分是從handleShowCommand方法開始的。
private ReplyEffect<ShowEvent, Show> handleShowCommand(Show show, ShowEntityCommand.ShowCommandEnvelope envelope) { ShowCommand command = envelope.command(); return show.process(command, clock).fold( error -> { return Effect().reply(envelope.replyTo(), new CommandRejected(error)); }, events -> { return Effect().persist(events.toJavaList()) .thenReply(envelope.replyTo(), s -> new CommandProcessed()); } ); } |
這是我們將我們的事件源庫與事件源域合併的地方。
要做到這一點,我們需要處理來自信封的域命令。有兩種可能的結果。在出錯的情況下,效果只是回覆發件人說命令被拒絕。如果命令處理成功,我們將得到一個事件列表。
如前文所述,在我們用CommandProcessed訊息回覆之前,我們需要儲存這些事件。
程式碼中沒有明確說明的是(雖然有很好的記錄),對於所有的持久化事件,將呼叫eventHandler方法,然後Akka Persistence將執行thenReply部分。由於EventSourcedBehaviorWithEnforcedReplies,編譯器會檢查我們是否返回ReplyEffect而不是簡單的Effect。
為什麼要在引擎蓋下呼叫eventHandler?因為這樣,你就不需要記住在所有的命令處理程式中使用它。另外,在恢復實體的過程中,當你需要載入給定聚合的所有事件並逐一應用它們時,這個方法會被呼叫。
public EventHandler<Show, ShowEvent> eventHandler() { return newEventHandlerBuilder() .forStateType(Show.class) .onAnyEvent(Show::apply); } |
在這種情況下,實現更加直接。我們只是將所有事件重定向到Show::apply域方法。
擴充套件EventSourcedBehavior*不僅為我們提供了處理命令、持久化和應用事件的良好抽象。它還以被動的方式保護狀態免受併發訪問。參與者執行緒不會被與資料庫的通訊阻塞。它將在底層事件儲存響應後立即恢復工作。從那時起,它將使用更復雜的Stash機制版本緩衝所有命令。這基本上是一個的實現單個寫入器的原理,建立真正的高效能一致的系統。
實體測試
好的,是時候執行它並測試它了。要建立一個Actor,您需要一個Actor系統。Actor系統就像Actor的“家”。必須管理所有參與者、它們的生命週期、排程策略等。您可以將其視為管理 Spring Bean 的 Spring Context。
對於測試,您不必手動建立 Actor System。您可以使用非常方便的實用程式,例如ActorTestKit某些單元測試配置覆蓋的某些預設配置。不要忘記在所有測試後將其關閉。
private static final ActorTestKit testKit = ActorTestKit.create(EventSourcedBehaviorTestKit.config().withFallback(UNIT_TEST_AKKA_CONFIGURATION)); @AfterAll public static void cleanUp() { testKit.shutdownTestKit(); } |
為了測試我們的事件源行為,至少有 2 種策略。
白盒測試
使用EventSourcedBehaviorTestKit的白盒測試非常好,因為您可以斷言任何您想要的東西。不僅是響應,還有持久化的事件,以及actor內部的狀態。
@Test public void shouldReserveSeat() { //given var showId = ShowId.of(); EventSourcedBehaviorTestKit<ShowEntityCommand, ShowEvent, Show> showEntityKit = EventSourcedBehaviorTestKit.create(testKit.system(), ShowEntity.create(showId, clock)); var reserveSeat = randomReserveSeat(showId); //when var result = showEntityKit.<ShowEntityResponse>runCommand(replyTo -> toEnvelope(reserveSeat, replyTo)); //then assertThat(result.reply()).isInstanceOf(CommandProcessed.class); assertThat(result.event()).isInstanceOf(SeatReserved.class); var reservedSeat = result.state().seats().get(reserveSeat.seatNumber()).get(); assertThat(reservedSeat.isReserved()).isTrue(); } |
黑盒測試
第二種策略是黑盒方法,您可以在其中與參與者互相呼叫並僅檢查響應。Actor內部發生的事情對你是隱藏的。這種策略更接近Actor的生產使用。要從參與者那裡獲得響應,您需要使用稱為 testing 的probe,它將模擬傳送者參與者。
//given var showId = ShowId.of(); var showEntityRef = testKit.spawn(ShowEntity.create(showId, clock)); var commandResponseProbe = testKit.<ShowEntityResponse>createTestProbe(); var reserveSeat = randomReserveSeat(showId); //when showEntityRef.tell(toEnvelope(reserveSeat, commandResponseProbe.ref())); //then commandResponseProbe.expectMessageClass(CommandProcessed.class); |
行為設定
到目前為止,我沒有涉及的一件事是ShowEntity.create方法。
public static Behavior<ShowEntityCommand> create(ShowId showId, Clock clock) { return Behaviors.setup(context -> { PersistenceId persistenceId = PersistenceId.of("Show", showId.id().toString()); return new ShowEntity(persistenceId, showId, clock, context); }); } |
要建立一個Actor或實體,無論是通過 testKit.spoon 還是 EventSourcedBehaviorTestKit.create 方法,我們都需要傳遞 Behavior[T]。為此,我們使用了一個靜態工廠方法。這個方法正在呼叫我們的私有建構函式,並用Behaviors.setup對其進行包裝,我們可以返回Behavior[ShowEntityCommand]。PersistenceId(傳遞給建構函式)是一個鍵,我們將在這個鍵下儲存所有的事件,對於一個給定的集合,在資料庫中(這是下一課的主題)。
如果這讓你感到困惑——別擔心。花點時間,分析程式碼,執行測試,閱讀日誌。
與大多數教程相比,領域與技術(Actor)的東西是分開的。這種分離使您能夠在將來從 Akka Persistence 切換到其他東西。域將保持不變。這很可能永遠不會發生,但最好在程式碼庫中分離關注點。通過這種方式新增新功能和改進應用程式將更加愉快。
請檢視part_2標記中的完整原始碼。
相關文章
- Java反應式事件溯源之第 4 部分:控制器Java事件
- Java反應式事件溯源之第5部分:事件儲存Java事件
- Java反應式事件溯源之第3部分:服務Java事件
- Java反應式事件溯源:領域Java事件
- 事件消費者之 Projector - 事件溯源事件Project
- 事件消費者之 Reactor - 事件溯源事件React
- .NET分散式Orleans - 6 - 事件溯源分散式事件
- 事件消費者之 Saga - 事件溯源事件
- 《反應式應用開發》之“什麼是反應式應用”
- 【Akka】Actor模型探索模型
- 事件協作和事件溯源事件
- 事件流與事件溯源事件
- PHP 事件溯源PHP事件
- 剖玄析微聚合 - 事件溯源事件
- 雲端計算服務模型,第 2 部分: 平臺即服務(PaaS)模型
- Jetlang是Java高併發Actor模型開源庫包Java模型
- Linux伺服器應急事件溯源報告Linux伺服器事件
- 事件溯源全指南 - Arkwrite事件
- 事件溯源不是什麼?事件
- Java XML和JSON:Java SE的文件處理 第2部分JavaXMLJSON
- 使用 React.js 的漸進式 Web 應用程式:第 2 部分 – 頁面載入效能ReactJSWeb
- 使用 React.js 的漸進式 Web 應用程式:第 2 部分 - 頁面載入效能ReactJSWeb
- Lite Actor:方舟Actor併發模型的輕量級優化模型優化
- 事件溯源將顛覆關聯式資料庫! - Remy事件資料庫REM
- 事件溯源超越關聯式資料庫 - confluent事件資料庫
- Java反應式框架Reactor中的Mono和FluxJava框架ReactMonoUX
- 關於Actor模型的實現模型
- 使用EventStoreDB實現事件溯源的Java開源專案事件Java
- 三分鐘掌控Actor模型和CSP模型模型
- mysql performance schema 第2部分MySqlORM
- 使用Kafka實現事件溯源Kafka事件
- Rust中的事件溯源 - ariseyhunRust事件
- 輕鬆學習 JavaScript——第 2 部分:函式中的 Rest 引數JavaScript函式REST
- Laravel 模型事件的應用Laravel模型事件
- 測開之函式進階· 第2篇《純函式》函式
- Spring 事務管理高階應用難點剖析: 第 2 部分Spring
- 用Java構建反應式REST API - Kalpa SenanayakeJavaRESTAPINaN
- 如何在Java後端中實現事件驅動架構:從事件匯流排到事件溯源Java後端事件架構