Eventsourced 使用指南
Revision History | |
---|---|
Revision 0.2 | 2013-12-31 |
本文是EventSourced庫使用指南的中文譯稿,基於EventSourced 0.6.0。
Eventsourced 庫爲Actor 的持久化提供了可縮放的解決方案,同時爲基於 Akka 的消息傳遞提供了“ 至少一次 ”的傳輸保證。使用 Eventsourced Actor 可以做到:
-
通過追加一條記錄到日誌來對消息進行持久化
-
應用收到消息並進行處理從而產生其當前的狀態
-
通常當前狀態的狀態保存在內存中(內存鏡像)
-
通過重播接收到的消息(在應用程序正常啓動或崩潰後)恢復當前(或歷史)的狀態
-
從不直接保存當前的狀態(除了主動保狀態快照,有快照的話恢復時間更短)
換句話說,Eventsourced 實現了預寫日誌(write-ahead log ,WAL)用於跟蹤一個Actor 所接收消息,並通過回放記錄的消息來恢復其狀態。附加信息記錄到日誌,而不是直接持久化Actor狀態,可以達到非常高的事務處理速度,並支持高效的複製。相對於其他 WAL-based 系統, Eventsourced 通常在日誌中保存整個消息歷史記錄,也可以選用狀態快照。
每個記錄的消息代表一個改變一個Actor 狀態的意圖。記錄變更,而不是更新當前狀態的是event sourcing的核心理念之一 。Eventsourced 庫可以用來實現 event sourcing 的概念,但它不侷限於此。關於Eventsourced 庫和 event sourcing 理論的關係可以讀這篇文章,如何使用Eventsourced持久化Actor的狀態,以及它與 event sourcing 理論的關係。
Eventsourced 也可以用來讓Actor相互進行可靠之間的消息交換,以便他們在崩潰後可以恢復。爲此目的,引入"信道 (Channel)" ,它提供了最少一次的消息傳輸保證。在日誌消息回放時, 信道也用來防止持久化Actor的輸出消息。因爲正常工作時,這些消息已經發送給了相關的其他對象或服務,回放時再發送就是多餘的了。
由Eventsourced提供的核心構建模塊是處理器(processors),信道(channel)和日誌(journal)。它們由一個 Akka 的擴展 EventsourcingExtension 進行管理。
處理器是一個有狀態的Actor,它將收到的消息記錄在日誌裏(持久化)。在構造時給一個有狀態的Actor混入一個Eventsourced 特質(trait),它就變成了一個處理器。當然,處理器本身就是Actor,仍然可以像使用Actor一樣使用其原本的功能。
包裝在Message對象中的消息會被處理器作爲日誌記錄下來,沒有包裝的消息不回被記錄。在本指南中被包裝的消息通常稱爲事件(events) 。包裝的消息也可以是命令(Commands) ,見下文 Application。
日誌記錄通過Eventsourced特質來實現,處理器的receive方法並不需要關心這一點。通過向發送者發送一個應答來確認消息成功收到。處理器也可以在動態改變行爲 (Actor可以用become/unbecome變更其對收到消息的處理邏輯。--譯註) 時仍保持其日誌記錄功能。
處理器向 EventsourcingExtension 註冊。這個 Akka 擴展提供了一些方法通過重放日誌記錄的消息來恢復處理器狀態。在應用程序運行過程中的任何時候處理器都可以進行註冊和恢復動作。
Eventsourced特質不會對處理器如何維護自己的狀態強加任何限制。處理器記錄狀態可以使用 var ,可變數據結構或STM引用(軟件事務內存)等等。
處理器使用信道是將消息發送到其他 Actor(信道目的地),並從他們那裏收到應答。信道具有以下特徵:
-
要求目的地(消息接收者)確認收到的消息,信道基於此來提供“最少一次”的傳遞保證(顯式 ACK 重試協議)。確認應答會寫入日誌。
-
處理器恢復時(通過消息重播)防止將消息重複傳遞給目的地。信道會將重播的消息和它之前收到的應答進行匹配, 匹配上的消息會被丟棄(說明已經被目的地收到,不必再發--譯註)
信道本身也是一個Actor,它與一個目的地向關聯,提供上述功能。處理器通常將信道創建爲子Actor,同時給它一個指向目的地的 actor 引用。
處理器也可以不使用的信道,直接發送消息到另一個Actor。在這種情況下,在處理器恢復時目的地 Actor 會收到重複的消息。
Eventsourced提供了三種不同的信道類型(計劃還有更多):
-
默認信道
-
不存儲接收到的消息。
-
僅在發送處理器恢復時重新發送未確認的消息。
-
在發送失敗的情況下,不保證消息的發送順序。
-
-
可靠信道
-
存儲接收到的消息
-
基於可配置重發策略重發未確認消息。
-
保證消息發送的順序,即使中間是失敗情況。
-
經常被用來對付不可靠的遠程目標。
-
可以從 JVM 崩潰中恢復
-
-
可靠的“請求-應答”信道
-
除了具有上述可靠信道的能力外,但還保證應答“最少一次”傳遞。
-
不保證應答順序和和消息的發送順序一致。
-
Eventsourced信道並不意味着替換任何現有的消息系統,相反它可以利用現有的消息系統。例如,如果需要的好,可以可靠地將處理器連接到已有的系統中。更一般地講,如果要把處理器與其他其它服務集成在一起,信道就非常有用,更多描述見 這篇文章 。
日誌也是一個Actor,處理器和信道使用它來記錄log.日誌提供的服務質量(可用性,可伸縮性,...)取決於所用的存儲技術。 下文多種日誌一節會給出了現有的日誌實現及其開發狀況的概述。
對於Message所包裝的實際數據,Eventsourced庫並沒有在數據結構和語義上強加任何限制。因此,持久性消息可以是事件以及命令。兩者都可以看做應用程序與其環境交互的方式。這在Eventsourced 的 reference application中有演示,該示例持久化事件以及命令。對應需要長期運行,具有長週期業務處理(有時也被稱爲sagas )來說,這也簡化了其實現。例如,可以把這些處理邏輯做成一些處理器,他們通過向外發出命令來對事件做出響應,命令的接收者可以是其他的處理器或外部服務。 Eventsourced 與實現CQRS模式,並與遵循領域驅動設計(DDD)(見參考應用程序 )的應用配合的非常好。在另一方面,它也不強制應用程序遵循CQRS和/或DDD,沒有CQRS和/或DDD應用程序一樣可以實現 event sourcing 機制。
對於消息的持久化,Eventsourced目前提供了以下日誌機制的實現:
Table 1.
日誌機制 | 開發狀態 |
LevelDB 日誌 。 它可以配置爲使用本地 LevelDB (通過訪問leveldbjni )或LevelDB 的 Java 接口 作爲存儲後端。從運行 SBT 本地 LevelDB 需要特殊設置 。本用戶指南中的所有例子都使用了 LevelDB 的 Java 接口。 | 生產 |
HBase 的日誌 。一個HBase 的 爲後端存儲的日誌,具有高可用性,橫向的讀 / 寫的縮放新,併發和非阻塞讀取和寫入。 詳情在這裏 。 | 試驗 |
基於 MongoDB Cabash 的日誌 。一個後端存儲爲MongoDB 的 備份日誌。 詳情在這裏 。感謝Duncan DeVore 。 | 試驗 |
基於 MongoDB Reactive 的日誌 。 一個MongoDB 的 備份日誌。詳情在這裏 。感謝DuncaDuncan DeVore。 | 試驗 |
DynamoDB 日誌 。一個DynamoDB 爲後端存儲的日誌。 詳情在這裏 。感謝Scott Clasen 。 | 試驗 |
基於 Journal.IO 的日誌 。 僅用於測試。 消息持久化。 | 測試 |
內存日誌 。 內存中的日誌用於測試目的。消息不持久。 | 測試 |
本節將示範創建,使用和回收的event-sourced 的Actor 所需的最少步驟,並演示了信道的使用。從本節代碼包含在FirstSteps.scala和FirstSteps.java 。它可以從 SBT 提示與執行。
Scala:
> project eventsourced-examples
> run-main org.eligosource.eventsourced.guide.FirstSteps
Java:
> project eventsourced-examples
> run-main org.eligosource.eventsourced.guide.japi.FirstSteps
EventsourcingExtension是由 Eventsourced 庫提供的 Akka 擴展。應用程序用它來
-
創建和註冊 event-sourced Actor(稱爲處理器或事件處理器)
-
創建和註冊信道
-
從日誌事件消息恢復註冊的處理器和信道
Scala:
import java.io.File import akka.actor._ import org.eligosource.eventsourced.core._ import org.eligosource.eventsourced.journal.leveldb._ val system: ActorSystem = ActorSystem("example") val journal: ActorRef = LeveldbJournalProps( new File("target/example-1"), native = false).createJournal val extension: EventsourcingExtension = EventsourcingExtension(system, journal)
Java:
import java.io.File; import akka.actor.*; import org.eligosource.eventsourced.core.*; import org.eligosource.eventsourced.journal.leveldb.*; final ActorSystem system = ActorSystem.create("guide"); final ActorRef journal = LeveldbJournalProps.create( new File("target/guide-1-java")).withNative(false).createJournal(system); final EventsourcingExtension extension = EventsourcingExtension.create(system, journal);
此示例使用一個LevelDB 日誌 ,但其他任何日誌實現也可用。
使用Scala 的API ,event-sourced 的參與者可以定義爲標準的 Actor 。使用 Java 的 API ,event-sourced的Actor 需要擴展抽象UntypedEventsourcedActor類。 舉個例子,
Scala:
class Processor extends Actor { var counter = 0 def receive = { case msg: Message => { counter = counter + 1 println("[processor] event = %s (%d)" format (msg.event, counter)) } } }
Java:
public class Processor extends UntypedEventsourcedActor { private int counter = 0; @Override public int id() { return 1; } @Override public void onReceive(Object message) throws Exception { if (message instanceof Message) { Message msg = (Message)message; counter = counter + 1; System.out.println(String.format("[processor] event = %s (%d)", msg.event(), counter)); } } }
這個Actor計算其收到的消息 (Message ) 數量。在 Eventsourced 應用,事件總是通過Message對象進行傳送。
爲了讓Scala Processor成爲一個event-sourced Actor ,它必須在實例化時混入Eventsourced 特質。Java 的 Processor已經擴展UntypedEventsourcedActor 類,所以沒有進一步的修改是必要的 。
Scala:
// create and register event-sourced processor val processor: ActorRef = extension.processorOf(Props(new Processor with Eventsourced { val id = 1 } )) // recover registered processors by replaying journaled events extension.recover()
Java:
// create and register event-sourced processor final ActorRef processor = extension.processorOf(Props.create(Processor.class), system); // recover registered processors by replaying journaled events extension.recover();
一個Actor混入Eventsourced特質(或繼承 UntypedEventsourcedActor )後,當它的receive方法(或onReceive方法)被調用之前,事件Message已經被計入日誌。processorOf方法使用一個唯一id將Actor註冊成爲處理器 。該處理器id定義實現Eventsourced.id這抽象成員。id必須爲正整數,在應用運行期間始終會用它來標誌這個處理器。recover方法通過回放應用運行期間processor收到的所有消息來恢復其狀態 。
作爲Actor ,event-sourced處理器可以像任何其他的Actor一樣使用。Message類型的消息會被被寫入日誌, 處理器直接收取其他任何類型的消息不會被記入日誌 。
Scala:
//發送Meaage類型事件給處理器(將會記入日誌) processor ! Message("foo")
Java:
//發送事件消息處理器(將會記入日誌) processor.tell(Message.create("foo"), null);
應用程序的首次運行會創建一個空的日誌。因此,沒有事件消息會被重播, processor 輸出 :
[processor] event = foo (1)
到stdout。當應用程序重新啓動,但是,processor的狀態將被重播先前日誌化事件消息恢復。然後,應用程序發送另一個事件消息。因此,你會在標準輸出看到:
[processor] event = foo (1) [processor] event = foo (2)
其中第一個println 是由一個重播事件消息觸發。
在該步驟中,我們擴展 event-sourced 處理器 ,讓他發送一個 Message 給 destination 。處理器創建另一個新的 Message (通過創建收到事件的一個副本) ,更新其 event 字段,再將新創建的消息發送給目標 。
Scala:
class Processor(destination: ActorRef) extends Actor { var counter = 0; def receive = { case msg: Message => { counter = counter + 1 // ... destination ! msg.copy(event = "processed %d event messages so far" format counter) } } } val destination: ActorRef = system.actorOf(Props[Destination]) //通過將目標作爲構造函數的參數實例化處理器 val processor: ActorRef = extension.processorOf(Props(new Processor(destination) with Eventsourced { val id = 1 } )) extension.recover()
Java:
public class Processor extends UntypedEventsourcedActor { private ActorRef destination; private int counter = 0; public Processor(ActorRef destination) { this.destination = destination; } @Override public int id() { return 1; } @Override public void onReceive(Object message) throws Exception { if (message instanceof Message) { Message msg = (Message)message; counter = counter + 1; // ... destination.tell(msg.withEvent(String.format("processed %d event messages so far", counter)), getSelf()); } } } final ActorRef destination = system.actorOf(Props.create(Destination.class)); //通過將目標作爲構造函數的參數實例化處理器 final ActorRef processor = extension.processorOf(Props.create(Processor.class, destination), system); extension.recover();
如果不採取進一步的行動,處理器在恢復過程中也會將Message再次發送給destination。每一次應用程序重新啓動, destination將一次又一次的重複收到了整個事件消息歷史記錄。在大多數情況下,這是不能接受的。比如destination 是一個外部服務 。
爲了防止重複消息傳遞到destination ,我們需要記住哪些消息已成功發送。這也正是信道 的用武之地 。信道會丟棄所有已經成功發送到目的地的消息。因此,我們將destination包裝在一個信道內,讓處理器通過該信道與目標進行通信。不必修改Processor的代碼就能完成這些處理。
Scala:
val destination: ActorRef = system.actorOf(Props[Destination]) //將目的地包裝在信道內 val channel: ActorRef = extension.channelOf(DefaultChannelProps(1, destination)) //處理器初始化時將信道(已經內置了目的地)作爲構造參數傳進去。 val processor: ActorRef = extension.processorOf(Props(new Processor(channel) with Eventsourced { val id = 1 } ))
Java:
final ActorRef destination = system.actorOf(Props.create(Destination.class)); //將目的地包裝在信道內 final ActorRef channel = extension.channelOf(DefaultChannelProps.create(1, destination), system); //處理器初始化時將信道(已經內置了目的地)作爲構造參數傳進去。 final ActorRef processor = extension.processorOf(Props.create(Processor.class, channel), system);
一個信道必須有一個唯一的id(在這個例子中是1),一個正整數,必須在應用程序運行期間始終定義一致。在這裏,我們創建了一個默認的信道配置了一個DefaultChannelProps配置對象。如果應用程序需要可靠的事件信息傳遞到目的地,他們應該使用可靠的信道並配置了一個ReliableChannelProps配置對象 。
如下定義的 Destination Actor
Scala:
class Destination extends Actor { def receive = { case msg: Message => { println("[destination] event = '%s'" format msg.event) //確認收到來自信道事件消息 msg.confirm() } } }
Java:
public class Destination extends UntypedActor { @Override public void onReceive(Object message) throws Exception { if (message instanceof Message) { Message msg = (Message)message; System.out.println(String.format("[destination] event = %s", msg.event())); msg.confirm(true); } } }
那我們再從空日誌開始,在第一個應用運行的標準輸出中,你應該會看 到
[processor] event = foo (1) [destination] event = 'processed 1 event messages so far'
再次運行該應用程序,你會看到該 event-sourced 的 processor 接收到完整的事件消息的歷史,但 destination 只收到處理器發出的最後一個事件消息 ( 這個消息對應於本次程序運行時發送給 processor 的那個消息) :
[processor] event = foo (1) [processor] event = foo (2) [destination] event = 'processed 2 event messages so far'
消息接收方得到來自信道的消息時,必須調用 Message.confirm() 來回復一個確認收到的應答消息,這個應答消息也會異步寫入日誌。 隨後,您會看到如何通過添加Confirm 特徵,讓消息接收者具有消息確認的功能 。
此快速入門指南是對 Eventsourced 庫一個非常簡單的介紹。 更高級的庫功能都包含在下面的章節 。
Eventsourced特質已經在快速入門中有所討論。它可與可與特質Receiver、Emitter和/或Confirm堆疊組合,但Eventsourced特質必須始終是堆疊的最後一個,即:
Scala:
new MyActor with Receiver with Confirm with Eventsourced
Java:
public class MyActor extends UntypedEventsourcedConfirmingReceiver
該 Eventsourced 的 Java API 提供的可堆疊的特徵作爲抽象基類的一些預定義的組合。例如,UntypedEventsourcedConfirmingReceiver 被定義爲
abstract class UntypedEventsourcedReceiver extends UntypedActor with Receiver with Confirm with Eventsourced
在Java API中其他可堆疊的特質組合在下面的章節中描述。關於所有預定義的組合請參考Untyped*抽象類的API 文檔 。
一個Actor收到Message事件,通常希望能通過模式匹配直接得到內置的事件內容,而不是需要處理整個事件消息(Message)。要達到這個效果可以在 Actor 初始化時混入Receiver特質( Scala API )或繼承抽象類UntypedReceiver( Java API )。
Scala:
class MyActor extends Actor { def receive = { case event => println("received event %s" format event) } } val myActor = system.actorOf(Props(new MyActor with Receiver)) myActor ! Message("foo")
Java:
public class MyActor extends UntypedReceiver { @Override public void onReceive(Object event) throws Exception { System.out.println(String.format("received event = %s", event)); } } final ActorRef myActor = system.actorOf(Props.create(MyActor.class)); myActor.tell(Message.create("foo"), null);
在上面的例子中,給 myActor 發送的 Message("foo") 將被輸出到標準輸出:
received event foo
Receiver特質將接收到的事件Message作爲當前(current)消息存儲在一個內部字段中。提取Message包含的event 內容並以該event爲參數調用MyActor的receive(或onReceive )的方法。如果MyActor想獲得當前事 Message,它必須定義一個Receiver自身類型,並調用message的方法(Scala API)或調用message() 方法(Java API)。
Scala:
class MyActor extends Actor { this: Receiver => def receive = { case event => { //獲得當前事件消息 val currentMessage = message // ... println("received event %s" format event) } } }
Java:
public class MyActor extends UntypedReceiver { @Override public void onReceive(Object event) throws Exception { //獲得當前事件消息 Message currentMessage = message(); // ... System.out.println(String.format("received event = %s", event)); } }
Receiver的特徵還可以與Eventsourced和/或Confirm特質堆疊組合,但Receiver必須是組合中的第一個。例如:
Scala:
new MyActor with Receiver with Eventsourced
Java:
public class MyActor extends UntypedEventsourcedReceiver
請參考API 文檔以瞭解詳情。
Receiver特質簡化了接收端,讓接收Actor使用模式匹配直接活動event內容而不是包裝event的Message。同理,我們引入Emitter特質來對發送端進行相應的簡化。 它使Actor可以直接向信道發送event,而不必手工進行Message 的包裝動作。emitter 也也可以按名稱(或 ID ,見下文)查找信道。
Scala:
class MyActor extends Actor { this: Emitter => def receive = { case event => { //向信道“MyChannel”發出事件 emitter("myChannel") sendEvent ("received: %s" format event) } } } //使用名字“MyChannel”創建並註冊信道 extension.channelOf(DefaultChannelProps(1, destination).withName("myChannel")) val myActor = system.actorOf(Props(new MyActor with Emitter))
Java:
public class MyActor extends UntypedEmitter { @Override public void onReceive(Object event) throws Exception { //向信道“MyChannel”發出事件 emitter("myChannel").sendEvent(String.format("received: %s", event), getSelf()); } } //使用名字“MyChannel”創建並註冊信道 extension.channelOf(DefaultChannelProps.create(1, destination).withName("myChannel"), system); final ActorRef myActor = extension.processorOf(Props.create(MyActor.class), system);
MyActor發出的事件經Emitter包裝成Message發送到信道,Message中包含的事件源於MyActor發出的事件(一個 Emitter 也是 Receiver ,並保持當前事件消息,另見Receiver)。以信道名稱作爲參數調用emitter方法會創建一個MessageEmitter對象,用於捕捉指定的信道和當前事件消息。調用該對象的 sendEvent 會使該對象使用特定的事件參數修改捕獲的事件消息,並將新的事件消息發送給信道(參見信道的使用提示 。一個 MessageEmitter對象也可以被傳遞給另一個Actor(或線程)使用,MessageEmitter對象是線程安全的。 MessageEmitter對象也可以通過 id 引用信道,另外,爲信道指定一個名稱並不是必須的:
Scala:
class MyActor extends Actor { this: Emitter => def receive = { case event => { //發出事件信道id爲1 emitter(1) sendEvent ("received: %s" format event) } } } //創建並註冊信道 extension.channelOf(DefaultChannelProps(1, destination))
Java:
public class MyActor extends UntypedEmitter { @Override public void onReceive(Object event) throws Exception { //發出事件信道id爲1 emitter(1).sendEvent(String.format("received: %s", event), getSelf()); } } //創建並註冊信道 extension.channelOf(DefaultChannelProps.create(1, destination), system);
Emitter的特質也可以與其他特質Eventsourced和/或Confirm等堆疊組合,但Emitter必須始終是第一個。 例如:
Scala:
new MyActor with Emitter with Eventsourced
Java:
public class MyActor extends UntypedEventsourcedEmitter
請參考API 文檔 以瞭解詳情。
對從信道收到消息進行確認應答必須調用Message的confirm() 或confirm(true) 。應用程序也可以做出一個否定的應答,通過調用confirm(false) 。如果是可靠信道,這會導致以重新傳送該事件消息。
Actor也可以混入Confirm特質。而不用顯式調用的confirm(true) 或confirm(false) 。 如果收到的事件經Actor 的 receive(或 onReceive )方法處理後正常返回, Confirm會自動調用confirm(true) 。如果receive (或 onReceive )拋出異常,Confirm 會自動調用confirm(false) 。
Confirm 特質可以獨立使用:
Scala:
new MyActor with Confirm
Java:
public class MyActor extends UntypedConfirmingActor
也可以與其他特質 Receiver 、Emitter和/或Eventsourced堆疊組合。但 Confirm 修改必須在Receiver或Emitter之後, Eventsourced之前。 例如:
Scala:
new MyActor with Receiver with Confirm with Eventsourced
Java:
public class MyActor extends UntypedEventsourcedConfirmingReceiver
請參考API 文檔 以瞭解詳情。
這個例子通過堆疊 Receiver、Emitter和Confirm等特質簡化了快速入門的例子。使用Scala API :
-
Processor會混入Emitter特質( 當然 Eventsourced 是必須有的)
-
Destination混入Receiver和Confirm
對於Java API :
-
Processor 會繼承 UntypedEventsourcedEmitter
-
Destination 會繼承 UntypedConfirmingReceiver
從本節代碼包含在StackableTraits.scala 和StackableTraits.java 。它可以從 SBT 提示與執行
Scala:
> project eventsourced-examples > run-main org.eligosource.eventsourced.guide.StackableTraits
Java:
> project eventsourced-examples > run-main org.eligosource.eventsourced.guide.japi.StackableTraits
新Scala版本的Processor定義有一個自身類型 Emitter,其模式匹匹配直接針對事件。在Java版Processor繼承 UntypedEventsourcedEmitter,還可以直接接收事件(而不是 Message )。
Scala:
class Processor extends Actor { this: Emitter => var counter = 0 def receive = { case event => { counter = counter + 1 println("[processor] event = %s (%d)" format (event, counter)) emitter("destination") sendEvent ("processed %d events so far" format counter) } } }
Java:
public class Processor extends UntypedEventsourcedEmitter { private int counter = 0; @Override public int id() { return 1; } @Override public void onReceive(Object event) throws Exception { counter = counter + 1; System.out.println(String.format("[processor] event = %s (%d)", event, counter)); emitter("destination").sendEvent( String.format("processed %d event messages so far", counter), getSelf()); } }
現在我們通過名字查找信道,而不是通過構造函數傳遞的信道。該信道名稱信道在信道創建時指定。
Scala:
extension.channelOf(DefaultChannelProps(1, destination).withName("destination"))
Java:
extension.channelOf(DefaultChannelProps.create(1, destination).withName("destination"), system);
Scala 的Processor實例化時必須混入Emitter特質,以符合Processor要求的自身類型定義。Java的處理器沒有額外的修改。
Scala:
val processor: ActorRef = extension.processorOf(Props(new Processor with Emitter with Eventsourced { val id = 1 } ))
Java:
final ActorRef processor = extension.processorOf(Props.create(Processor.class), system);
新版Destination定義:
Scala:
class Destination extends Actor { def receive = { case event => { println("[destination] event = '%s'" format event) } } }
Java:
public class Destination extends UntypedConfirmingReceiver { @Override public void onReceive(Object event) throws Exception { System.out.println(String.format("[destination] event = %s", event)); } }
直接對事件內容進行模式匹配,並且消息確認應答交由 Confirm 特質完成。 Scala 版的Destination 實例化時要混入 Receiver和Confirm 特質,Java版Destination仍然不用做修改。
Scala:
val destination: ActorRef = system.actorOf(Props(new Destination with Receiver with Confirm))
Java:
final ActorRef destination = system.actorOf(Props.create(Destination.class));
Eventsourced 庫保存所有發送者的引用
-
Actor之間交換的消息被EventsourcedReceiver、Emitter和/或Confirm等特質修改(對於Java API 是Untyped* 系列基類)。
-
消息通過 信道 傳遞到目標Actor
使用event-sourced Actor 應用程序可以和普通Actor應用程序一樣使用對發送者的引用。 如果您知道Akka Actor的發送者引用是如何工作的,下面的內容應該比較熟悉。
例如,基於的快速入門的代碼開始擴展,Processor可以增加對發送者的消息回覆,如下:
Scala:
class Processor(destination: ActorRef) extends Actor { // … def receive = { case msg: Message => { // … //回覆給發件人 sender ! ("done processing event = %s" format msg.event) } } }
Java:
public class Processor extends UntypedEventsourcedActor { // … @Override public void onReceive(Object message) throws Exception { if (message instanceof Message) { // … getSender().tell(String.format("done processing event = %s", msg.event()), getSelf()); } } }
主程序現在可以使用ask 方法給 processor 發消息 ,並會得到一個異步響應。
Scala:
processor ? Message("foo") onSuccess { case response => println(response) }
Java:
ask(processor, Message.create("foo"), 5000L).onSuccess(new OnSuccess<Object>() { @Override public void onSuccess(Object response) throws Throwable { System.out.println(response); } }, system.dispatcher());
這裏沒有什麼特別的東東。在這個例子中,處理器中的sender代表的就是?(也就是ask)方法返回的Future 。但消息重放時期間會發生什麼情況呢?消息重放時,sender將會是deadLetters因爲Eventsourced處理器不在日誌中存儲 sender 的引用。主要的原因是,應用程序通常不希望在消息重播時發送冗餘地回覆給sender。
除了直接回復給sender之外,該處理器還可以將sender引用進一步轉發給它的destination, 讓destination回覆sender。即使 sender被封裝在一個信道中,這種方法也能正常工作。因爲信道只是簡單的轉發sender引用。出於這個原因,一個ReliableChannel需要存儲sender 的引用(相對於處理器),從而使得sender引用即使在可靠的信道被重新啓動之後仍然可用。如果存儲的sender的是一個遠程引用,即使從 JVM 崩潰(例如可靠信道所在的 JVM 崩潰)中恢復,保存的sender依然有效。
Scala:
class Processor(destination: ActorRef) extends Actor { var counter = 0 def receive = { case msg: Message => { // … //轉發修改後的事件消息到目的地(sender 引用也一起過去了) destination forward msg.copy( event = "processed %d event messages so far" format counter) } } } class Destination extends Actor { def receive = { case msg: Message => { // … // reply to sender sender ! ("done processing event = %s (%d)" format msg.event) } } } val destination: ActorRef = system.actorOf(Props[Destination]) val channel: ActorRef = extension.channelOf(DefaultChannelProps(1, destination)) val processor: ActorRef = extension.processorOf( Props(new Processor(channel) with Eventsourced { val id = 1 } ))
Java:
public class Processor extends UntypedEventsourcedActor { private ActorRef destination; private int counter = 0; public Processor(ActorRef destination) { this.destination = destination; } @Override public int id() { return 1; } @Override public void onReceive(Object message) throws Exception { if (message instanceof Message) { Message msg = (Message)message; //轉發修改後的事件消息到目的地(sender 引用也一起過去了) destination.forward(msg.withEvent( String.format("processed %d event messages so far", counter)), getContext()); } } } public class Destination extends UntypedActor { @Override public void onReceive(Object message) throws Exception { if (message instanceof Message) { Message msg = (Message)message; // … //回覆給 sender getSender().tell( String.format("done processing event = %s", msg.event()), getSelf()); } } } final ActorRef destination = system.actorOf(Props.create(Destination.class)); final ActorRef channel = extension.channelOf(DefaultChannelProps.create(1, destination), system); final ActorRef processor = extension.processorOf(Props.create(Processor.class, channel), system);
當使用MessageEmitter(另請參見Emitter )應用程序可以選擇使用方法sendEvent或forwardEvent。sendEvent需要一個(隱式)sender 引用作爲參數,而forwardEvent使用當前消息的sender,並對消息進行轉發。它們分別與 ActorRef 的tell(!)方法和forward方法工作方式相同。
從本節代碼包含在SenderReferences.scala和SenderReferences.java 。它可以從 SBT 提示與執行
Scala:
> project eventsourced-examples > run-main org.eligosource.eventsourced.guide.SenderReferences
Java:
> project eventsourced-examples > run-main org.eligosource.eventsourced.guide.japi.SenderReferences
信道也是一個Actor ,用於跟蹤成功的事件消息傳遞。event-sourced模式的處理器使用信道來避免在消息回放時重複給目的地發送消息。某些信道也可以獨立 (standalong)使用 ,即不與event-sourced處理器綁定在一起。
Martin Fowler 的文章event sourcing中有一節External Updates描述了一個信道的使用案例 。本文快速入門示例中信道使用一節也給出了一個起步的例子。
目前,EventSourced 庫提供了兩個不同的信道實現,默認信道 (DefaultChannel)和可靠信道 (ReliableChannel) ,並基於可靠信道還實現了一個個可靠的“請求-應答”信道 (reliable request-reply channel) 。 這些將在下面的小節解釋。
默認信道是傳遞事件消息到接收Actor一個臨時信道。當接收 Actor 在收到的Message上調用confirm()或confirm(true) 進行確認時,一個確認信息會被異步寫入日誌。當消息回放時,消息會與日誌中的確認信息進行匹配,能匹配上的就不會被再次發送給接收者。
如果接收者對事件消息進行了否定的應答(confirm(false),該消息就會在回放時重新發送。 如果消息沒有被確認,回放時也會重新發送。因此,在存在消息丟失或否定應答的情況下,接收者從一個缺省信道收到的事件消息次序可能和event-sourced處理器生成事件消息的順序不同。
DefaultChannel創建後要在EventsourcingExtension 上註冊,如下:
val extension: EventsourcingExtension = … val destination: ActorRef = … val channelId: Int = … val channel: ActorRef = extension.channelOf(DefaultChannelProps(channelId, destination))
該channelId必須是一個正整數並在應用程序運行時保持不變。EventsourcingExtension的channels可以獲得所有已註冊的信道映射,Map[Int, ActorRef] , 主鍵是信道 ID 。信道還可以定義一個名稱(另見Emitter)。
// … val channelId: Int = … val channelName: String = … val channel: ActorRef = extension.channelOf( DefaultChannelProps(channelId, destination).withName(channelName))
調用namedChannels方法,可以得到所有已註冊的有名信道,Map[String, ActorRef],主鍵是信道名稱。
默認信道保留sender引用 。因此,應用程序可以對信道Actor使用?和forward方法,消息會傳遞到信道的接收者Actor。不過,使用時?必須特別注意:消息回放時,已被信道接收者確認的消息會被信道忽略,從而接收者無法對回放的消息進行回覆。因此,發送方將得到一個響應超時。要避免這種情況發生,應用可以預先判斷信道是否會忽略消息。例如,應用可以維護一個Message.acks列表,如果信道id 在列表中,其消息就可能會被忽略,對這種信道就不應該使用ask方法。
val channelId: Int = … val channel: ActorRef = … if (!message.acks.contains(channelId)) channel ? message.copy(…) onComplete { case result => … }
關於 message.copy(…) 另請參閱使用提示。
可靠的信道是一個具有持久化能力的信道,它會在把消息傳遞給接收者Actor之前,就把消息寫入日誌。與默認信道相比,可靠信道保留event-sourced處理器產生消息的順序並在接收者發生錯誤時會試圖重發消息。因此,使用可靠的信道時,如果消息接收端臨時發生了錯誤,即使不進行消息回放 , 應用程序能也能夠恢復正常運行。此外,一個可靠的信道,也可以從 JVM 崩潰中恢復。這樣就無論是接收者出故障,還是發送者出故障,應用程序都可以獲得“ 至少一次 ”的消息傳遞保證。
當接收者確認收到事件消息時,所存儲的消息從信道移除,下一個開始傳送。如果接收者沒有確認 ( 如 , 超時 ) 或給出一個否定確認,信道會在一定重發延遲內進行重試。如果達到最大重試次數還不成功,信道在一定的重啓延遲後重新啓動自己,再進行重發消息。如果達到最大的重啓次數還不成功,信道會停止消息傳遞,並且發佈一個DeliveryStopped事件給信道actor所屬的Actor System。應用程序要重新激活信道,可以調用 EventsourcingExtension的deliver(Int) 的方法,將信道 ID 作爲參數傳給它。請參閱ReliableChannel API 文檔瞭解詳情。
ReliableChannel的創建和註冊與默認信道類似,唯一不同的是需要使用配置對象ReliableChannelProps。
// … val channel: ActorRef = extension.channelOf(ReliableChannelProps(channelId, destination))
這個配置對象還允許應用程序配置信道的可靠策略 (RedeliveryPolicy)。
可靠信道會保存sender引用。 因此,應用程序可以對信道Actor使用?和forward 方法,消息會傳遞到信道的接收者Actor。詳情已在默認信道部分描述過。可靠信道還會將sender引用和事件消息一起存儲,這樣即使信道重啓,事件消息和sender仍能夠原樣傳遞到接收者。如果sender是遠程引用,即使從JVM崩潰 ( 如信道所在的jvm 崩潰 ) 中恢復,引用仍然有效。
熟悉 Akka 的人會發現可靠信道與類似於Akka 可靠代理很相似,不同的是它還能使應用從發送方的 JVM 崩潰從恢復過來 ( 另見Remote destinations )。
可靠的“請求-應答”信道是在可靠信道基礎上實現的。 它在發送者(通常是event-sourced處理器 )和接收者插入可靠的請求響應機制。 與一般可靠信道相比,它還有下列附加的屬性:
-
在Message轉交給接收者之前,提取其中的請求內容。
-
將接收者的回覆包裝進一個Message,再發送回請求的發送者。
-
如果在配置的超時時間內,接收者沒有應答,它會發送一個特殊的DestinationNotResponding答覆給請求的發送方。
-
發送一個特殊的DestinationFailure答覆請求發送方,如果接收者響應Status.Failure。
-
對請求的發送方保證在“最少一次”的應答交付(當然,對接收方也有“最少一次”的請求交付)。
-
需要對接收者的回覆再給一個確認應答,來標誌一次請求-應答交互成功完成。
-
如果缺少應答或得到否定應答,會重發請求及後續的回覆。
可靠的“請求-應答”信道的創建和註冊與可靠信道類似,只是需要使用一個ReliableRequestReplyChannelProps配置對象。
// … import org.eligosource.eventsourced.patterns.reliable.requestreply._ val channel: ActorRef = extension.channelOf( ReliableRequestReplyChannelProps(channelId, destination))
這個配置對象還允許應用程序爲接收者的回覆配置一個replyTimeout。一個可靠的“請求-應答”信道的詳細用法例子在這篇文章中 。
信道必須在使用前激活,請參閱 extension.deliver ()。
爲了讓信道正常工作,event-sourced處理器必須從收到( 或日誌中 )的消息中複製processorId和sequenceNr到輸出的事件消息中。這通常是通過調用輸入Message的copy()方法,僅更新那些需要修改的字段即可。如:
class Processor(channel: ActorRef) extends Actor { def receive = { case msg: Message => { // … channel ! msg.copy(event = …, ack = …) } } }
如果使用Emitter,這個動作可以自動完成。
可靠信道和可靠的“請求-應答”信道也可以獨立於Eventsourced 理器使用。獨立使用時,sender 必須將待發送的Message的Message.processorId設置爲0(這是默認值):
val channel = extension.channelOf(ReliableChannelProps(…)) channel ! Message("my event") // processorId == 0
這相當於直接發送 Message.event :
channel ! "my event"
可靠信道內會在內部將接收到的事件封裝成爲一個Message,並將Message的processorId設置爲0 。將processorId 置爲0會讓可靠信道不對這個消息做寫確認(acknowledgement)。event-sourced處理器收到的消息都會和一個確認(acknowledgement)相關聯。但在這裏因爲信道沒有綁定處理器,所以這個確認也是不必要的。另一個需要關閉寫確認的場景請查看事件系列。
如果發送方處理器需要將消息傳遞到遠程接收者,要求消息“ 最少一次 ”到達,並且要按照發送的順序到達,那麼應用程序應該考慮使用可靠的信道,這可以防止:
-
發送者和目的地之間的網絡錯誤(遠程Actor引用仍然有效,但遠程Actor暫時不可用)
-
目標JVM的崩潰(遠程Actor引用無效),並
-
發送方JVM崩潰(消息已經到達sender處理器,但尚未傳遞到遠程的接收者,應該在sender從崩潰中恢復時自動發送)
如果將遠程Actor的引用作爲信道的接收方,能解決第1中情況的問題,但第2種情況不行。對第2種情況有一個可行的辦法,使用本地Actor作爲遠程Actor的代理與遠程Actor(通過ActorSelection得到)通信。ActorSelection可以從一個Actor路徑來創建,它並不和遠程Actor的生命週期綁定。
class DestinationProxy(destinationPath: String) extends Actor { val destinationSelection: ActorSelection = context.actorSelection(destinationPath) def receive = { case msg => destinationSelection tell (msg, sender) // forward } } val destinationPath: String = … val proxy = system.actorOf(Props(new DestinationProxy(destinationPath))) val channel = extension.channelOf(ReliableChannelProps(1, proxy))
當然,使用 ActorSelection 的辦法也能解決第 1 種情況的問題。第 3 種情況反而最簡單,只要 sender 處理器使用可靠信道就沒問題。 如下例:
class Sender extends Actor { val id = 1 val ext = EventsourcingExtension(context.system) val proxy = context.actorOf(Props(new DestinationProxy( "akka.tcp://[email protected]:2852/user/destination"))) val channel = ext.channelOf(ReliableChannelProps(1, proxy)) def receive = { case msg: Message => channel forward msg } } //創建和恢復發送者和其信道 val sender = extension.processorOf(Props(new Sender with Eventsourced)) sender ! Message("hello")
如果遠程的Actor是一個Eventsourced Actor,必須特別注意。 在這種情況下,應用程序必須確保遠程的Actor在成功恢復之後,只能通過遠程方式進行訪問。這可以實現,例如,通過使用一個附加endpointActor,簡單地轉發消息給到目的地Actor。endpoint Actor使用目標路徑進行註冊,在目標Actor恢復之後與之關聯起來。
class DestinationEndpoint(destination: ActorRef) extends Actor { def receive = { case msg => destination forward msg } } class Destination extends Actor { val id = 2 def receive = { case msg: Message => { println(s"received ${msg.event}") msg.confirm() } } } val destination = extension.processorOf(Props(new Destination with Eventsourced)) //等待目標完成恢復 extension.recover() //使目標恢復後可以遠程訪問 system.actorOf(Props(new DestinationEndpoint(destination)), "destination")
這確保了新的遠程消息永遠不會和恢復期間重播的消息交叉弄混。
從本節代碼包含在ReliableChannelExample.scala。發送方應用程序可以從 SBT 開始與
> project eventsourced-examples > run-main org.eligosource.eventsourced.example.Sender
遠程接收者啓動:
> project eventsourced-examples > run-main org.eligosource.eventsourced.example.Destination
發送方應用程序提示用戶在標準輸入敲入消息,消息會可靠地傳送到遠程目標。
恢復是實現event-sourced模式的應用程序進行狀態重建的過程。 恢復動作通常是在應用程序啓動時完成,無論程序是在正常終止或崩潰後重啓(但也可以做到任何時間執行,甚至是單個處理器和信道也可以做恢復動作)。
val system: ActorSystem = … val journal: ActorRef = … val extension = EventsourcingExtension(system, journal) //創建和註冊event-sourced處理器 extension.processorOf(…) // … //創建和註冊信道 extension.channelOf(…) // ... //恢復註冊的處理器狀態,並激活信道 extension.recover() //處理器和信道都已經可以使用 // …
recover()方法首先向所有已註冊的處理器重播日誌記錄的事件消息。 通過重播事件消息歷史,處理器可以恢復狀態。消息回放期間,處理器也會向一個或多個信道發送消息。這些信道要麼忽略在之前應用程序運行是已經成功交付(即收到過確認 )的消息,或緩衝消息,稍後傳遞。重播結束後,recover()方法觸發信道傳送被緩衝的消息。
如果信道立即傳遞,而不是先緩衝事件消息,傳遞的事件消息很可能與重播的事件消息錯誤地交織在一起。這可能會導致在事件消息順序的不一致,從而引起應用程序狀態的不一致。因此,恢復動作必須確保所有重放的事件消息已經放入處理器郵箱之後,才傳遞緩衝確保緩衝的事件消息。這對於連接成環狀、有向圖形式的的處理器和信道的恢復尤其重要。
可以給EventsourcingExtension.recover(Seq[ReplayParams]) 方法 ( 或其他的重載方法 ) 傳遞一個ReplayParams來對重播過程進行定製。ReplayParams允許對各個處理器的狀態恢復做細粒度地控制。對於每一個需要恢復的處理器,應用程序都要爲它創建一個ReplayParams實例。ReplayParams指定
-
是否應該重播從頭開始,或是從一個快照,或從一個給定的順序號(指定起始序號)。
-
一直恢復到當前狀態,還是在某一個歷史狀態終止(指定終止序號)
下面兩小節演示一些 ReplayParams 用法示例。 欲瞭解更多詳情,請參閱的 API 文檔ReplayParams及其伴生對象 。 有關創建快照的詳細信息請參閱快照。
如前所述:
val extension: EventsourcingExtension = … import extension._ recover()
沒有指定序號的上下限恢復所有的處理器,所有消息都會被重播。 這等效於
recover(replayParams.allFromScratch)
或
recover(processors.keys.map(pid => ReplayParams(pid)).toSeq)
如果應用程序只是想恢復特定的處理器,它應該只爲這些處理器創建ReplayParams。例如
recover(Seq(ReplayParams(1), ReplayParams(2)))
只有恢復與IDS爲1和2處理器 。也可以指定序號的上下範圍。
recover(Seq(ReplayParams(1, toSequenceNr = 12651L), ReplayParams(2, fromSequenceNr = 10L)))
這裏處理器1將會收到範圍在0到12651(含)之間的消息,處理器2與接收到從10開始的所有消息。
在基於快照恢復時,處理器會先收到一個SnapshotOffer,再收到剩餘的事件消息之前(如果有的話)。處理器使用SnapshotOffer消息以恢復其狀態。
Scala:
class Processor extends Actor { var state = // … def receive = { case so: SnapshotOffer => state = so.snapshot.state // … }
Java:
public class Processor extends UntypedEventsourcedActor { private Object state = // … @Override public void onReceive(Object message) throws Exception { if (message instanceof SnapshotOffer) { SnapshotOffer so = (SnapshotOffer)message; state = so.snapshot().state(); } // … } }
基於快照的恢復只會給處理器發送一個 SnapshotOffer 消息,即使之前已經爲處理器創建了多個快照,並且這些快照都匹配相應ReplayParams。 toSequenceNr和snapshotFilter可以用來限定快照選擇的標準。如果處理器沒有快照的或現有的快照不匹配ReplayParams條件,事件消息將從頭開始播放,即從序號0開始 。
要從最新快照恢復所有的處理器可以調用:
recover(replayParams.allWithSnapshot)
這等效於
recover(processors.keys.map(pid => ReplayParams(pid, snapshot = true)).toSeq)
基於快照恢復也可以指定序號上限。
recover(Seq(ReplayParams(1, snapshot = true, toSequenceNr = 12651L)))
這將恢復處理器1先恢復到序號<= 12651的最新快照 。在重播其後序號小於12651(含)的事件消息(如果有的話)。應用程序也可以爲快照定義進一步的約束。 例如:
import scala.concurrent.duration._ val limit = System.currentTimeMillis - 24.hours.toMillis recover(Seq(ReplayParams(1, snapshotFilter = snapshotMetadata => snapshotMetadata.timestamp < limit)))
對處理器 1 使用 24 小時內的最新快照。這是通過 snapshotFilter 來實現,它基於時間戳對快照進行過濾。快照過濾器在SnapshotMetadata上操作 。
recover方法等待回放的消息被髮送到所有處理器(通過 "!" 方法)但不會等待這些處理器處理完回放的事件消息。然而,發送到任何註冊的處理器的任何新的消息,會在 recover 成功返回後,由處理器在重播事件消息處理完之後處理。如果應用程序希望等待處理器處理完所有回播的事件消息,可以使用EventsourcingExtension的 awaitProcessing() 方法 。
val extension: EventsourcingExtension = …
extension.recover()
extension.awaitProcessing()
這特別適合於應用程序使用 STM 引用來保存狀態的情景。這時應用程序會希望在收到客戶端新的讀請求之前,所有的(外部可見的)狀態都已經完全恢復完成。默認情況下, awaitProcessing() 方法等待所有已註冊的處理器完成處理,而應用程序也可以指定註冊的處理器的子集。
在 recover 和 awaitProcessing 方法會阻塞調用線程。如果 event-sourced 應用程序希望在主線程中完成所有回覆動作後再幹新的事情,這可能比較方便。在其他情況下,例如,只是恢復 Actor 中的個別子處理器(和信道)(見OrderExampleReliable.scala ),應使用非阻塞恢復 API :
val extension: EventsourcingExtension = … val future = for { _ <- extension.replay(…) _ <- extension.deliver(…) // optional _ <- extension.completeProcessing(…) // optional } yield () future onSuccess { case _ => // event-sourced processors now ready to use … }
在這裏, replay 、deliver和completeProcessing返回的future由for表達式組合在一起,保證這些異步操作能夠順序依次執行。當組合的future完成時,被恢復處理器和信道就可以使用了。 更多細節在API 文檔 。 該replay方法也可以用ReplayParams定製 ( 參見Replay parameters).
信道甚至可以在創建之後激活之前,立即被應用程序使用。這一點非常重要,尤其是當event-sourced(父)處理器在處理事件的過程中創建新的event-sourced子處理器時:
class Parent extends Actor { this: Receiver with Eventsourced => import context.dispatcher var child: Option[ActorRef] = None def receive = { case event => { //創建用信道封裝的子處理器 if (child.isEmpty) { child = Some(createChildProcessor(2, 2)) } //信道可立即使用 child.foreach(_ ! message.copy(…)) } } def createChildProcessor(pid: Int, cid: Int): ActorRef = { implicit val recoveryTimeout = Timeout(10 seconds) val childProcessor = extension.processorOf(Props(new Child with Receiver with Eventsourced { val id = pid } )) val childChannel = extension.channelOf(DefaultChannelProps(cid, childProcessor)) for { // asynchronous, non-blocking recovery _ <- extension.replay(Seq(ReplayParams(pid))) _ <- extension.deliver(cid) } yield () childChannel } } class Child extends Actor { this: Receiver => def receive = { case event => { … confirm() } } }
在這裏,Parent收到事件消息後,創建一個新的childProcessor(以默認信道包裝)。該childChannel是由Parent創造立即被使用,於此同時,子處理器的消息回放和信道的激活也在進行。 這是可行的,因爲信道在內部會緩衝的新的消息,在它完成激活後再發送給目的方。這確保了新的消息只會在 子信道 激活完成後才被傳遞到 子處理器 。在Parent的恢復期間,childChannel將忽略已成功交付給childActor的消息(即childActor已經確認的消息)。
一個Eventsourced處理器的行爲可能依賴於其他 Eventsourced 處理器的狀態。例如,處理器A發送一個消息給處理器B ,然後處理器B回覆一個消息,消息中包含了處理器B的狀態(或部分狀態)。根據回覆中狀態的不同,處理器A可以採取不同的行動。爲了確保在這種情況下能夠正確恢復恢復,A與B之間的任何狀態傳輸或狀態相關的消息交換都必須是Message類型 (參見DependentStateRecoverySpec.scala )。通過非日誌化的消息交換狀態數據(如傳輸非 Message 類型的數據 )會導致無法保證恢復時的一致性。還有一種情況也有類似問題:如果一個Eventsourced處理器通過一個外部可見的STM引用保存狀態,並且另外一個Eventsourced處理器直接讀取該引用。Eventsourced處理器之間的通信與外部查詢 (external queries )和外部更新 (external updates)密切相關 。
一個快照代表處理器在一個特定時間點的狀態,可以用來大大減少恢復所需的時間。快照捕捉和保存由應用程序觸發,快照並不會刪除事件消息的歷史記錄,除非由應用程序明確要求。
應用程序可以通過給送Eventsourced處理器發 SnapshotRequest消息( Scala API )或SnapshotRequest.get() 消息( Java API )來創建快照。
Scala:
import org.eligosource.eventsourced.core._ // … val processor: ActorRef = … processor ! SnapshotRequest
Java:
import org.eligosource.eventsourced.core.*; // … final ActorRef processor = … processor.tell(SnapshotRequest.get(), …)
這將異步採集和保存processor的狀態快照。快照成功保存後發送方將收到通知。
Scala:
(processor ? SnapshotRequest).mapTo[SnapshotSaved].onComplete { case Success(SnapshotSaved(processorId, sequenceNr, timestamp)) => … case Failure(e) => … }
Java:
ask(processor, SnapshotRequest.get(), 5000L).mapTo(Util.classTag(SnapshotSaved.class)).onComplete(new OnComplete<SnapshotSaved>() { public void onComplete(Throwable failure, SnapshotSaved result) { if (failure != null) { … } else { … } } }, system.dispatcher());
或者,應用程序也可以使用EventsourcingExtension.snapshot方法來觸發快照的創建。舉個例子,
Scala:
val extension: EventsourcingExtension = … extension.snapshot(Set(1, 2)) onComplete { case Success(snapshotSavedSet: Set[SnapshotSaved]) => … case Failure(_) => … }
Java:
Set<Integer> processorIds = new HashSet<Integer>(); processorIds.add(1); processorIds.add(2); extension.snapshot(processorIds, new Timeout(5000L)).onComplete(new OnComplete<Set<SnapshotSaved>>() { public void onComplete(Throwable failure, Set<SnapshotSaved> snapshotSavedSet) { if (failure != null) { … } else { … } } }, system.dispatcher());
爲處理器1和2創建快照 ,返回的 Future (類型的 Future[scala.immutable.Set[SnapshotSaved]]--Scala API )或 Future<java.util.Set<SnapshotSaved>> --Java API))成功完成時,兩個處理器的快照已保存成功。
爲了能夠保存快照,處理器必須處理SnapshotRequest的消息,以其當前 state 作爲參數調用它自身的的 process 方法:
Scala:
class Processor extends Actor { var state = … def receive = { case sr: SnapshotRequest => sr.process(state) // … } }
Java:
public class Processor extends UntypedEventsourcedActor { private Object state = … @Override public void onReceive(Object message) throws Exception { if (message instanceof SnapshotRequest) { SnapshotRequest sr = (SnapshotRequest)message; sr.process(state, getContext()); } // … } }
調用process將異步保存state參數,快照產生的元數據也會一起保存。 創建一個新的快照不會刪除舊的快照,除非由應用程序明確要求。 因此,每個處理器可以有 N 個快照。
SnapshotExample.scala和SnapshotExample.java 中的例子示範快照的創建和基於快照的恢復。它可以從 SBT 中執行:
Scala:
> project eventsourced-examples > run-main org.eligosource.eventsourced.example.SnapshotExample
Java:
> project eventsourced-examples > run-main org.eligosource.eventsourced.example.japi.SnapshotExample
所有日誌都支持快照,它是通過抽象的Hadoop FileSystem來實現的。缺省的 FileSystem 實例就是本地文件系統,默認情況下快照會寫在本地,除非應用程序指定了另外的配置。關於如何創建基於 HDFS , FTP , S3 的 FileSystem 實例請參考Hadoop的文檔。應用程序定義的FileSystem爲實例可以如下配置:
Scala:
// … import org.apache.hadoop.fs.FileSystem // … val hdfs: FileSystem = FileSystem.get(...) val journal: ActorRef = LeveldbJournalProps(..., snapshotFilesystem = hdfs).createJournal
Java:
// … import org.apache.hadoop.fs.FileSystem; // … final FileSystem hdfs = FileSystem.get(…); final ActorRef journal = LeveldbJournalProps.create(...).withSnapshotFilesystem(hdfs).createJournal(system);
請查看HadoopFilesystemSnapshottingProps API 文檔瞭解更多信息。
Envent-sourced Actor 一般都會混入Receiver ,Emitter和/ 或Eventsourced 特質(Scala API)或繼承自相應的 Untyped* 系列基類(Java API),這些Acotr可以用方法become()和unbecome()動態改變Actor的行爲。become()和 unbecome()方法定義在Behavior特質中,Receiver、 Emitter和Eventsourced都繼承自Behavior。
即使Actor使用become()和unbecome()改變了自己的行爲,它從Receiver ,Emitter和/或Eventsource 特質引入的功能仍然有效。 例如,一個混入了Eventsourced特質的Actor(Scala API )使用become()改變其行爲後仍會繼續記錄日誌事件消息。
但是, Actor 如果使用 context.become()(Scala API )或getContext().become() 的 Java API ) 改變其行爲,將會喪失由 Receiver、 Emitter和/或Eventsourced等特質引入的功能(即使可以使用context.unbecome() 或 getContext().unbecome()切換回來)
當一個處理器根據一個輸入的事件消息產生出多個輸出事件消息,並將這些輸出信息發出到一個信道,我們稱它產生了一個事件消息系列。對於事件消息系列,事件處理器應該將系列中除了最後一個消息外的所有消息的 ack 字段設置爲 false
class Processor(channel: ActorRef) extends Actor { def receive = { case msg: Message => { // … channel ! msg.copy(event = "event 1", ack = false) // 1st message of series channel ! msg.copy(event = "event 2", ack = false) // 2nd message of series // … channel ! msg.copy(event = "event n") // last message of series } } }
如果處理器使用了發射器,則按如下方式:
class Processor extends Actor { this: Emitter => def receive = { case event => { // … emitter("channelName") send (msg => msg.copy(event = "event 1", ack = false)) // 1st message of series emitter("channelName") send (msg => msg.copy(event = "event 2", ack = false)) // 2nd message of series // … emitter("channelName") sendEvent "event n" } } }
這確保了當確認信息被寫入到日誌之前,系類的最後一條消息已經成功完成下述動作 ,
不過對於目標接收者,應該確認收到的每一個事件消息,而不管其是否屬於一個系列。
在某些故障條件下,信道可能將同一個消息不止一次地發送到目的地。一個典型的例子是,目標接收者給出了肯定確認的消息,但確認消息記入日誌前不久,應用程序崩潰。在這種情況下,恢復期間目標接收者將再次接收到同樣的事件消息。
由於這些(還有其他)的原因,信道的目標接收者地必須是冪等事件消息的消費者,這要在應用級別考慮。假如,有一個事件消息的消費者,它將收到的訂單存儲在一個映射 (Map,key 是訂單號 ) 中 , 由於無論它收到訂單消息一次或多次,都是同樣的結果,映射中包含的訂單隻有一份,所以這是一個冪等消費者。如果一個事件消息的消費者任務是對收到的訂單進行計數,那麼它就不是一個冪等消費者。因爲從業務邏輯看,重複收到的消息會導致錯誤的計數。 在這種情況下,該事件消息的消費者必須採取一些額外的手段來檢測並處理重複的事件的消息 。
爲了檢測重複,應用程序應該對他們的事件進行標識。event-sourced處理器應該在事件被髮送到信道之前設置標識值。信道目的接收者(或其他下游消費者)應該保持成功處理事件標識,並將它與新接收到的事件的標識進行比較。如果新收到的事件標誌在已記錄的標識中已經存在,這被認爲是一個重複消息(假設發射處理器生成的標識符是唯一的)。 爲了生成唯一標識符,處理器可以使用接收到的事件消息的序列號:
case class MyEvent(details: Any, eventId: Long) class Processor extends Actor { this: Emitter with Eventsourced => def receive = { case event => { // get sequence number of current event message val snr: Long = sequenceNr val details: Any = … // … emitter("channelName") sendEvent MyEvent(details, snr) } } }
使用序列號的優勢在於,消費者只需要記住上次成功收到事件的標識。如果新收到的事件的標識是否小於或等於原先記住的值,就認爲是重複的,可以忽略。
class Consumer extends Actor { var lastEventId = 0L def receive = { case MyEvent(details, eventId) => if (eventId <= lastEventId) { // duplicate } else { // … lastEventId = eventId } } }
如果消費者是 event-sourced 處理器,則可以將事件標識存儲在自己的狀態數據中,消息回放時事件標誌也能被恢復。其他類型的消費者必須有自己的方式保存標識。
發出系列事件的處理器除了使用唯一標識外還應該使用一個事件消息的索引來標定發出的事件:
case class MyEvent(details: Any, eventId: (Long, Int)) class Processor extends Actor { this: Emitter with Eventsourced => def receive = { case event => { // get sequence number of current event message val snr: Long = sequenceNr val details: Seq[Any] = … // … emitter("channelName") send (msg => msg.copy(event = MyEvent(details(0), (snr, 0)), ack = false)) emitter("channelName") send (msg => msg.copy(event = MyEvent(details(1), (snr, 1)), ack = false)) // … } } }
爲了檢測重複,消費者應該對 “ 序列號 - 索引”對進行比對。
應用程序可以爲 Message 中的事件自定義序列化方式。自定義序列化可以用與事件的日誌記錄和遠程通訊。它們可以和任何其他的Akka 序列化一樣的方式進行配置 。 例如:
akka { actor { serializers { custom = "example.MyEventSerializer" } serialization-bindings { "example.MyEvent" = custom } } }
在這裏, example.MyEvent是一個應用程序特定的事件類型,example.MyEventSerializer是應用程序特定的序列器,它繼承自akka.serialization.Serializer。
import akka.serialization.Serializer class CustomEventSerializer extends Serializer { def identifier = … def includeManifest = true def toBinary(o: AnyRef) = … def fromBinary(bytes: Array[Byte], manifest: Option[Class[_]]) = … }
事件Message 自己使用Enven-Sourced 庫預定義的序列化方式。只要 eventsourced-journal-common-*.jar 在 Akka 應用程序的類路徑中,這個用於Message的序列化器會自動被應用上。
本節中的訂單管理例子是取自Martin Fowler 的著名的文章The LMAX Architecture :
想象一下,你正在使用信用卡下單購買糖豆。一個簡單的零售系統,將獲取您的訂單信息,使用信用卡驗證服務來檢查你的信用卡號碼,然後確認您的訂單,所有這些動作在一個單一操作中完成。處理您的訂單的線程在等待信用卡檢查結果時將會被阻塞,阻塞時間對用戶來說不會太長,服務器在等待時也可以在處理器上運行的另一個線程。
在 LMAX 架構中,你會將這個操作一分爲二。第一個操作獲取信用卡信息,向信用卡公司發出一個事件 ( 信用卡校驗請求 ) 就會完成。業務邏輯處理器會繼續處理其他客戶事件,直到在其輸入事件流收到一個信用卡驗證事件。處理這個事件時在繼續進行前述訂單的確認動作。
這可以通過Eventsourced庫來實現,如圖(見附錄 A )。
-
我們將前文提到的 業務邏輯處理器 處理器作爲event-sourced Actor來實現(OrderProcessor)。它處理OrderSubmitted事件時將一個id賦予已提交的訂單,並將訂單保存在一個Map映射(這就是OrderProcessor的狀態數據)中。對於每一個提交的訂單會他發出一個CreditCardValidationRequested事件。
-
CreditCardValidationRequested事件由CreditCardValidator Actor 處理。它聯繫外包的信用卡驗證服務,驗證完成後,對每一份信用卡有效的訂單,他都將一個 CreditCardValidated事件發送回 OrderProcessor。在下面的例子中爲了實現簡單,我們沒有真的使用外部的服務,,但對於真實的實現,akka-camel 將特別適合用到這裏。
-
收到CreditCardValidated事件,event-sourced OrderProcessor 更新的相應訂單狀態,即設置validated = true,併發送一個OrderAccepted事件,包含更新的訂單信息到它的Destination。這也會將更新後的訂單回發給初始的發送者。
領域對象Order ,領域域事件和以及OrderProcessor定義如下:
//領域對象 case class Order(id: Int = -1, details: String, validated: Boolean = false, creditCardNumber: String) // 領域事件 case class OrderSubmitted(order: Order) case class OrderAccepted(order: Order) case class CreditCardValidationRequested(order: Order) case class CreditCardValidated(orderId: Int) //event-sourced訂單處理器 class OrderProcessor extends Actor { this: Emitter => var orders = Map.empty[Int, Order] // processor state def receive = { case OrderSubmitted(order) => { val id = orders.size val upd = order.copy(id = id) orders = orders + (id -> upd) emitter("validation_requests") forwardEvent CreditCardValidationRequested(upd) } case CreditCardValidated(orderId) => { orders.get(orderId).foreach { order => val upd = order.copy(validated = true) orders = orders + (orderId -> upd) sender ! upd emitter("accepted_orders") sendEvent OrderAccepted(upd) } } } }
OrderProcessor處理器將一個CreditCardValidationRequested事件以Message的形式通過信道"validation_requests"發送給 CreditCardValidator。 該forwardEvent方法不僅發送事件,還會將事件的發送者設置爲最初的sernder引用 。 CreditCardValidator 在接收到CreditCardValidationRequested事件後,在後臺執行一個信用卡驗證動作,驗證完成後並回發一個CreditCardValidated事件給 OrderProcessor。
class CreditCardValidator(orderProcessor: ActorRef) extends Actor { this: Receiver => def receive = { case CreditCardValidationRequested(order) => { val sdr = sender // initial sender val msg = message // current event message Future { //做一些信用卡驗證 // … // and send back a successful validation result (preserving the initial sender) orderProcessor tell (msg.copy(event = CreditCardValidated(order.id)), sdr) } } } }
CreditCardValidator 向 OrderProcessor 發送 CreditCardValidated 事件時,再次把之前保存的初始sender引用一起發給OrderProcessor,這樣OrderProcessor就可以答覆初始的sender引用。OrderProcessor還會使用名爲"accepted_orders"信道的信道發送一個OrderAccepted 件給最終的Destination。
class Destination extends Actor { def receive = { case event => println("received event %s" format event) } }
下一步是將這些對象協同在一起,並且恢復他們:
val extension: EventsourcingExtension = … val processor = extension.processorOf(Props(new OrderProcessor with Emitter with Confirm with Eventsourced { val id = 1 })) val validator = system.actorOf(Props(new CreditCardValidator(processor) with Receiver)) val destination = system.actorOf(Props(new Destination with Receiver with Confirm)) extension.channelOf(ReliableChannelProps(1, validator).withName("validation_requests")) extension.channelOf(DefaultChannelProps(2, destination).withName("accepted_orders")) extension.recover()
名爲 "validation requests" 的信道是一個可靠的信道,當 CreditCardValidator 故障(例如,外部信用卡驗證服務暫時不可用)時會重發 CreditCardValidationRequested 。此外,應該指出的是, CreditCardValidator 沒有對收到的事件消息做確認(它既沒有顯式的調用 confirm() 也沒有在初始化時混入 Confirm 特質)。傳輸確認是在 OrderProcessor 成功處理了 CreditCardValidated 事件時做出的。
現在 Order processor ,已經做好了接收 OrderSubmitted 事件的準備。
processor ? Message(OrderSubmitted(Order(details = "jelly beans", creditCardNumber = "1234-5678-1234-5678"))) onSuccess { case order: Order => println("received response %s" format order) }
以一個空的日誌運行這個例子會在標準輸出打印:
received response Order(0,jelly beans,true,1234-5678-1234-5678) received event OrderAccepted(Order(0,jelly beans,true,1234-5678-1234-5678))
運行示例時,您看到的訂單信息可能有所不同。 提交訂單的id被設置爲OrderProcessor內部映射orders的初始大小,第一次運行時值爲0。第二次運行時會先恢復之前的應用程序狀態,所以生成的id變成了1 。 程序第二次運行將首先恢復以前的應用程序的狀態,所以其他訂單提交會產生一個訂單id爲1 。
received response Order(1,jelly beans,true,1234-5678-1234-5678) received event OrderAccepted(Order(1,jelly beans,true,1234-5678-1234-5678))
該示例代碼包含在OrderExample.scala ,可以從SBT提示與執行
> project eventsourced-examples > run-main org.eligosource.eventsourced.example.OrderExample
這個例子的高級版本,使用的是前面講述過的可靠的“請求-應答”信道 。
自從 Akka 2.1 ,將 event-sourcing 模式應用於 Akka FSM s 非常方便。下面演示了一個 門 (Door) 的狀態機例子, 門 (Door) 有兩種狀態, Open 和 Closed 。
sealed trait DoorState case object Open extends DoorState case object Closed extends DoorState case class DoorMoved(state: DoorState, times: Int) case class DoorNotMoved(state: DoorState, cmd: String) case class NotSupported(cmd: Any) class Door extends Actor with FSM[DoorState, Int] { this: Emitter => startWith(Closed, 0) when(Closed) { case Event("open", counter) => { emit(DoorMoved(Open, counter + 1)) goto(Open) using(counter + 1) } } when(Open) { case Event("close", counter) => { emit(DoorMoved(Closed, counter + 1)) goto(Closed) using(counter + 1) } } whenUnhandled { case Event(cmd @ ("open" | "close"), counter) => { emit(DoorNotMoved(stateName, "cannot %s door" format cmd)) stay } case Event(cmd, counter) => { emit(NotSupported(cmd)) stay } } def emit(event: Any) = emitter("destination") forwardEvent event }
來看看狀態的變化情況, " 門 (door)" 向名爲 "destination" 的信道發出 DoorMoved 事件。 DoorMoved 事件中包含當前狀態和移動計數。對無效移動嘗試,例如試圖打開已經打開的門,會發出 DoorNotMoved 事件。 信道接收者只是簡單的將事件信息打印到標準輸出。
class Destination extends Actor { def receive = { case event => println("received event %s" format event) } }
應用程序配置:
val system: ActorSystem = … val extension: EventsourcingExtension = … val destination = system.actorOf(Props(new Destination with Receiver with Confirm)) extension.channelOf(DefaultChannelProps(1, destination).withName("destination")) extension.processorOf(Props(new Door with Emitter with Eventsourced { val id = 1 } )) extension.recover() val door = extension.processors(1)
我們就可以開始發送事件消息到 door :
door ! Message("open") door ! Message("close")
標準輸出會打印出:
received event DoorMoved(Open,1) received event DoorMoved(Closed,2)
當試圖嘗試一個無效的狀態與變化
door ! Message("close")
信道 destination 將獲得DoorNotMoved事件:
received event DoorNotMoved(Closed,cannot close door)
重新啓動示例應用程序將恢復門的狀態,所以
door ! Message("open") door ! Message("close")
會產生
received event DoorMoved(Open,3) received event DoorMoved(Closed,4)
從本節中的代碼在FsmExample.scala ( 有少量修改 ) 。
這一節使用 Akka 集羣 技術將前面例子中的 Door 狀態機改造爲高可用性系統。 該Door狀態機在整個集羣中是單子對象,由一組NodeActor管理。每個集羣節點都有一個NodeActor偵聽集羣中的事件。如果某一個NodeActor成爲 主節點 (Master) 它創建並恢復一個 Door 實例。其他 NodeActor 保留一個主節點上 Door 實例的遠程引用。
客戶端與 Door 單子實例通過集羣的 NodeActor 交互,發送("open"或"close" )命令。集羣中任何節點上NodeActor都會接收命令,不僅僅是主節點。NodeActor將這些命令封裝成Message轉發給Door對象。 由Door發出的事件Message 由命名信道 "destination" 信道發送個目標接收者 (Destination) 。目標接收者 (Destination) 根據收到的事件創建相應,併發送該響應給初始發送者。 運行 Destination 的 Actor 的應用程序不是集羣的一部分,但是是一個獨立的遠程應用程序。它也同集羣節點一樣使用日誌(在本例中是 SPOF ,將來的版本會使用分佈式日誌)。
當主節點崩潰,集羣中的另一個節點會成爲主節點並恢復 Door 狀態機。其餘從節點更新其遠程引用指向新的主節點上的Door 實例。
從本節代碼包含在ClusterExample.scala ,使用的配置文件journal.conf和cluster.conf 。 對於示例代碼的詳細說明,請參考代碼中的註釋。 若要從 SBT 分佈式示例應用程序,首先啓動承載的應用程序 Destination 的 Actor 和日誌:
> run-main org.eligosource.eventsourced.example.DestinationApp
然後啓動集羣的第一個種子節點
> run-main org.eligosource.eventsourced.example.NodeApp 2561
然後啓動集羣的第二個種子節點
> run-main org.eligosource.eventsourced.example.NodeApp 2562
最後啓動的第三個集羣種子節點
> run-main org.eligosource.eventsourced.example.NodeApp
上面的命令要求你在eventsourced-examples項目。你可以在SBT中切換到這個工程
> project eventsourced-examples
第一個種子節點最有可能成爲主節點,它在標準輸出打印:
MASTER: recovered door at akka://[email protected]:2561
其他從節點輸出:
SLAVE: referenced door at akka://[email protected]:2561
所有節點都會提示用戶輸入一個門操作命令:
command (open|close):
現在,我們在最後啓動的一個羣集節點(從節點)上輸入命令:
Door 單子實例的初始狀態是關閉狀態。敲入 open 命令打開它:
command (open|close): open moved 1 times: door now open
然後再關閉它:
command (open|close): close moved 2 times: door now closed
試圖關閉一個已經關上的門會導致錯誤:
command (open|close): close cannot close door: door is closed
現在,使用 ctrl^c 殺掉主節點 。 Door 單子實例也會被破壞。經過 1-2 秒後,新的主節點已由集羣決定。新的主節點會恢復 event-sourced Door 子實例。從節點將更新其對 Door 的遠程引用 。 要驗證 Door 已經恢復正常,再次打開門:
command (open|close): open moved 3 times: door now open
你可以看到, Door狀態(其中包含了過去的移動次數)已從故障中正確恢復。
Eventsourced 庫還定義了組播 (Multicast)處理器 , 用來將收到的事件消息轉發給多個目標。 相比讓多個 Eventsourced 處理器接收相同的事件消息,使用 多播 Multicast 處理器更爲優化。 使用多播處理器,接收的事件消息只會記入日誌一次,而使用 n Eventsourced 處理器的消息將會記入日誌 n 次(每個處理器一次)。進行了如果有大量的接收目標,使用 Multicast 處理器可以顯著節省磁盤空間並提高吞吐量。
應用程序可以用定義在包core 中的多播工廠方法創建 Multicast 處理器。
// … import org.eligosource.eventsourced.core._ val extension: EventsourcingExtension = … val processorId: Int = … val target1: ActorRef = … val target2: ActorRef = … val multicast = extension.processorOf(Props(multicast(processorId, List(target1, target2))))
這等效於
val multicast = extension.processorOf(Props(new Multicast(List(target1, target2), identity) with Eventsourced { val id = processorId } ))
應用程序如果想在MessageS 被轉發到目標之前修改接收的事件,可以指定一個transformer的函數。
val transformer: Message => Any = msg => msg.event val multicast = extension.processorOf(Props(multicast(1, List(target1, target2), transformer)))
在上面的例子中,transformer 函數從接收到的事件Message提取的event。如果transformer函數沒有指定,則默認爲identity函數。還有一個 Multicast 工廠方法decorator,這個方法用來創建只有一個目標接收者的組播處理器。
-
Commercial support byEligotech B.V.