持久化
????當(dāng)我們在集群系統(tǒng)中,一臺機器向另一臺機器發(fā)送一段數(shù)據(jù),負責(zé)接收的機器在接收數(shù)據(jù)前突然宕機,就會造成數(shù)據(jù)丟失無法恢復(fù)。Akka實現(xiàn)了對actor 持久化的方法來恢復(fù)數(shù)據(jù)。
????????????Akka持久化使有狀態(tài)的actor能保留它的內(nèi)部狀態(tài),因此我們不會因為JVM崩潰、監(jiān)管者引起或集群中遷移導(dǎo)致數(shù)據(jù)丟失無法恢復(fù)而尷尬,Akka持久化可以幫助我們恢復(fù)actor。
? ? ? ? ? ? 持久化的什么?
? ? ? ? ? ? 持久化的是actor內(nèi)部狀態(tài)的變化,并且這些變化只是附加到原有的存儲上。
? ??????????actor是如何進行恢復(fù)的呢?
? ??????????我們可以通過將保存的變化進行重放,從而使它們可以重建其內(nèi)部狀態(tài)。當(dāng)重放的內(nèi)容龐大時會需要很多時間,因此Akka提供了快照功能將重放記錄分解從而減少恢復(fù)時間。
????????????另外一個重點是Akka持久化也提供了“至少一次消息傳遞語義”的點對點通信來保證消息不丟失。
依賴
使用Akka持久化,要在你的項目中添加以下依賴:
libraryDependencies += "com.typesafe.akka" %% "akka-persistence" % "2.5.11"
Akka持久化還提供了一些內(nèi)置的持久化插件,包括基于內(nèi)存堆的日志、基于本地文件系統(tǒng)的快照存儲以及基于LevelDB的日志。
基于LevelDB的插件需要以下額外的依賴:
libraryDependencies += "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"
體系結(jié)構(gòu)
1. PersistentActor(持久化actor):是一種特殊的帶有內(nèi)部狀態(tài)的Actor,既可以執(zhí)行命令又能以事件來源模式來進行內(nèi)部狀態(tài)持久化的。若因系統(tǒng)崩潰、人為終結(jié)等,系統(tǒng)在重啟后Actor通過之前持久化的信息可以恢復(fù)之前的狀態(tài)。
2.?AtLeastOnceDelivery(至少一次傳遞):消息傳遞的機制,意味著每條應(yīng)用了這種機制的消息潛在的存在多次投遞嘗試并保證至少會成功一次。就是說這條消息可能會重復(fù)但是不會丟失。
3. AsyncWriteJournal(異步存儲日志): 將發(fā)送給持久化Actor的消息序列異步存儲到日志中。日志為每條消息維護一個不斷增加的序列號。日志存儲的底層實現(xiàn)是可插拔的。Akka的持久化擴展自帶一個叫做"leveldb",向本地文件系統(tǒng)寫入的日志插件。Akka社區(qū)里還有更多日志存儲插件提供。
4. Snapshot store(快照存儲):對持久化Actor或持久化視圖的內(nèi)部狀態(tài)的快照進行持久化。快照用于優(yōu)化回復(fù)的時間。快照存儲的底層是可插拔的。Akka持久化擴展自帶一個向本地文件系統(tǒng)寫入的“本地”快照存儲插件。Akka社區(qū)里還有更多快照存儲插件提供。
5.?Event sourcing(事件來源):把使?fàn)顟B(tài)產(chǎn)生變化的事件按發(fā)生時間順序持久化,而不是把當(dāng)前整個狀態(tài)存儲起來。在恢復(fù)狀態(tài)時把日志中這些事件按原來的時間順序重演一遍回到原來的狀態(tài)。
下面我們根據(jù)持久化的體系結(jié)構(gòu)來詳細地介紹
事件來源Event sourcing
事件來源背后的基本思想其實很簡單,當(dāng)一個持久化actor接收到一個(非持久化)命令,首先它要驗證(這個命令)是否可以運用到當(dāng)前狀態(tài)。
如果命令驗證成功,根據(jù)這個命令產(chǎn)生一個事件。在事件成功持久化之后,可以用來改變actor的狀態(tài)。
當(dāng)持久化actor需要恢復(fù)時,因為之前已經(jīng)驗證過可以運用到當(dāng)前狀態(tài),我們可以直接將持久化的事件進行重放。
文章推薦:Events As First-Class Citizens
Akka持久化通過PersistentActor支持事件來源。actor可以使用persist方法持久化和處理事件。通過實現(xiàn)receiveRecover和receiveCommand定義PersistentActor的行為。下面的示例演示了這一點。
本例子中定義了Cmd和Evt兩種數(shù)據(jù)類型,Cmd代表命令,Evt代表事件
receiveRecover方法是在恢復(fù)過程中處理事件和快照消息。
receiveCommand方法用來處理普通Actor的消息。在上面的示例中,如果actor收到的是命令的話會調(diào)用persist方法
persist方法是異步的方式持久化事件。它有兩個參數(shù) ,一個是事件 ,另一個是事件處理程序(event handler)。
事件處理程序是將之前持久化過的事件進行處理,該事件在內(nèi)部作為獨立消息發(fā)送回持久化actor 來使事件處理程序執(zhí)行,來改變或關(guān)閉持久化actor的狀態(tài)。持久化事件的發(fā)送者也是相應(yīng)命令的發(fā)送者,因此當(dāng)命令的發(fā)送者沒顯出時事件處理程序也可以回復(fù)。
當(dāng)使用persist方法來持久化事件時在調(diào)用和執(zhí)行相關(guān)事件處理程序的過程中,要保證持久化actor不會收到下一步的命令,否則會受到影響。當(dāng)在某個命令的上下文中多次調(diào)用persist方法時。這個過程中收到的消息一直被暫存直到presist方法運行結(jié)束。
如果實例化事件失敗,onPersistFailure方法將被調(diào)用(默認記錄為error),并且actor將無條件地被停止。如果持久化事件在存儲之前被拒絕,比如事件發(fā)生連續(xù)錯誤,onPersistRejected將被調(diào)用(默認記錄為warning)并且actor繼續(xù)下一條消息
運行該示例最簡單的方法是下載Typesafe Activator,并打開Akka Persistence Samples with Scala這個教程。它包含如何運行PersistentActorExample的說明。這個例子的源代碼可以在?Akka Samples Repository?中找到。
??? ??標(biāo)識符
? ?一個持久化actor必須有個標(biāo)識符 這個標(biāo)識符必須用persistenceId?方法來定義。
override def persistenceId = "my-stable-persistence-id"
? ? ?恢復(fù)
PersistentActor在啟動和重啟時通過重放之前持久化的日志消息來實現(xiàn)自動恢復(fù),如果在恢復(fù)過程中收到新的消息,會將新消息先存儲起來等恢復(fù)完成后,在收到新的消息。
可以限制同一時間并發(fā)的恢復(fù)的數(shù)量,來限制系統(tǒng)和后端的數(shù)據(jù)存儲不超載,如果超過限制actor將等待到其他恢復(fù)都完成后才開始。配置方式:
akka.persistence.max-concurrent-recoveries = 50
? ? ? ? 自定義恢復(fù)
在應(yīng)用程序中有時也需要依照客戶具體要求來恢復(fù),通過返回recovery?方法中的自定義Recovery對象來執(zhí)行自定義恢復(fù)。recovery?是PersistentActor的一個方法。
你可以使用SnapshotSelectionCriteria.None.?來跳過加載快照和重放所有事件。它用于將快照序列化格式變成互不相容的方式時。不適宜用于事件被刪除的情況下。
override?def?recovery = Recovery(fromSnapshot =?SnapshotSelectionCriteria.None)
另一個可能的自定義恢復(fù)是設(shè)置重放的上界,對debug很有幫助,使得actor僅在過去的某個點重放。
override def recovery = Recovery(toSequenceNr = 457L)
在PersistentActor的recovery?方法中返回Recovery.none()可以使恢復(fù)失效。
override def recovery = Recovery.none
? ??????恢復(fù)狀態(tài)
持久化actor可以通過以下方法查詢它自己的恢復(fù)狀態(tài)
def recoveryRunning: Boolean
def recoveryFinished: Boolean
持久化actor在回復(fù)完成后會收到一個特殊的RecoveryCompleted?消息。然后再執(zhí)行下一步操作
如果actor從日志中的恢復(fù)狀態(tài)有問題,onRecoveryFailure?會被調(diào)用(記錄為error)并且actor將被停止。
內(nèi)部暫存(stash)
持久化actor有一個私有的暫存用來緩存整個恢復(fù)過程中進來的消息或者暫存persist\persistAll方法持久化的事件。內(nèi)部暫存通過掛鉤到unstashAll?與普通暫存協(xié)作
你應(yīng)該控制消息的產(chǎn)出不要超過持久化actor的處理能力,否則暫存消息的數(shù)量將無限增長。所以我們要在mailbox配置中定義暫存的容量來保護暫存并防止發(fā)生OutOfMemoryError?
akka.actor.default-mailbox.stash-capacity=10000
注意,如果你有很多持久化actor,要定義一個小的暫存容量,防止占用過多的內(nèi)存
持久化actor定義了三個策略來處理內(nèi)部暫存容量超出的故障。默認的溢出策略是ThrowOverflowExceptionStrategy,具體內(nèi)容是丟棄當(dāng)前的信息,拋出StashOverflowException異常,造成actor重啟。
你可以覆蓋internalStashOverflowStrategy?方法為了“獨特的”持久化actor來返回DiscardToDeadLetterStrategy?或者ReplyToStrategy?或者通過提供FQCN(Fully Qualified Class Name完全限定類名)來給所有的持久化actor來定義“默認值”。
在persistence?的配置中:
akka.persistence.internal-stash-overflow-strategy="akka.persistence.ThrowExceptionConfigurator"
DiscardToDeadLetterStrategy?策略也有一個打包好的配akka.persistence.DiscardConfigurator.
你也可以查詢默認策略:
Persistence(context.system).defaultInternalStashOverflowStrategy?
放寬的局部一致性要求和高吞吐量的用例
如果面臨放寬的局部一致性要求和高吞吐量,有時PersistentActor及其persist在處理大量涌入的命令時可能會不夠,有時你可能會放寬一致性要求——例如你會想要盡可能快速地處理命令,假設(shè)事件最終會持久化并在后臺恰當(dāng)處理,并在需要時追溯性地回應(yīng)持久性故障。
persistAsync方法提供了一個工具,用于實現(xiàn)高吞吐量的持久化actor。當(dāng)日志仍在致力于持久化和執(zhí)行用戶事件回調(diào)代碼時,它不會暫存傳入的命令。
推遲操作,直到持久化處理程序已經(jīng)執(zhí)行
PersistentActor?提供了一個實用的方法deferAsync(延遲異步),它工作起來類似于persistAsync但是不持久化傳遞過的事件,它將保留在內(nèi)存中,并在調(diào)用處理程序時使用。建議將其用于讀取操作,以及在domain模型中沒有相應(yīng)事件的操作。
請注意,sender()在處理程序回調(diào)中是安全的,將指向該命令的原始發(fā)送方,該命令將調(diào)用這個deferAsync處理程序。
持久化嵌套調(diào)用
可以在各自的回調(diào)塊中調(diào)用persistAsync?和persist,它們將適當(dāng)?shù)乇A艟€程安全性(包括sender的正確值)和存儲保證。
在下面的示例中,將發(fā)出兩個持久調(diào)用,每個調(diào)用都在其回調(diào)中發(fā)出另一個持久化調(diào)用
向PersistentActor發(fā)送a和b命令,執(zhí)行順序:
首先“outer層”的持久化調(diào)用先被發(fā)出并且它們的回調(diào)被應(yīng)用。一旦這個事件在日志(journal)中被確認持久化,inner的回調(diào)將被調(diào)用。只有這些所有的處理程序成功調(diào)用,下一個命令才會被傳遞給持久化actor。換句話說,初始調(diào)用在外部層中的persist()所保證的傳入命令的存儲(stash)被擴展,直到所有嵌套的持久化回調(diào)都被處理。
故障
如果事件的持久化失敗,將會調(diào)用onPersistFailure(在默認情況下記錄錯誤),并且將無條件地停止actor。
因為日志是不可用的,所以當(dāng)持久化失敗重啟是很困難的,最好是停止該角色,在退出超時之后重新啟動它。akka.pattern.BackoffSupervisoractor支持這重啟
如果事件的持久化在存儲之前被拒絕,將會調(diào)用onpersistreject(在默認情況下記錄一個警告),并且actor?將繼續(xù)使用下一個消息
如果在啟動actor時從日志中恢復(fù)actor?的狀態(tài)有問題,就會調(diào)用onRecoveryFailure(在默認情況下記錄錯誤),并停止該actor。
Atomic?寫入
通過使用persistAll或persistAllAsync方法來原子地存儲多個事件。這意味著傳遞給該方法的所有事件都被存儲,如果出現(xiàn)錯誤,則不會存儲它們。
使用persistAll來只持久化事件的子集,因此一個持久化actor的恢復(fù)將永遠不會被部分地完成。
一些日志在各自的事件中不支持原子寫入而且它們反對使用persistAll命令。也就是說onPersistRejected是調(diào)用一個異常(類似?UnsupportedOperationException).
Batch 寫入
為了在使用persistAsync時優(yōu)化吞吐量,在將事件寫入日志之前,一個持久的參與者內(nèi)部批量事件將被存儲在高負載下。批量大小是由日志往返期間發(fā)出的事件數(shù)量動態(tài)決定的。在將一個批次發(fā)送到日志后,在確認收到之前的批次之前,不能再發(fā)送批次。批處理寫入從來都不是基于時間的,這使得延遲至少是最小的。
消息刪除
可以將所有消息(由單個持久actor記錄的日志)刪除到指定的序列號。持久化actor可以調(diào)用deleteMessages方法
deletemessage請求的結(jié)果向持久化actor發(fā)出信號,如果刪除成功,則發(fā)出DeleteMessagesSuccess?如果發(fā)送失敗則發(fā)出DeleteMessagesFailure
即使所有的消息在deleteMessages調(diào)用后被刪除,消息刪除也不會影響日志的最高序列號
持久化狀態(tài)處理
在PersistentActor中重寫的故障處理程序的回調(diào)是顯式。這些處理程序的默認實現(xiàn)發(fā)出一個日志消息(persist的error/恢復(fù)故障/其他warning)并記錄失敗的原因和導(dǎo)致失敗的消息。
對于決定性的故障(例如恢復(fù)或持久化事件失敗),在故障處理程序調(diào)用之后持久化actor將被停止。這是因為如果底層的日志發(fā)送持久化故障信號,它很可能要么完全失敗,要么超負荷,重新啟動,并試圖再次堅持這個事件,這不會幫助日志恢復(fù),而會引起?Thundering herd problem,?因為許多持久化actor將重新啟動并嘗試再次持久化他們的事件。
使用BackoffSupervisor?(在?Failures中描述)它實現(xiàn)了一個指數(shù)回退策略,該策略允許日志在持久化actor的重新啟動之間恢復(fù)更多的喘息空間。
安全地關(guān)閉持久化actor
對于普通的actor來說,可以接受使用特殊的PoisonPill?消息來向一個actor發(fā)出信號,當(dāng)它接收到這個信息時,它應(yīng)該停止自己的動作,且actor自己無法阻止。
注意:在使用PersistentActor時,(在調(diào)用持久化處理程序之前)actor在它處理其他需要放入暫存的消息之前,可能收到或處理PoisonPill?,造成actor提前關(guān)閉。
重放過濾器
當(dāng)多個寫入者(即多個持久化actor實例)用相同的序列號來記錄不同的消息,可能會有事件流被破壞。在這種情況下,您可以配置如何在恢復(fù)時過濾來自多個寫入者的重播消息。
在你的配置中akka.persistence.journal.xxx下。replay-filter部分(xxx是您的日志插件id),您可以從以下值中選擇回放過濾器模式:
????????repair-by-discard-old
????????fail
????????warn
????????off
快照
當(dāng)你使用actor時,你可能要注意有些actor可能會積累非常長的事件日志,并經(jīng)歷較長的恢復(fù)時間。正確的方法是分裂成一組較短的actor來大幅度減少恢復(fù)時間。
持久actor可以通過調(diào)用saveSnapshot方法來保存內(nèi)部狀態(tài)的快照。如果保存快照成功,持久化actor將接收到SaveSnapshotSuccess消息,否則是SaveSnapshotFailure消息
如果沒有指定,他們默認為SnapshotSelectionCriteria.Latest?(最新的快照)。若要禁用基于快照的恢復(fù),應(yīng)用程序應(yīng)使用SnapshotSelectionCriteria.None。如果已保存的快照沒有匹配指定的SnapshotSelectionCriteria,恢復(fù)時將重播所有日志消息。
快照刪除
持久化actor可以通過調(diào)用deleteSnapshot方法來刪除單個快照,該方法使用快照的時間戳。
如果大量刪除一個范圍內(nèi)的與SnapshotSelectionCriteria匹配的快照,持久化actor可以使用deleteSnapshots
快照狀態(tài)處理
保存和刪除快照也可以有成功或失敗,此信息通過如下表所示的狀態(tài)消息反饋給持久actor
如果失敗的消息actor沒有處理,每個傳入的失敗消息將記錄一個默認的警告日志消息。在成功消息上沒有執(zhí)行默認操作,但是您可以自由地處理它們,例如,為了刪除在內(nèi)存中快照,或者在失敗的情況下再次嘗試保存快照。
至少一次傳遞
要發(fā)送至少一次發(fā)送語義到目的地的消息,您可以在發(fā)送端對您的PersistentActor混合AtLeastOnceDelivery特性。當(dāng)它們在可配置超時內(nèi)未被確認時,它負責(zé)重新發(fā)送消息。
發(fā)送actor的狀態(tài)包括未被接收方確認的消息被發(fā)送的狀態(tài)必須是持久的,因此它可以在方送的actor或jvm崩潰時幸存下來。AtLeastOnceDelivery?特性堅持傳遞消息的意圖,并接收到確認信息。
deliver方法用于將消息發(fā)送到目的地。當(dāng)目的地已回復(fù)一條確認消息,調(diào)用confirmDelivery方法。
傳遞和確認傳遞之間的關(guān)系
要將消息發(fā)送到目標(biāo)路徑,請在持久化發(fā)送消息的意圖之后使用deliver?方法。
目的地的actor必須發(fā)送回一個確認消息,當(dāng)發(fā)送方的actor收到確認消息 應(yīng)該將消息傳送成功這個事實持久化,然后調(diào)用confirmDelivery方法
如果持久化actor當(dāng)前沒有恢復(fù),則deliver?方法將向目標(biāo)參與者發(fā)送消息。當(dāng)恢復(fù)時,消息將被緩沖,直到它們被確認使用confirmDelivery.。一旦恢復(fù)完成,在整個恢復(fù)過程中如果有未解決的消息沒有被確認,在發(fā)送任何其他消息之前,持久參與者將重新發(fā)送這些消息。
傳遞要求一個deliveryIdToMessage?函數(shù)通過在消息中提供deliveryId?,因此deliver?和confirmDelivery?創(chuàng)建關(guān)系是可能的。deliveryId?必須往返于傳遞。在接收到消息后,目標(biāo)actor將把相同的deliveryId包裹在確認消息中,并返回給發(fā)送方。發(fā)送方隨后將使用它調(diào)用confirmDelivery方法來完成傳遞程序。
deliveryId是無間隙嚴格單調(diào)遞增序列號。所有目標(biāo)actor將使用相同的序列,即當(dāng)發(fā)送到多個目標(biāo)時會看到序列中的空白。所以不可能使用自定義的deliveryId。然而你可以在消息中發(fā)送自定義關(guān)聯(lián)標(biāo)識符到目的地。然后,您必須保留內(nèi)部deliveryId(傳遞到deliveryIdToMessage函數(shù))和自定義關(guān)聯(lián)id之間的映射(傳遞到消息中)。您可以通過在Map(correlationId -> deliveryId)中存儲這樣的映射來實現(xiàn)這一點,在您的消息的接收方以您的自定義相關(guān)id進行回復(fù)之后,您可以從該映射中檢索到要傳遞的deliveryId,并將其傳遞到confirmDelivery?方法中。
AtLeastOnceDelivery?特性有由未經(jīng)證實的消息和一個序列號組成的一個狀態(tài),它并不存儲這個狀態(tài)本身。你必須持久化從你的PersistentActor調(diào)用deliver和confirmDelivery所對應(yīng)的事件,從而可以在PersistentActor的恢復(fù)階段調(diào)用相同的方法恢復(fù)狀態(tài)。有時,這些事件可以從其他業(yè)務(wù)級別的事件中派生出來,有時您必須創(chuàng)建單獨的事件。在恢復(fù)過程中deliver的調(diào)用不會發(fā)出消息,但如果沒有匹配的confirmDelivery執(zhí)行,它將稍后發(fā)送。
getDeliverySnapshot和setDeliverySnapshot提供支持快照功能。AtLeastOnceDeliverySnapshot包含完整的投遞狀態(tài),包括未經(jīng)確認的消息。如果你需要一個自定義的快照保存actor其他部分的狀態(tài),你還必須包括AtLeastOnceDeliverySnapshot,它使用protobuf序列化,即利用Akka的通用序列化機制。最簡單的方法是將AtLeastOnceDeliverySnapshot中的字節(jié)作為blob包含在你自定義的快照中。
重新傳遞嘗試之間的間隔是由redeliverInterval方法定義的,默認值是由akka.persistence.at-least-once-delivery.redeliver-interval來配置,可以在實現(xiàn)類中重寫該方法來返回非默認值。
在每次重新發(fā)送爆發(fā)時將發(fā)送的消息的最大數(shù)量由redeliveryBurstLimit方法定義(爆發(fā)的頻率是重發(fā)間隔的一半)。如果有很多未確認的消息(例如,如果目的地很長一段時間都沒有),這有助于防止大量的消息同時發(fā)送。默認值可以被akka.persistence.at-least-once-delivery.redelivery-burst-limit來配置,可以在實現(xiàn)類中重寫該方法來返回非默認值。
在進行n次嘗試之后,有一個AtLeastOnceDelivery.UnconfirmedWarning消息發(fā)送到self,重新發(fā)送將仍然繼續(xù),但是你可以選擇調(diào)用confirmDelivery?來取消重新發(fā)送。傳遞的數(shù)量嘗試之前發(fā)出的警告是由warnAfterNumberOfUnconfirmedAttempts定義方法。默認值可以被akka.persistence.at-least-once-delivery.warn-after-number-of-unconfirmed-attempts可以在實現(xiàn)類中重寫該方法來返回非默認值
AtLeastOnceDelivery?特性將消息保存在內(nèi)存中,直到確認其成功交付。actor能保留在內(nèi)存中的未經(jīng)確認的消息的最大數(shù)目限制是由maxUnconfirmedMessages方法定義的。如果超過了此限制deliver方法將不會接受更多的消息,它將拋出AtLeastOnceDelivery.MaxUnconfirmedMessagesExceededException異常。。可以用akka.persistence.at-least-once-delivery.max-unconfirmed-messages配置其默認值。可以用實現(xiàn)類重寫該方法來返回非默認值。
事件適配器
在長期運行的項目中,使用事件源有時需要將數(shù)據(jù)模型完全從domain?模型中分離出來。
事件適配器在以下情況提供幫助:
Version Migrations版本遷移:
存在事件存儲在版本1應(yīng)該被“上拋”到版本2,這樣的過程需要實際代碼來實現(xiàn),不只是序列化層的變化。在這些場景下,toJournal?函數(shù)通常是特性函數(shù),然而v1.Event=>v2.Event被formJournal實現(xiàn),在fromJournal ?方法中執(zhí)行必要的映射。這種技術(shù)有時被稱為其他CQRS庫中的“向上轉(zhuǎn)換”。
Separating Domain and Data models?分離Domain?和Data模型
由于事件適配器,可以將domain模型與用于在日志中持久存儲數(shù)據(jù)的模型完全分離,例如可能希望在domain模型中使用case類,但是,將它們的協(xié)議緩沖區(qū)(或任何其他二進制序列化格式)保留到日志中。一個簡單的toJournal:MyModel=>MyDataModel?和?fromJournal:MyDataModel=>MyModel適配器可以用來實現(xiàn)
Journal Specialized Data Types?日志專門的數(shù)據(jù)類型:
公開底層日志中已知的數(shù)據(jù)類型,例如數(shù)據(jù)用JSON來存儲可以寫一個事件適配器toJournal:Any=>JSON。日志可以直接存儲json,而不是將對象序列化為二進制表示。
可以將多個適配器綁定到一個類以進行恢復(fù),在這種情況下,所有綁定適配器的fromJournal方法將應(yīng)用于給定的匹配事件(按照配置中的定義順序)。由于每個適配器可能從0返回到n個適應(yīng)事件(稱為EventSeq)每個適配器都可以對事件進行調(diào)查,如果它確實需要調(diào)整,它將返回相應(yīng)的事件(s)。在此修改過程中沒有任何可貢獻的其他適配器只返回EventSeq.empty。在重放過程中,改編事件按次序傳遞給PersistentActor?。
注意
對于更高級的模式演化技術(shù)參考Persistence - Schema Evolution文檔
持久化有限狀態(tài)機(FSM)
PersistentFSM?以FSM的方式處理傳入消息。它的內(nèi)部狀態(tài)是作為一系列變化被持久化的,后來被稱為domain事件。傳入消息、FSM狀態(tài)和轉(zhuǎn)換之間的關(guān)系,domain事件的持久性是由DSL定義的。
一個簡單的例子
為了演示PersistentFSM特性的特性,請考慮一個代表Web store客戶的actor。我們的“WebStoreCustomerFSMActor”的合同是
AddItem?在客戶向購物車中添加商品時發(fā)送。
Buy?當(dāng)客戶完成購買時。
Leave當(dāng)顧客離開商店時沒有購買任何東西。
GetCurrentCart?允許查詢客戶購物車的當(dāng)前狀態(tài)。
顧客的幾種狀態(tài):
LookingAround客戶正在瀏覽網(wǎng)站,但沒有添加任何東西到購物車
Shopping?客戶最近將商品添加到購物車中。
Inactive?顧客購物車里有商品,但最近沒有添加任何東西。
Paid?顧客已經(jīng)購買了商品。
客戶的操作被“記錄”為連續(xù)的“domain事件”序列。為了恢復(fù)最新客戶的狀態(tài),這些事件在一個actor的開始時重播。
使用snapshot-after進行定期快照
如果你把reference.conf中的下列flag改編,你能夠周期性的調(diào)用PersistentFSM?中的saveStateSnapshot()
akka.persistence.fsm.snapshot-after = 1000
這意味著在序列號達到1000的倍數(shù)后調(diào)用saveStateSnapshot()。
存儲插件
在Akka持久性擴展中,用于日志和快照存儲的存儲備份是可插入的。在Akka社區(qū)項目頁面上有一個持久性日志和快照存儲插件的目錄,請參閱社區(qū)插件。
當(dāng)一個持久化actor定義自己的一組插件時,插件可以被“默認”選擇為所有持久化actor,或者“單獨”
當(dāng)一個持久的actor不重寫journalPluginId和snapshotPluginId方法時,持久性擴展將使用在reference.conf中配置的“默認”日志和快照存儲插件。
但是,這些條目是空的””,并且需要在用戶application.conf中通過覆蓋來顯式的用戶配置。一個日志插件的例子,把消息寫入LevelDB ,詳情請查看看?Local LevelDB journal.對于快照存儲插件的示例,該插件將快照寫入到本地文件系統(tǒng)的單個文件中,詳情請查看看?Local snapshot store.
應(yīng)用程序可以通過實現(xiàn)插件API并通過配置激活它們來提供自己的插件。插件開發(fā)需要以下導(dǎo)入:
急切地初始化持久化插件
默認情況下,持久化插件根據(jù)自己的用處靈活的啟動。急切地啟動某個插件可能是有益的。為了做到這些,你應(yīng)該先添加akka.persistence.Persistence在akka.extensions關(guān)鍵字下。在akka.persistence.journal.auto-start-journals和?akka.persistence.snapshot-store.auto-start-snapshot-stores下指定您希望自動啟動的插件的id。例如,如果您希望對leveldb日志插件和本地快照存儲插件進行急切的初始化,您的配置應(yīng)該如下所示:
日志插件API
日志插件繼承AsyncWriteJournal
AsyncWriteJournal?是一個actor,實現(xiàn)方法
如果存儲后端API僅支持同步,阻塞寫入,方法實現(xiàn)如下,
日志插件還必須實現(xiàn)在AsyncRecovery中定義的用于重放和序列號恢復(fù)的方法,實現(xiàn)方法
日志插件實例是一個actor,因此與persist actor的請求相對應(yīng)的方法將按順序執(zhí)行。
它可以委托給異步庫,或者委托給其他參與者來實現(xiàn)并行。
日志插件類必須有一個帶有這些簽名的構(gòu)造函數(shù)。
????????·?一個com.typesafe.config.Config參數(shù)和一個配置路徑的String參數(shù)
????????· 一個com.typesafe.config.Config參數(shù)
? ? ? ? ·?沒有參數(shù)的構(gòu)造函數(shù)
actor系統(tǒng)配置的插件部分將在配置構(gòu)造函數(shù)參數(shù)中傳遞。插件配置路徑在String參數(shù)中傳遞。
plugin-dispatcher是插件actor的分配器,如果沒有指定,則默認為akka.persistence.dispatchers.default-plugin-dispatcher.
不要在系統(tǒng)默認調(diào)度器上運行日志任務(wù),因為這可能會導(dǎo)致其他任務(wù)的匱乏。
快照存儲插件API
一個快照存儲插件必須擴展SnapshotStore?actor并實現(xiàn)以下方法:一個快照存儲插件必須擴展SnapshotStore actor并實現(xiàn)以下方法:實現(xiàn)方法
快照存儲實例是一個actor,因此與persist actor的請求相對應(yīng)的方法將按順序執(zhí)行。
它可以委托給異步庫,或者委托給其他參與者來實現(xiàn)并行。
快照存儲插件類須有一個帶有這些簽名的構(gòu)造函數(shù)。
????????1.一個com.typesafe.config.Config參數(shù)和一個配置路徑的String參數(shù)
????????2.一個com.typesafe.config.Config參數(shù)
????????3.沒有參數(shù)的構(gòu)造函數(shù)
actor系統(tǒng)配置的插件部分將在配置構(gòu)造函數(shù)參數(shù)中傳遞。插件配置路徑在String參數(shù)中傳遞。
plugin-dispatcher是插件actor的分配器,如果沒有指定,則默認為akka.persistence.dispatchers.default-plugin-dispatcher.
不要在系統(tǒng)默認調(diào)度器上運行快照存儲任務(wù),因為這可能會導(dǎo)致其他任務(wù)的匱乏。
TCK插件
為了幫助開發(fā)人員構(gòu)建正確和高質(zhì)量的存儲插件。我們提供了Technology Compatibility Kit(技術(shù)兼容性工具包)
TCK可以從Java和Scala項目中使用。要測試您的實現(xiàn)(獨立于語言),您需要包括akka-persistence-tck依賴:
"com.typesafe.akka" %% "akka-persistence-tck" % "2.5.11" % "test"
要在測試套件中包含TCK測試,只需擴展所提供的JournalSpe
請注意,有些測試是可選的,并且通過覆蓋support…方法給TCK提供運行測試所需的信息。你可以使用布爾值或提供的CapabilityFlag.on?/?CapabilityFlag.off 值來實現(xiàn)這些方法
我們也提供了一個簡單的基準類JournalPerfSpec它包含了JournalSpec所擁有的所有測試,在打印性能統(tǒng)計數(shù)據(jù)時,還可以在日志上執(zhí)行一些較長的操作。雖然它不是為了提供一個合適的基準測試環(huán)境,但它可以用來對您的日志在最典型的情況下的性能有一個粗略的感覺。
為了將SnapshotStore TCK測試包含在測試套件中,只需擴展SnapshotStoreSpec:
如果你的插件需要一些設(shè)置情況下(啟動一個模擬數(shù)據(jù)庫,刪除臨時文件等等),你可以重寫beforeAll和afterAll方法,并在測試的生命周期中加入:
我們強烈建議在你的測試套件包括這些規(guī)格,因為它們涵蓋了您可能已經(jīng)忘記在從頭編寫插件時要測試的范圍廣泛的案例。
預(yù)先包裝好的插件
本地levelDB日志
levelDB日志插件的配置入口:akka.persistence.journal.leveldb它將消息寫入一個本地的LevelDB實例。通過定義配置屬性啟用此插件:
基于LevelDB的插件還需要以下附加的依賴聲明:
"org.fusesource.leveldbjni" ??% "leveldbjni-all" ??% "1.8"
LevelDB文件的默認位置是當(dāng)前工作目錄中一個名為journal的目錄。此位置可以由配置中指定的相對或絕對的路徑更改:
akka.persistence.journal.leveldb.dir = "target/journal"
用這個插件,每個actor系統(tǒng)可運行其自己私有的LevelDB實例。
LevelDB的一個特性是,刪除操作不會移除日志中的消息,而是為每個刪除的消息添加一個“墓碑”。在大量使用日志的情況下,特別是頻繁的刪除,這可能是一個問題,因為用戶可能發(fā)現(xiàn)自己在處理不斷增加的日志大小。為此,LevelDB提供了一個特殊的journal compaction函數(shù),它通過以下配置公開。
共享LevelDB日志
一個LevelDB實例還可以由多個actor系統(tǒng)(在相同或不同節(jié)點上)共享。它,例如,允許持久化actor進行故障轉(zhuǎn)移到備份節(jié)點,并從備份節(jié)點繼續(xù)使用共享的日志實例。
通過實例化SharedLeveldbStore actor可以啟動一個共享的LevelDB實例。
默認情況下,共享的實例將日志消息寫入到當(dāng)前的工作目錄中一個名為journal的本地目錄。可以通過配置更改存儲位置:
akka.persistence.journal.leveldb-shared.store.dir = "target/shared"
使用共享的LevelDB存儲的actor系統(tǒng)必須激活akka.persistence.journal.leveldb-shared插件。
akka.persistence.journal.plugin = "akka.persistence.journal.leveldb-shared"
這個插件必須通過注入(遠程)SharedLeveldbStore??actor引用來初始化。注入是通過以actor引用作為參數(shù)調(diào)用SharedLeveldbJournal.setStore方法完成的。
內(nèi)部日志命令(由持久化actor發(fā)送的)會被緩沖直到注入完成。注入是冪等的,即只有第一次的注入被使用。
本地快照存儲區(qū)
本地快照存儲插件配置條目是akka.persistence.snapshot-store.local?它將快照文件寫入到本地文件系統(tǒng)中。通過定義配置屬性來啟用這個插件。
默認的存儲位置是當(dāng)前工作目錄中一個名為snapshots?的目錄。這可以通過配置中指定的相對或絕對的路徑來更改
akka.persistence.snapshot-store.local.dir =?"target/snapshots"
默認的存儲位置是當(dāng)前工作目錄中一個名為snapshots?的目錄。這可以通過配置中指定的相對或絕對的路徑來更改
持久性插件代理
持久性插件代理允許共享日志和快照存儲在多個actor系統(tǒng)(在相同或不同的節(jié)點)。
例如,這允許持久化actor將故障轉(zhuǎn)移到備份節(jié)點,并繼續(xù)使用備份節(jié)點上的共享日志實例。代理的工作方式是將所有日志/快照存儲消息轉(zhuǎn)發(fā)到一個單獨的、共享的持久性插件實例,因此支持由代理插件支持的任何用例。
日志和快照存儲代理分別通過akka.persistence.journal.proxy和akka.persistence.snapshot-store.proxy配置來控制。
通過target-journal-plugin或target-snapshot-store-plugin來設(shè)置你想優(yōu)先使用的插件(例如akka.persistence.journal.leveldb)。start-target-journal和?start-target-snapshot-store關(guān)鍵字應(yīng)該在一個actor系統(tǒng)中設(shè)置成on——這是一個將實例化共享持久性插件的系統(tǒng)。
接下來,需要告訴代理如何找到共享插件。這個可以通過設(shè)置?target-journal-address 和 target-snapshot-store-address?配置關(guān)鍵字來完成或者以編程方式調(diào)用PersistencePluginProxy.setTargetLocation方法
自定義序列化
快照序列化和Persistent消息的有效載荷是可以通過Akka序列化基礎(chǔ)架構(gòu)配置的。例如,如果應(yīng)用程序想要序列化
????????????·有效載荷的MyPayload類型與自定義的MyPayloadSerializer
????????????·快照的類型MySnapshot與自定義的MySnapshotSerializer
在應(yīng)用程序配置中。如果未指定,則使用默認的序列化程序。
對于更高級的模式演化技術(shù),請參考?Persistence - Schema Evolution文檔
測試
運行測試時使用sbt的LevelDB默認設(shè)置,請確保在你的sbt項目中設(shè)置fork := true,否則你將看到一個UnsatisfiedLinkError。或者,你可以切換到一個LevelDB Java 端口,通過這樣的設(shè)置
akka.persistence.journal.leveldb.native = off
或
akka.persistence.journal.leveldb-shared.store.native = off
在Akka配置中。LevelDB 的Java端口僅用于測試目的。
還要注意,對于LevelDB Java端口,您需要以下依賴項:
"org.iq80.leveldb" ???????????% "leveldb" ?????????% "0.9" ?????????% "test"
配置
持久化模塊有幾個配置屬性,請參考?reference configuration.
多個持久性插件配置
默認情況下,持久化actor將使用“默認”的日志和快照存儲插件,這些插件配置在reference.conf配置資源中,
當(dāng)持久參與者重寫了journalPluginId和snapshotPluginId方法時,該actor將由這些特定的持久性插件而不是默認值服務(wù):
請注意,journalPluginId和snapshotPluginId必須引用正確配置的引用。帶有標(biāo)準類屬性的conf插件條目以及特定于這些插件的設(shè)置,也就是: