Akka 指南 之「持久化」

溫馨提示:Akka 中文指南的 GitHub 地址爲「akka-guide」,歡迎大家StarFork,糾錯。

持久化

依賴

爲了使用 Akka 持久化(Persistence)功能,你必須在項目中添加如下依賴:

<!-- Maven -->
<dependency>
  <groupId>com.typesafe.akka</groupId>
  <artifactId>akka-persistence_2.12</artifactId>
  <version>2.5.20</version>
</dependency>

<!-- Gradle -->
dependencies {
  compile group: 'com.typesafe.akka', name: 'akka-persistence_2.12', version: '2.5.20'
}

<!-- sbt -->
libraryDependencies += "com.typesafe.akka" %% "akka-persistence" % "2.5.20"

Akka 持久性擴展附帶了一些內置持久性插件,包括基於內存堆的日誌、基於本地文件系統的快照存儲和基於 LevelDB 的日誌。

基於 LevelDB 的插件需要以下附加依賴:

<!-- Maven -->
<dependency>
  <groupId>org.fusesource.leveldbjni</groupId>
  <artifactId>leveldbjni-all</artifactId>
  <version>1.8</version>
</dependency>

<!-- Gradle -->
dependencies {
  compile group: 'org.fusesource.leveldbjni', name: 'leveldbjni-all', version: '1.8'
}

<!-- sbt -->
libraryDependencies += "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"

示例項目

你可以查看「持久化示例」項目,以瞭解 Akka 持久化的實際使用情況。

簡介

Akka 持久性使有狀態的 Actor 能夠持久化其狀態,以便在 Actor 重新啓動(例如,在 JVM 崩潰之後)、由監督者或手動停止啓動或遷移到集羣中時可以恢復狀態。Akka 持久性背後的關鍵概念是,只有 Actor 接收到的事件才被持久化,而不是 Actor 的實際狀態(儘管也提供了 Actor 狀態快照支持)。事件通過附加到存儲(沒有任何變化)來持久化,這允許非常高的事務速率和高效的複製。有狀態的 Actor 通過將存儲的事件重放給 Actor 來恢復,從而允許它重建其狀態。這可以是更改的完整歷史記錄,也可以從快照中的檢查點開始,這樣可以顯著縮短恢復時間。Akka 持久化(persistence)還提供具有至少一次消息傳遞語義的點對點(point-to-point)通信。

  • 註釋:《通用數據保護條例》(GDPR)要求必須根據用戶的要求刪除個人信息。刪除或修改攜帶個人信息的事件是困難的。數據分解可以用來忘記信息,而不是刪除或修改信息。這是通過使用給定數據主體 ID(person)的密鑰加密數據,並在忘記該數據主體時刪除密鑰來實現的。Lightbend 的「GDPR for Akka Persistence」提供了一些工具來幫助構建支持 GDPR 的系統。

Akka 持久化的靈感來自於「eventsourced」庫的正式替換。它遵循與eventsourced相同的概念和體系結構,但在 API 和實現級別上存在顯著差異。另請參見「migration-eventsourced-2.3」。

體系結構

  • AbstractPersistentActor:是一個持久的、有狀態的 Actor。它能夠將事件持久化到日誌中,並能夠以線程安全的方式對它們作出響應。它可以用於實現命令和事件源 Actor。當一個持久的 Actor 啓動或重新啓動時,日誌消息將重播給該 Actor,以便它可以從這些消息中恢復其狀態。
  • AbstractPersistentActorAtLeastOnceDelivery:將具有至少一次傳遞語義的消息發送到目的地,也可以在發送方和接收方 JVM 崩潰的情況下發送。
  • AsyncWriteJournal:日誌存儲發送給持久 Actor 的消息序列。應用程序可以控制哪些消息是日誌記錄的,哪些消息是由持久 Actor 接收的,而不進行日誌記錄。日誌維護每一條消息上增加的highestSequenceNr。日誌的存儲後端是可插入的。持久性擴展附帶了一個leveldb日誌插件,它將寫入本地文件系統。
  • 快照存儲區(Snapshot store):快照存儲區保存持久 Actor 狀態的快照。快照用於優化恢復時間。快照存儲的存儲後端是可插入的。持久性擴展附帶了一個“本地”快照存儲插件,該插件將寫入本地文件系統。
  • 事件源(Event sourcing):基於上面描述的構建塊,Akka 持久化爲事件源應用程序的開發提供了抽象(詳見「事件源」部分)。

事件源

請參閱「EventSourcing」的介紹,下面是 Akka 通過持久的 Actor 實現的。

持久性 Actor 接收(非持久性)命令,如果該命令可以應用於當前狀態,則首先對其進行驗證。在這裏,驗證可以意味着任何事情,從簡單檢查命令消息的字段到與幾個外部服務的對話。如果驗證成功,則從命令生成事件,表示命令的效果。這些事件隨後被持久化,並且在成功持久化之後,用於更改 Actor 的狀態。當需要恢復持久性 Actor 時,只重播持久性事件,我們知道這些事件可以成功應用。換句話說,與命令相反,事件在被重播到持久的 Actor 時不會失敗。事件源 Actor 還可以處理不更改應用程序狀態的命令,例如查詢命令。

關於“事件思考”的另一篇優秀文章是 Randy Shoup 的「Events As First-Class Citizens」。如果你開始開發基於事件的應用程序,這是一個簡短的推薦閱讀。

Akka 持久化使用AbstractPersistentActor抽象類支持事件源。擴展此類的 Actor 使用持久方法來持久化和處理事件。AbstractPersistentActor的行爲是通過實現createReceiveRecovercreateReceive來定義的。這在下面的示例中進行了演示。

import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.persistence.AbstractPersistentActor;
import akka.persistence.SnapshotOffer;

import java.io.Serializable;
import java.util.ArrayList;

class Cmd implements Serializable {
  private static final long serialVersionUID = 1L;
  private final String data;

  public Cmd(String data) {
    this.data = data;
  }

  public String getData() {
    return data;
  }
}

class Evt implements Serializable {
  private static final long serialVersionUID = 1L;
  private final String data;

  public Evt(String data) {
    this.data = data;
  }

  public String getData() {
    return data;
  }
}

class ExampleState implements Serializable {
  private static final long serialVersionUID = 1L;
  private final ArrayList<String> events;

  public ExampleState() {
    this(new ArrayList<>());
  }

  public ExampleState(ArrayList<String> events) {
    this.events = events;
  }

  public ExampleState copy() {
    return new ExampleState(new ArrayList<>(events));
  }

  public void update(Evt evt) {
    events.add(evt.getData());
  }

  public int size() {
    return events.size();
  }

  @Override
  public String toString() {
    return events.toString();
  }
}

class ExamplePersistentActor extends AbstractPersistentActor {

  private ExampleState state = new ExampleState();
  private int snapShotInterval = 1000;

  public int getNumEvents() {
    return state.size();
  }

  @Override
  public String persistenceId() {
    return "sample-id-1";
  }

  @Override
  public Receive createReceiveRecover() {
    return receiveBuilder()
        .match(Evt.class, state::update)
        .match(SnapshotOffer.class, ss -> state = (ExampleState) ss.snapshot())
        .build();
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(
            Cmd.class,
            c -> {
              final String data = c.getData();
              final Evt evt = new Evt(data + "-" + getNumEvents());
              persist(
                  evt,
                  (Evt e) -> {
                    state.update(e);
                    getContext().getSystem().getEventStream().publish(e);
                    if (lastSequenceNr() % snapShotInterval == 0 && lastSequenceNr() != 0)
                      // IMPORTANT: create a copy of snapshot because ExampleState is mutable
                      saveSnapshot(state.copy());
                  });
            })
        .matchEquals("print", s -> System.out.println(state))
        .build();
  }
}

該示例定義了兩種數據類型,即CmdEvt,分別表示命令和事件。ExamplePersistentActor的狀態是包含在ExampleState中的持久化事件數據的列表。

持久化 Actor 的createReceiveRecover方法通過處理EvtSnapshotOffer消息來定義在恢復過程中如何更新狀態。持久化 Actor 的createReceive方法是命令處理程序。在本例中,通過生成一個事件來處理命令,該事件隨後被持久化和處理。通過使用事件(或事件序列)作爲第一個參數和事件處理程序作爲第二個參數調用persist來持久化事件。

persist方法異步地持久化事件,併爲成功持久化的事件執行事件處理程序。成功的持久化事件在內部作爲觸發事件處理程序執行的單個消息發送回持久化 Actor。事件處理程序可能會關閉持久的 Actor 狀態並對其進行改變。持久化事件的發送者是相應命令的發送者。這允許事件處理程序回覆命令的發送者(未顯示)。

事件處理程序的主要職責是使用事件數據更改持久的 Actor 狀態,並通過發佈事件通知其他人成功的狀態更改。

當使用persist持久化事件時,可以確保持久化 Actor 不會在persist調用和關聯事件處理程序的執行之間接收進一步的命令。這也適用於單個命令上下文中的多個persist調用。傳入的消息將被存儲,直到持久化完成。

如果事件的持久性失敗,將調用onPersistFailure(默認情況下記錄錯誤),並且 Actor 將無條件停止。如果在存儲事件之前拒絕了該事件的持久性,例如,由於序列化錯誤,將調用onPersistRejected(默認情況下記錄警告),並且 Actor 將繼續執行下一條消息。

運行這個例子最簡單的方法是自己下載準備好的「 Akka 持久性示例」和教程。它包含有關如何運行PersistentActorExample的說明。此示例的源代碼也可以在「Akka 示例倉庫」中找到。

  • 註釋:在使用getContext().become()getContext().unbecome()進行正常處理和恢復期間,還可以在不同的命令處理程序之間切換。要使 Actor 在恢復後進入相同的狀態,你需要特別注意在createReceiveRecover方法中使用becomeunbecome執行相同的狀態轉換,就像在命令處理程序中那樣。請注意,當使用來自createReceiveRecoverbecome時,在重播事件時,它仍然只使用createReceiveRecover行爲。重播完成後,將使用新行爲。

標識符

持久性 Actor 必須有一個標識符(identifier),該標識符在不同的 Actor 化身之間不會發生變化。必須使用persistenceId方法定義標識符。

@Override
public String persistenceId() {
  return "my-stable-persistence-id";
}
  • 註釋persistenceId對於日誌中的給定實體(數據庫表/鍵空間)必須是唯一的。當重播持久化到日誌的消息時,你將查詢具有persistenceId的消息。因此,如果兩個不同的實體共享相同的persistenceId,則消息重播行爲已損壞。

恢復

默認情況下,通過重放日誌消息,在啓動和重新啓動時自動恢復持久性 Actor。在恢復期間發送給持久性 Actor 的新消息不會干擾重播的消息。在恢復階段完成後,它們被一個持久的 Actor 存放和接收。

可以同時進行的併發恢復的數量限制爲不使系統和後端數據存儲過載。當超過限制時,Actor 將等待其他恢復完成。配置方式爲:

akka.persistence.max-concurrent-recoveries = 50
  • 註釋:假設原始發件人已經很長時間不在,那麼使用getSender()訪問已重播消息的發件人將始終導致deadLetters引用。如果在將來的恢復過程中確實需要通知某個 Actor,請將其ActorPath顯式存儲在持久化事件中。

恢復自定義

應用程序還可以通過在AbstractPersistentActorrecovery方法中返回自定義的Recovery對象來定製恢復的執行方式,

要跳過加載快照和重播所有事件,可以使用SnapshotSelectionCriteria.none()。如果快照序列化格式以不兼容的方式更改,則此選項非常有用。它通常不應該在事件被刪除時使用。

@Override
public Recovery recovery() {
  return Recovery.create(SnapshotSelectionCriteria.none());
}

另一種可能的恢復自定義(對調試有用)是在重播上設置上限,使 Actor 僅在“過去”的某個點上重播(而不是重播到其最新狀態)。請注意,在這之後,保留新事件是一個壞主意,因爲以後的恢復可能會被以前跳過的事件後面的新事件混淆。

@Override
public Recovery recovery() {
  return Recovery.create(457L);
}

通過在PersistentActorrecovery方法中返回Recovery.none()可以關閉恢復:

@Override
public Recovery recovery() {
  return Recovery.none();
}

恢復狀態

通過以下方法,持久性 Actor 可以查詢其自己的恢復狀態:

public boolean recoveryRunning();

public boolean recoveryFinished();

有時候,在處理髮送給持久性 Actor 的任何其他消息之前,當恢復完成時,需要執行額外的初始化。持久性 Actor 將在恢復之後和任何其他收到的消息之前收到一條特殊的RecoveryCompleted消息。

class MyPersistentActor5 extends AbstractPersistentActor {

  @Override
  public String persistenceId() {
    return "my-stable-persistence-id";
  }

  @Override
  public Receive createReceiveRecover() {
    return receiveBuilder()
        .match(
            RecoveryCompleted.class,
            r -> {
              // perform init after recovery, before any other messages
              // ...
            })
        .match(String.class, this::handleEvent)
        .build();
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(String.class, s -> s.equals("cmd"), s -> persist("evt", this::handleEvent))
        .build();
  }

  private void handleEvent(String event) {
    // update state
    // ...
  }
}

即使日誌中沒有事件且快照存儲爲空,或者是具有以前未使用的persistenceId的新持久性 Actor,Actor 也將始終收到RecoveryCompleted消息。

如果從日誌中恢復 Actor 的狀態時出現問題,則調用onRecoveryFailure(默認情況下記錄錯誤),Actor 將停止。

內部存儲

持久性 Actor 有一個私有存儲區,用於在恢復期間對傳入消息進行內部緩存,或者persist\persistAll方法持久化事件。你仍然可以從Stash接口use/inherit。內部存儲(internal stash)與正常存儲進行合作,通過unstashAll方法並確保消息正確地unstashed到內部存儲以維持順序保證。

你應該小心,不要向持久性 Actor 發送超過它所能跟上的消息,否則隱藏的消息的數量將無限增長。通過在郵箱配置中定義最大存儲容量來防止OutOfMemoryError是明智的:

akka.actor.default-mailbox.stash-capacity=10000

注意,其是每個 Actor 的藏匿存儲容量(stash capacity)。如果你有許多持久的 Actor,例如在使用集羣分片(cluster sharding)時,你可能需要定義一個小的存儲容量,以確保系統中存儲的消息總數不會消耗太多的內存。此外,持久性 Actor 定義了三種策略來處理超過內部存儲容量時的故障。默認的溢出策略是ThrowOverflowExceptionStrategy,它丟棄當前接收到的消息並拋出StashOverflowException,如果使用默認的監視策略,則會導致 Actor 重新啓動。你可以重寫internalStashOverflowStrategy方法,爲任何“單個(individual)”持久 Actor 返回DiscardToDeadLetterStrategyReplyToStrategy,或者通過提供 FQCN 爲所有持久 Actor 定義“默認值”,FQCN 必須是持久配置中StashOverflowStrategyConfigurator的子類:

akka.persistence.internal-stash-overflow-strategy=
  "akka.persistence.ThrowExceptionConfigurator"

DiscardToDeadLetterStrategy策略還具有預打包(pre-packaged)的伴生配置程序akka.persistence.DiscardConfigurator

你還可以通過 Akka 的持久性擴展查詢默認策略:

Persistence.get(getContext().getSystem()).defaultInternalStashOverflowStrategy();
  • 註釋:在持久性 Actor 中,應避免使用有界的郵箱(bounded mailbox),否則來自存儲後端的消息可能會被丟棄。你可以用有界的存儲(bounded stash)來代替它。

Relaxed 本地一致性需求和高吞吐量用例

如果面對relaxed本地一致性和高吞吐量要求,有時PersistentActor及其persist在高速使用傳入命令方面可能不夠,因爲它必須等到與給定命令相關的所有事件都被處理後才能開始處理下一個命令。雖然這種抽象在大多數情況下都非常有用,但有時你可能會面臨關於一致性的relaxed要求——例如,你可能希望儘可能快地處理命令,假設事件最終將在後臺被持久化並正確處理,如果需要,可以對持久性失敗進行逆向反應(retroactively reacting)。

persistAsync方法提供了一個工具來實現高吞吐量的持久 Actor。當日志仍在處理持久化and/or用戶代碼正在執行事件回調時,它不會存儲傳入的命令。

在下面的示例中,即使在處理下一個命令之後,事件回調也可以“任何時候”調用。事件之間的順序仍然是有保證的(evt-b-1將在evt-a-2之後發送,也將在evt-a-1之後發送)。

class MyPersistentActor extends AbstractPersistentActor {

  @Override
  public String persistenceId() {
    return "my-stable-persistence-id";
  }

  private void handleCommand(String c) {
    getSender().tell(c, getSelf());

    persistAsync(
        String.format("evt-%s-1", c),
        e -> {
          getSender().tell(e, getSelf());
        });
    persistAsync(
        String.format("evt-%s-2", c),
        e -> {
          getSender().tell(e, getSelf());
        });
  }

  @Override
  public Receive createReceiveRecover() {
    return receiveBuilder().match(String.class, this::handleCommand).build();
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder().match(String.class, this::handleCommand).build();
  }
}
  • 註釋:爲了實現名爲“命令源(command sourcing)”的模式,請立即對所有傳入消息調用persistAsync,並在回調中處理它們。
  • 警告:如果在對persistAsync的調用和日誌確認寫入之間重新啓動或停止 Actor,則不會調用回調。

推遲操作,直到執行了前面的持久處理程序

有時候,在處理persistAsyncpersist時,你可能會發現,最好定義一些“在調用以前的persistAsync/persist處理程序之後發生”的操作。PersistentActor提供了名爲deferdeferAsync的實用方法,它們分別與persistpersistAsync工作類似,但不會持久化傳入事件。建議將它們用於讀取操作,在域模型中沒有相應事件的操作。

使用這些方法與持久化方法非常相似,但它們不會持久化傳入事件。它將保存在內存中,並在調用處理程序時使用。

class MyPersistentActor extends AbstractPersistentActor {

  @Override
  public String persistenceId() {
    return "my-stable-persistence-id";
  }

  private void handleCommand(String c) {
    persistAsync(
        String.format("evt-%s-1", c),
        e -> {
          getSender().tell(e, getSelf());
        });
    persistAsync(
        String.format("evt-%s-2", c),
        e -> {
          getSender().tell(e, getSelf());
        });

    deferAsync(
        String.format("evt-%s-3", c),
        e -> {
          getSender().tell(e, getSelf());
        });
  }

  @Override
  public Receive createReceiveRecover() {
    return receiveBuilder().match(String.class, this::handleCommand).build();
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder().match(String.class, this::handleCommand).build();
  }
}

請注意,sender()在處理程序回調中是安全的,它將指向調用此deferdeferAsync處理程序的命令的原始發送者。

調用方將按此(保證)順序得到響應:

final ActorRef persistentActor = system.actorOf(Props.create(MyPersistentActor.class));
persistentActor.tell("a", sender);
persistentActor.tell("b", sender);

// order of received messages:
// a
// b
// evt-a-1
// evt-a-2
// evt-a-3
// evt-b-1
// evt-b-2
// evt-b-3

你也可以在調用persist時,調用defer或者deferAsync

class MyPersistentActor extends AbstractPersistentActor {

  @Override
  public String persistenceId() {
    return "my-stable-persistence-id";
  }

  private void handleCommand(String c) {
    persist(
        String.format("evt-%s-1", c),
        e -> {
          sender().tell(e, self());
        });
    persist(
        String.format("evt-%s-2", c),
        e -> {
          sender().tell(e, self());
        });

    defer(
        String.format("evt-%s-3", c),
        e -> {
          sender().tell(e, self());
        });
  }

  @Override
  public Receive createReceiveRecover() {
    return receiveBuilder().match(String.class, this::handleCommand).build();
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder().match(String.class, this::handleCommand).build();
  }
}
  • 警告:如果在對deferdeferAsync的調用之間重新啓動或停止 Actor,並且日誌已經處理並確認了前面的所有寫入操作,則不會調用回調。

嵌套的持久調用

可以在各自的回調塊中調用persistpersistAsync,它們將正確地保留線程安全性(包括getSender()的正確值)和存儲保證。

一般來說,鼓勵創建不需要使用嵌套事件持久化的命令處理程序,但是在某些情況下,它可能會有用。瞭解這些情況下回調執行的順序以及它們對隱藏行爲(persist()強制執行)的影響是很重要的。在下面的示例中,發出了兩個持久調用,每個持久調用在其回調中發出另一個持久調用:

@Override
public Receive createReceiveRecover() {
  final Procedure<String> replyToSender = event -> getSender().tell(event, getSelf());

  return receiveBuilder()
      .match(
          String.class,
          msg -> {
            persist(
                String.format("%s-outer-1", msg),
                event -> {
                  getSender().tell(event, getSelf());
                  persist(String.format("%s-inner-1", event), replyToSender);
                });

            persist(
                String.format("%s-outer-2", msg),
                event -> {
                  getSender().tell(event, getSelf());
                  persist(String.format("%s-inner-2", event), replyToSender);
                });
          })
      .build();
}

向此PersistentActor發送兩個命令時,將按以下順序執行持久化處理程序:

persistentActor.tell("a", ActorRef.noSender());
persistentActor.tell("b", ActorRef.noSender());

// order of received messages:
// a
// a-outer-1
// a-outer-2
// a-inner-1
// a-inner-2
// and only then process "b"
// b
// b-outer-1
// b-outer-2
// b-inner-1
// b-inner-2

首先,發出持久調用的“外層”,並應用它們的回調。成功完成這些操作後,將調用內部回調(一旦日誌確認了它們所持續的事件是持久的)。只有在成功地調用了所有這些處理程序之後,才能將下一個命令傳遞給持久 Actor。換句話說,通過最初在外層上調用persist()來保證輸入命令的存儲被擴展,直到所有嵌套的persist回調都被處理完畢。

也可以使用相同的模式嵌套persistAsync調用:

@Override
public Receive createReceive() {
  final Procedure<String> replyToSender = event -> getSender().tell(event, getSelf());

  return receiveBuilder()
      .match(
          String.class,
          msg -> {
            persistAsync(
                String.format("%s-outer-1", msg),
                event -> {
                  getSender().tell(event, getSelf());
                  persistAsync(String.format("%s-inner-1", event), replyToSender);
                });

            persistAsync(
                String.format("%s-outer-2", msg),
                event -> {
                  getSender().tell(event, getSelf());
                  persistAsync(String.format("%s-inner-1", event), replyToSender);
                });
          })
      .build();
}

在這種情況下,不會發生存儲,但事件仍將持續,並且按預期順序執行回調:

persistentActor.tell("a", getSelf());
persistentActor.tell("b", getSelf());

// order of received messages:
// a
// b
// a-outer-1
// a-outer-2
// b-outer-1
// b-outer-2
// a-inner-1
// a-inner-2
// b-inner-1
// b-inner-2

// which can be seen as the following causal relationship:
// a -> a-outer-1 -> a-outer-2 -> a-inner-1 -> a-inner-2
// b -> b-outer-1 -> b-outer-2 -> b-inner-1 -> b-inner-2

儘管可以通過保持各自的語義來嵌套混合persistpersistAsync,但這不是推薦的做法,因爲這可能會導致嵌套過於複雜。

  • 警告:雖然可以在彼此內部嵌套persist調用,但從 Actor 消息處理線程以外的任何其他線程調用persist都是非法的。例如,通過Futures調用persist就是非法的!這樣做將打破persist方法旨在提供的保證,應該始終從 Actor 的接收塊(receive block)中調用persistpersistAsync

失敗

如果事件的持久性失敗,將調用onPersistFailure(默認情況下記錄錯誤),並且 Actor 將無條件停止。

persist失敗時,它無法恢復的原因是不知道事件是否實際持續,因此處於不一致狀態。由於日誌可能不可用,在持續失敗時重新啓動很可能會失敗。最好是停止 Actor,然後在退後超時後重新啓動。提供akka.pattern.BackoffSupervisor Actor 以支持此類重新啓動。

@Override
public void preStart() throws Exception {
  final Props childProps = Props.create(MyPersistentActor1.class);
  final Props props =
      BackoffSupervisor.props(
          childProps, "myActor", Duration.ofSeconds(3), Duration.ofSeconds(30), 0.2);
  getContext().actorOf(props, "mySupervisor");
  super.preStart();
}

如果在存儲事件之前拒絕了該事件的持久性,例如,由於序列化錯誤,將調用onPersistRejected(默認情況下記錄警告),並且 Actor 將繼續執行下一條消息。

如果在啓動 Actor 時無法從日誌中恢復 Actor 的狀態,將調用onRecoveryFailure(默認情況下記錄錯誤),並且 Actor 將被停止。請注意,加載快照失敗也會像這樣處理,但如果你知道序列化格式已以不兼容的方式更改,則可以禁用快照加載,請參閱「恢復自定義」。

原子寫入

每個事件都是原子存儲的(stored atomically),但也可以使用persistAllpersistAllAsync方法原子存儲多個事件。這意味着傳遞給該方法的所有事件都將被存儲,或者在出現錯誤時不存儲任何事件。

因此,持久性 Actor 的恢復永遠不會只在persistAll持久化事件的一個子集的情況下部分完成。

有些日誌可能不支持幾個事件的原子寫入(atomic writes),它們將拒絕persistAll命令,例如調用OnPersistRejected時出現異常(通常是UnsupportedOperationException)。

批量寫入

爲了在使用persistAsync時優化吞吐量,持久性 Actor 在將事件寫入日誌(作爲單個批處理)之前在內部批處理要在高負載下存儲的事件。批(batch)的大小由日誌往返期間發出的事件數動態確定:向日志發送批之後,在收到上一批已寫入的確認信息之前,不能再發送其他批。批寫入從不基於計時器,它將延遲保持在最小值。

消息刪除

可以在指定的序列號之前刪除所有消息(由單個持久 Actor 記錄);持久 Actor 可以爲此端調用deleteMessages方法。

在基於事件源的應用程序中刪除消息通常要麼根本不使用,要麼與快照一起使用,即在成功存儲快照之後,可以發出一條deleteMessages(toSequenceNr)消息。

  • 警告:如果你使用「持久性查詢」,查詢結果可能會丟失日誌中已刪除的消息,這取決於日誌插件中如何實現刪除。除非你使用的插件在持久性查詢結果中仍然顯示已刪除的消息,否則你必須設計應用程序,使其不受丟失消息的影響。

在持久 Actor 發出deleteMessages消息之後,如果刪除成功,則向持久 Actor 發送DeleteMessagesSuccess消息,如果刪除失敗,則向持久 Actor 發送DeleteMessagesFailure消息。

消息刪除不會影響日誌的最高序列號,即使在調用deleteMessages之後從日誌中刪除了所有消息。

持久化狀態處理

持久化、刪除和重放消息可以成功,也可以失敗。

Method Success
persist / persistAsync 調用持久化處理器
onPersistRejected 無自動行爲
recovery RecoveryCompleted
deleteMessages DeleteMessagesSuccess

最重要的操作(persistrecovery)將故障處理程序建模爲顯式回調,用戶可以在PersistentActor中重寫該回調。這些處理程序的默認實現會發出一條日誌消息(persistrecovery失敗的error),記錄失敗原因和有關導致失敗的消息的信息。

對於嚴重的故障(如恢復或持久化事件失敗),在調用故障處理程序後將停止持久性 Actor。這是因爲,如果底層日誌實現發出持久性失敗的信號,那麼它很可能要麼完全失敗,要麼過載並立即重新啓動,然後再次嘗試持久性事件,這很可能不會幫助日誌恢復,因爲它可能會導致一個「Thundering herd」問題,因爲許多持久性 Actor 會重新啓動並嘗試繼續他們的活動。相反,使用BackoffSupervisor,它實現了一個指數級的退避(exponential-backoff)策略,允許持久性 Actor 在重新啓動之間有更多的喘息空間。

  • 註釋:日誌實現可以選擇實現重試機制,例如,只有在寫入失敗N次之後,纔會向用戶發出持久化失敗的信號。換句話說,一旦一個日誌返回一個失敗,它就被 Akka 持久化認爲是致命的,導致失敗的持久行 Actor 將被停止。檢查你正在使用的日誌實現文檔,瞭解它是否或如何使用此技術。

安全地關閉持久性 Actor

當從外部關閉持久性 Actor 時,應該特別小心。對於正常的 Actor,通常可以接受使用特殊的PoisonPill消息來向 Actor 發出信號,一旦收到此信息,它就應該停止自己。事實上,此消息是由 Akka 自動處理的。

當與PersistentActor一起使用時,這可能很危險。由於傳入的命令將從 Actor 的郵箱中排出,並在等待確認時放入其內部存儲(在調用持久處理程序之前),因此 Actor 可以在處理已放入其存儲的其他消息之前接收和(自動)處理PoisonPill,從而導致 Actor 的提前(pre-mature)停止。

  • 警告:當與持久性 Actor 一起工作時,考慮使用明確的關閉消息而不是使用PoisonPill

下面的示例強調了消息如何到達 Actor 的郵箱,以及在使用persist()時它們如何與其內部存儲機制交互。注意,使用PoisonPill時可能發生的早期停止行爲:

final class Shutdown {}

class MyPersistentActor extends AbstractPersistentActor {
  @Override
  public String persistenceId() {
    return "some-persistence-id";
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(
            Shutdown.class,
            shutdown -> {
              getContext().stop(getSelf());
            })
        .match(
            String.class,
            msg -> {
              System.out.println(msg);
              persist("handle-" + msg, e -> System.out.println(e));
            })
        .build();
  }

  @Override
  public Receive createReceiveRecover() {
    return receiveBuilder().matchAny(any -> {}).build();
  }
}
// UN-SAFE, due to PersistentActor's command stashing:
persistentActor.tell("a", ActorRef.noSender());
persistentActor.tell("b", ActorRef.noSender());
persistentActor.tell(PoisonPill.getInstance(), ActorRef.noSender());
// order of received messages:
// a
//   # b arrives at mailbox, stashing;        internal-stash = [b]
//   # PoisonPill arrives at mailbox, stashing; internal-stash = [b, Shutdown]
// PoisonPill is an AutoReceivedMessage, is handled automatically
// !! stop !!
// Actor is stopped without handling `b` nor the `a` handler!
// SAFE:
persistentActor.tell("a", ActorRef.noSender());
persistentActor.tell("b", ActorRef.noSender());
persistentActor.tell(new Shutdown(), ActorRef.noSender());
// order of received messages:
// a
//   # b arrives at mailbox, stashing;        internal-stash = [b]
//   # Shutdown arrives at mailbox, stashing; internal-stash = [b, Shutdown]
// handle-a
//   # unstashing;                            internal-stash = [Shutdown]
// b
// handle-b
//   # unstashing;                            internal-stash = []
// Shutdown
// -- stop --

重播濾波器

在某些情況下,事件流可能已損壞,並且多個寫入程序(即多個持久性 Actor 實例)使用相同的序列號記錄不同的消息。在這種情況下,你可以配置如何在恢復時過濾來自多個編寫器(writers)的重播(replayed)消息。

在你的配置中,在akka.persistence.journal.xxx.replay-filter部分(其中xxx是日誌插件id)下,你可以從以下值中選擇重播過濾器(replay filter)的模式:

  • repair-by-discard-old
  • fail
  • warn
  • off

例如,如果爲 LevelDB 插件配置重播過濾器,則如下所示:

# The replay filter can detect a corrupt event stream by inspecting
# sequence numbers and writerUuid when replaying events.
akka.persistence.journal.leveldb.replay-filter {
  # What the filter should do when detecting invalid events.
  # Supported values:
  # `repair-by-discard-old` : discard events from old writers,
  #                           warning is logged
  # `fail` : fail the replay, error is logged
  # `warn` : log warning but emit events untouched
  # `off` : disable this feature completely
  mode = repair-by-discard-old
}

快照

當你使用 Actor 建模你的域時,你可能會注意到一些 Actor 可能會積累非常長的事件日誌並經歷很長的恢復時間。有時,正確的方法可能是分成一組生命週期較短的 Actor。但是,如果這不是一個選項,你可以使用快照(snapshots)來大幅縮短恢復時間。

持久性 Actor 可以通過調用saveSnapshot方法來保存內部狀態的快照。如果快照保存成功,持久性 Actor 將收到SaveSnapshotSuccess消息,否則將收到SaveSnapshotFailure消息。

private Object state;
private int snapShotInterval = 1000;

@Override
public Receive createReceive() {
  return receiveBuilder()
      .match(
          SaveSnapshotSuccess.class,
          ss -> {
            SnapshotMetadata metadata = ss.metadata();
            // ...
          })
      .match(
          SaveSnapshotFailure.class,
          sf -> {
            SnapshotMetadata metadata = sf.metadata();
            // ...
          })
      .match(
          String.class,
          cmd -> {
            persist(
                "evt-" + cmd,
                e -> {
                  updateState(e);
                  if (lastSequenceNr() % snapShotInterval == 0 && lastSequenceNr() != 0)
                    saveSnapshot(state);
                });
          })
      .build();
}

其中,metadata的類型爲SnapshotMetadata

final case class SnapshotMetadata(persistenceId: String, sequenceNr: Long, timestamp: Long = 0L)

在恢復過程中,通過SnapshotOffer消息向持久性 Actor 提供以前保存的快照,從中可以初始化內部狀態。

private Object state;

@Override
public Receive createReceiveRecover() {
  return receiveBuilder()
      .match(
          SnapshotOffer.class,
          s -> {
            state = s.snapshot();
            // ...
          })
      .match(
          String.class,
          s -> {
            /* ...*/
          })
      .build();
}

SnapshotOffer消息之後重播的消息(如果有)比提供的快照狀態年輕(younger)。他們最終將持久性 Actor 恢復到當前(即最新)狀態。

通常,只有在持久 Actor 以前保存過一個或多個快照,並且其中至少一個快照與可以指定用於恢復的SnapshotSelectionCriteria匹配時,纔會提供持久 Actor 快照。

@Override
public Recovery recovery() {
  return Recovery.create(
      SnapshotSelectionCriteria.create(457L, System.currentTimeMillis()));
}

如果未指定,則默認爲SnapshotSelectionCriteria.latest(),後者選擇最新的(最年輕的)快照。要禁用基於快照的恢復,應用程序應使用SnapshotSelectionCriteria.none()。如果沒有保存的快照與指定的SnapshotSelectionCriteria匹配,則恢復將重播所有日誌消息。

  • 註釋:爲了使用快照,必須配置默認的快照存儲(akka.persistence.snapshot-store.plugin),或者持久 Actor 可以通過重寫String snapshotPluginId()顯式地選擇快照存儲。由於某些應用程序可以不使用任何快照,因此不配置快照存儲是合法的。但是,當檢測到這種情況時,Akka 會記錄一條警告消息,然後繼續操作,直到 Actor 嘗試存儲快照,此時操作將失敗(例如,通過使用SaveSnapshotFailure進行響應)。注意集羣分片(Cluster Sharding)的“持久性模式”使用快照。如果使用該模式,則需要定義快照存儲插件。

快照刪除

持久性 Actor 可以通過使用快照拍攝時間的序列號調用deleteSnapshot方法來刪除單個快照。

要批量刪除與SnapshotSelectionCriteria匹配的一系列快照,持久 Actor 應使用deleteSnapshots方法。根據所用的日誌,這可能是低效的。最佳做法是使用deleteSnapshot執行特定的刪除,或者爲SnapshotSelectionCriteria包含minSequenceNrmaxSequenceNr

快照狀態處理

保存或刪除快照既可以成功,也可以失敗,此信息通過狀態消息報告給持久 Actor,如下表所示:

Method Success Failure message
saveSnapshot(Any) SaveSnapshotSuccess SaveSnapshotFailure
deleteSnapshot(Long) DeleteSnapshotSuccess DeleteSnapshotFailure
deleteSnapshots(SnapshotSelectionCriteria) DeleteSnapshotsSuccess DeleteSnapshotsFailure

如果 Actor 未處理故障消息,則將爲每個傳入的故障消息記錄默認的警告日誌消息。不會對成功消息執行默認操作,但是你可以自由地處理它們,例如,爲了刪除快照的內存中表示形式,或者在嘗試再次保存快照失敗的情況下。

擴容

在一個用例中,如果需要的持久性 Actor 的數量高於一個節點的內存中所能容納的數量,或者彈性很重要,因此如果一個節點崩潰,那麼持久性 Actor 很快就會在一個新節點上啓動,並且可以恢復操作,那麼「集羣分片」非常適合將持久性 Actor 通過他們的id分散到集羣和地址上。

Akka 持久化(persistence)是基於單寫入(single-writer)原則的。對於特定的persistenceId,一次只能激活一個PersistentActor實例。如果多個實例同時持久化事件,那麼這些事件將被交錯,並且在重播時可能無法正確解釋。集羣分片確保數據中心內每個id只有一個活動實體(PersistentActor)。LightBend 的「Multi-DC Persistence」支持跨數據中心的雙活(active-active)持久性實體。

在 Akka 之上構建的「Lagom」框架編碼了許多與此相關的最佳實踐。有關更多詳細信息,請參閱 Lagom 文檔中的「Managing Data Persistence」和「Persistent Entity」。

至少一次傳遞

要將具有至少一次傳遞(at-least-once delivery)語義的消息發送到目標,可以使用AbstractPersistentActorWithAtLeastOnceDelivery,而不是在發送端擴展AbstractPersistentActor。當消息在可配置的超時時間內未被確認時,它負責重新發送消息。

發送 Actor 的狀態,包括那些已發送但未被接收者確認的消息,必須是持久的,這樣它才能在發送 Actor 或 JVM 崩潰後存活下來。AbstractPersistentActorWithAtLeastOnceDelivery類本身不持久任何內容。

  • 註釋:至少有一次傳遞意味着原始消息發送順序並不總是保持不變,並且目標可能接收到重複的消息。該語義與普通ActorRef發送操作的語義不匹配:
    • 至少一次傳遞
    • 同一“發送方和接收者”對的消息順序由於可能的重發而不被保留
    • 在崩潰和目標 Actor 的重新啓動之後,消息仍然被傳遞給新的 Actor 化身。

這些語義類似於ActorPath所表示的含義,因此在傳遞消息時需要提供路徑而不是引用。消息將與 Actor 選擇(selection)一起發送到路徑。

使用deliver方法將消息發送到目標。當目標已用確認消息答覆時,調用confirmDelivery方法。

deliver 與 confirmDelivery 的關係

若要將消息發送到目標路徑,請在持久化發送消息的意圖之後使用deliver方法。

目標 Actor 必須返回確認消息。當發送 Actor 收到此確認消息時,你應該持久化消息已成功傳遞的事實,然後調用confirmDelivery方法。

如果持久性 Actor 當前未恢復,則deliver方法將消息發送到目標 Actor。恢復時,將緩衝消息,直到使用confirmDelivery確認消息。一旦恢復完成,如果有未確認的未完成消息(在消息重播期間),持久性 Actor 將在發送任何其他消息之前重新發送這些消息。

傳遞需要deliveryIdToMessage函數將提供的deliveryId傳遞到消息中,以便deliverconfirmDelivery之間的關聯成爲可能。deliveryId必須在傳遞之間往返。在收到消息後,目標 Actor 會將包裝在確認消息中的相同deliveryId發送回發送者。然後,發送方將使用它調用confirmDelivery方法來完成傳遞過程。

class Msg implements Serializable {
  private static final long serialVersionUID = 1L;
  public final long deliveryId;
  public final String s;

  public Msg(long deliveryId, String s) {
    this.deliveryId = deliveryId;
    this.s = s;
  }
}

class Confirm implements Serializable {
  private static final long serialVersionUID = 1L;
  public final long deliveryId;

  public Confirm(long deliveryId) {
    this.deliveryId = deliveryId;
  }
}

class MsgSent implements Serializable {
  private static final long serialVersionUID = 1L;
  public final String s;

  public MsgSent(String s) {
    this.s = s;
  }
}

class MsgConfirmed implements Serializable {
  private static final long serialVersionUID = 1L;
  public final long deliveryId;

  public MsgConfirmed(long deliveryId) {
    this.deliveryId = deliveryId;
  }
}

class MyPersistentActor extends AbstractPersistentActorWithAtLeastOnceDelivery {
  private final ActorSelection destination;

  public MyPersistentActor(ActorSelection destination) {
    this.destination = destination;
  }

  @Override
  public String persistenceId() {
    return "persistence-id";
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(
            String.class,
            s -> {
              persist(new MsgSent(s), evt -> updateState(evt));
            })
        .match(
            Confirm.class,
            confirm -> {
              persist(new MsgConfirmed(confirm.deliveryId), evt -> updateState(evt));
            })
        .build();
  }

  @Override
  public Receive createReceiveRecover() {
    return receiveBuilder().match(Object.class, evt -> updateState(evt)).build();
  }

  void updateState(Object event) {
    if (event instanceof MsgSent) {
      final MsgSent evt = (MsgSent) event;
      deliver(destination, deliveryId -> new Msg(deliveryId, evt.s));
    } else if (event instanceof MsgConfirmed) {
      final MsgConfirmed evt = (MsgConfirmed) event;
      confirmDelivery(evt.deliveryId);
    }
  }
}

class MyDestination extends AbstractActor {
  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(
            Msg.class,
            msg -> {
              // ...
              getSender().tell(new Confirm(msg.deliveryId), getSelf());
            })
        .build();
  }
}

持久化模塊生成的deliveryId是嚴格單調遞增的序列號,沒有間隙。相同的序列用於 Actor 的所有目的地,即當發送到多個目的地時,目的地將看到序列中的間隙。無法使用自定義deliveryId。但是,你可以將消息中的自定義關聯標識符發送到目標。然後必須在內部deliveryId(傳遞到deliveryIdToMessage函數)和自定義關聯id(傳遞到消息)之間保留映射。你可以通過將此類映射存儲在一個Map(correlationId -> deliveryId)中來實現這一點,從該映射中,你可以在消息的接收者用你的自定義關聯id答覆之後,檢索要傳遞到confirmDelivery方法的deliveryId

AbstractPersistentActorWithAtLeastOnceDelivery類的狀態由未確認的消息和序列號組成。它不存儲此狀態本身。你必須持久化與PersistentActordeliverconfirmDelivery調用相對應的事件,以便在PersistentActor的恢復階段通過調用相同的方法恢復狀態。有時,這些事件可以從其他業務級事件派生,有時必須創建單獨的事件。在恢復過程中,deliver調用不會發送消息,如果未執行匹配的confirmDelivery,則稍後將發送這些消息。

對快照的支持由getDeliverySnapshotsetDeliverySnapshot提供。AtLeastOnceDeliverySnapshot包含完整的傳遞狀態,也包括未確認的消息。如你需要 Actor 狀態的其他部分的自定義快照,則還必須包括AtLeastOnceDeliverySnapshot。它使用protobuf和普通的 Akka 序列化機制進行序列化。最簡單的方法是將AtLeastOnceDeliverySnapshot的字節作爲blob包含在自定義快照中。

重新傳遞嘗試之間的間隔由redeliverInterval方法定義。可以使用akka.persistence.at-least-once-delivery.redeliver-interval配置鍵配置默認值。方法可以被實現類重寫以返回非默認值。

在每次重新傳遞突發時將發送的最大消息數由redeliveryBurstLimit方法定義(突發頻率是重新傳遞間隔的一半)。如果有很多未確認的消息(例如,如果目標 Actor 長時間不可用),這有助於防止同時發送大量的消息。默認值可以使用akka.persistence.at-least-once-delivery.redelivery-burst-limit配置鍵進行配置。方法可以被實現類重寫以返回非默認值。

在多次嘗試傳遞之後,至少會向self發送一條AtLeastOnceDelivery.UnconfirmedWarning消息。重新發送仍將繼續,但你可以選擇調用confirmDelivery以取消重新發送。發出警告前的傳送嘗試次數由warnAfterNumberOfUnconfirmedAttempts方法定義。可以使用akka.persistence.at-least-once-delivery.warn-after-number-of-unconfirmed-attempts配置鍵配置默認值。方法可以被實現類重寫以返回非默認值。

AbstractPersistentActorWithAtLeastOnceDelivery類將消息保存在內存中,直到確認它們的成功傳遞爲止。允許 Actor 在內存中保留的未確認消息的最大數目由maxUnconfirmedMessages方法定義。如果超過此限制,則傳遞方法將不接受更多的消息,並將引發AtLeastOnceDelivery.MaxUnconfirmedMessagesExceededException。可以使用akka.persistence.at-least-once-delivery.max-unconfirmed-messages配置鍵配置默認值。方法可以被實現類重寫以返回非默認值。

事件適配器

在使用事件源(event sourcing)的長時間運行的項目中,有時需要將數據模型與域模型完全分離。

事件適配器(Event Adapters)在以下情況中提供幫助:

  • 版本遷移Version Migrations),存儲在版本 1 中的現有事件應“向上轉換”爲新的版本 2 表示,這樣做的過程涉及實際代碼,而不僅僅是序列化層的更改。對於這些場景,toJournal函數通常是一個標識函數,但是fromJournal實現爲v1.Event=>v2.Event,在fromJournal方法中執行必要的映射。這種技術有時在其他 CQRS 庫中被稱爲upcasting
  • 分離域和數據模型Separating Domain and Data models),由於EventAdapters,可以完全分離域模型和用於在日誌中持久化數據的模型。例如,你可能希望在域模型中使用case類,但是將它們的協議緩衝區(或任何其他二進制序列化格式)計數器部分保留到日誌中。可以使用簡單的toJournal:MyModel=>MyDataModelfromJournal:MyDataModel=>MyModel適配器來實現此功能。
  • 日誌專用數據類型Journal Specialized Data Types),暴露基礎日誌所理解的數據類型,例如,對於理解 JSON 的數據存儲,可以寫一個EventAdaptertoJournal:Any=>JSON,這樣日誌就可以直接存儲 JSON,而不是將對象序列化爲其二進制表示。

實現一個EventAdapter非常重要:

class MyEventAdapter implements EventAdapter {
  @Override
  public String manifest(Object event) {
    return ""; // if no manifest needed, return ""
  }

  @Override
  public Object toJournal(Object event) {
    return event; // identity
  }

  @Override
  public EventSeq fromJournal(Object event, String manifest) {
    return EventSeq.single(event); // identity
  }
}

然後,爲了在日誌中的事件上使用它,必須使用以下配置語法綁定它:

akka.persistence.journal {
  inmem {
    event-adapters {
      tagging        = "docs.persistence.MyTaggingEventAdapter"
      user-upcasting = "docs.persistence.UserUpcastingEventAdapter"
      item-upcasting = "docs.persistence.ItemUpcastingEventAdapter"
    }

    event-adapter-bindings {
      "docs.persistence.Item"        = tagging
      "docs.persistence.TaggedEvent" = tagging
      "docs.persistence.v1.Event"    = [user-upcasting, item-upcasting]
    }
  }
}

可以將多個適配器(adapter)綁定到一個類以進行恢復,在這種情況下,所有綁定適配器的fromJournal方法將應用於給定的匹配事件(按照配置中的定義順序)。由於每個適配器可以返回從0n個適配事件(稱爲EventSeq),因此每個適配器都可以調查事件,如果確實需要對其進行適配,則返回相應的事件。在這個過程中沒有任何貢獻的其他適配器只返回EventSeq.empty。然後,在重放過程中,將調整後的事件傳遞給PersistentActor

存儲插件

日誌和快照存儲的存儲後端可以插入到 Akka 持久性擴展中。

Akka 社區項目頁面提供了持久性日誌和快照存儲插件的目錄,請參閱「社區插件」。

插件可以通過“默認”爲所有持久 Actor 的選擇,也可以在持久 Actor 定義自己的插件集時“單獨”選擇。

當持久性 Actor 不重寫journalPluginIdsnapshotPluginId方法時,持久性擴展將使用reference.conf中配置的“默認”日誌和快照存儲插件:

akka.persistence.journal.plugin = ""
akka.persistence.snapshot-store.plugin = ""

但是,這些條目作爲空的""提供,需要通過在用戶的application.conf中的覆蓋進行顯式的用戶配置。有關將消息寫入 LevelDB 的日誌插件的示例,請參閱「Local LevelDB」。有關將快照作爲單個文件寫入本地文件系統的快照存儲插件的示例,請參閱「Local snapshot」。

應用程序可以通過實現插件 API 並通過配置激活插件來提供自己的插件。插件開發需要以下導入:

import akka.dispatch.Futures;
import akka.persistence.*;
import akka.persistence.journal.japi.*;
import akka.persistence.snapshot.japi.*;

持久性插件的預先初始化

默認情況下,持久性插件在使用時按需啓動。然而,在某些情況下,預先啓動某個插件可能會很有好處。爲了做到這一點,你應該首先在akka.extensions鍵下添加akka.persistence.Persistence。然後,在akka.persistence.journal.auto-start-journalsakka.persistence.snapshot-store.auto-start-snapshot-stores下指定希望自動啓動的插件的ID

例如,如果你希望對 LevelDB 日誌插件和本地快照存儲插件進行預先初始化,那麼你的配置應該如下所示:

akka {

  extensions = [akka.persistence.Persistence]

  persistence {

    journal {
      plugin = "akka.persistence.journal.leveldb"
      auto-start-journals = ["akka.persistence.journal.leveldb"]
    }

    snapshot-store {
      plugin = "akka.persistence.snapshot-store.local"
      auto-start-snapshot-stores = ["akka.persistence.snapshot-store.local"]
    }
  }
}

預打包插件

本地 LevelDB 日誌

LevelDB 日誌插件配置條目是akka.persistence.journal.leveldb。它將消息寫入本地 LevelDB 實例。通過定義配置屬性啓用此插件:

# Path to the journal plugin to be used
akka.persistence.journal.plugin = "akka.persistence.journal.leveldb"

基於 LevelDB 的插件還需要以下附加依賴聲明:

<!-- Maven -->
<dependency>
  <groupId>org.fusesource.leveldbjni</groupId>
  <artifactId>leveldbjni-all</artifactId>
  <version>1.8</version>
</dependency>

<!-- Gradle -->
dependencies {
  compile group: 'org.fusesource.leveldbjni', name: 'leveldbjni-all', version: '1.8'
}

<!-- sbt -->
libraryDependencies += "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"

LevelDB 文件的默認位置是當前工作目錄中名爲journal的目錄。可以通過配置更改此位置,其中指定的路徑可以是相對路徑或絕對路徑:

akka.persistence.journal.leveldb.dir = "target/journal"

使用這個插件,每個 Actor 系統運行自己的私有 LevelDB 實例。

LevelDB 的一個特點是,刪除操作不會從日誌中刪除消息,而是爲每個已刪除的消息添加一個“邏輯刪除”。在大量使用日誌的情況下,尤其是包括頻繁刪除的情況下,這可能是一個問題,因爲用戶可能會發現自己正在處理不斷增加的日誌大小。爲此,LevelDB 提供了一個特殊的功能,通過以下配置開啓:

# Number of deleted messages per persistence id that will trigger journal compaction
akka.persistence.journal.leveldb.compaction-intervals {
  persistence-id-1 = 100
  persistence-id-2 = 200
  # ...
  persistence-id-N = 1000
  # use wildcards to match unspecified persistence ids, if any
  "*" = 250
}

共享 LevelDB 日記

一個 LevelDB 實例也可以由多個 Actor 系統(在同一個或不同的節點上)共享。例如,這允許持久 Actor 故障轉移到備份節點,並繼續從備份節點使用共享日誌實例。

  • 警告:共享的 LevelDB 實例是一個單一的故障點,因此只能用於測試目的。
  • 註釋:此插件已被「Persistence Plugin Proxy」取代。

通過實例化SharedLeveldbStore Actor 可以啓動共享 LevelDB 實例。

final ActorRef store = system.actorOf(Props.create(SharedLeveldbStore.class), "store");

默認情況下,共享實例將日誌消息寫入當前工作目錄中名爲journal的本地目錄。存儲位置可以通過配置進行更改:

akka.persistence.journal.leveldb-shared.store.dir = "target/shared"

使用共享 LevelDB 存儲的 Actor 系統必須激活akka.persistence.journal.leveldb-shared插件。

akka.persistence.journal.plugin = "akka.persistence.journal.leveldb-shared"

必須通過插入(遠程)SharedLeveldbStore Actor 引用來初始化此插件。注入是通過使用 Actor 引用作爲參數調用SharedLeveldbJournal.setStore方法完成的。

class SharedStorageUsage extends AbstractActor {
  @Override
  public void preStart() throws Exception {
    String path = "akka.tcp://[email protected]:2552/user/store";
    ActorSelection selection = getContext().actorSelection(path);
    selection.tell(new Identify(1), getSelf());
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(
            ActorIdentity.class,
            ai -> {
              if (ai.correlationId().equals(1)) {
                Optional<ActorRef> store = ai.getActorRef();
                if (store.isPresent()) {
                  SharedLeveldbJournal.setStore(store.get(), getContext().getSystem());
                } else {
                  throw new RuntimeException("Couldn't identify store");
                }
              }
            })
        .build();
  }
}

內部日誌命令(由持久 Actor 發送)被緩衝,直到注入完成。注入是冪等的,即只使用第一次注入。

本地快照存儲

本地快照存儲(local snapshot store)插件配置條目爲akka.persistence.snapshot-store.local。它將快照文件寫入本地文件系統。通過定義配置屬性啓用此插件:

# Path to the snapshot store plugin to be used
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"

默認存儲位置是當前工作目錄中名爲snapshots的目錄。這可以通過配置進行更改,其中指定的路徑可以是相對路徑或絕對路徑:

akka.persistence.snapshot-store.local.dir = "target/snapshots"

請注意,不必指定快照存儲插件。如果不使用快照,則無需對其進行配置。

持久化插件代理

持久化插件代理(persistence plugin proxy)允許跨多個 Actor 系統(在相同或不同節點上)共享日誌和快照存儲。例如,這允許持久 Actor 故障轉移到備份節點,並繼續從備份節點使用共享日誌實例。代理的工作方式是將所有日誌/快照存儲消息轉發到一個共享的持久性插件實例,因此支持代理插件支持的任何用例。

  • 警告:共享日誌/快照存儲是單一故障點,因此應僅用於測試目的。

日誌和快照存儲代理分別通過akka.persistence.journal.proxyakka.persistence.snapshot-store.proxy配置條目進行控制。將target-journal-plugintarget-snapshot-store-plugin鍵設置爲要使用的基礎插件(例如:akka.persistence.journal.leveldb)。在一個 Actor 系統中,start-target-journalstart-target-snapshot-store鍵應設置爲on,這是將實例化共享持久性插件的系統。接下來,需要告訴代理如何找到共享插件。這可以通過設置target-journal-addresstarget-snapshot-store-address配置鍵來實現,也可以通過編程方式調用PersistencePluginProxy.setTargetLocation方法來實現。

  • 註釋:當需要擴展時,Akka 會延遲地啓動擴展,這包括代理。這意味着爲了讓代理正常工作,必須實例化目標節點上的持久性插件。這可以通過實例化PersistencePluginProxyExtension擴展或調用PersistencePluginProxy.start方法來完成。此外,代理持久性插件可以(也應該)使用其原始配置鍵進行配置。

自定義序列化

快照的序列化和Persistent消息的有效負載可以通過 Akka 的序列化基礎設施進行配置。例如,如果應用程序想要序列化

  • payloads of type MyPayload with a custom MyPayloadSerializer and
  • snapshots of type MySnapshot with a custom MySnapshotSerializer

它必須增加:

akka.actor {
  serializers {
    my-payload = "docs.persistence.MyPayloadSerializer"
    my-snapshot = "docs.persistence.MySnapshotSerializer"
  }
  serialization-bindings {
    "docs.persistence.MyPayload" = my-payload
    "docs.persistence.MySnapshot" = my-snapshot
  }
}

到應用程序配置。如果未指定,則使用默認序列化程序。

有關更高級的模式演化技術,請參閱「Persistence - Schema Evolution」文檔。

測試

在 sbt 中使用 LevelDB 默認設置運行測試時,請確保在 sbt 項目中設置fork := true。否則,你將看到一個UnsatisfiedLinkError。或者,你可以通過設置切換到 LevelDB Java 端口。

akka.persistence.journal.leveldb.native = off

akka.persistence.journal.leveldb-shared.store.native = off

在你的 Akka 配置中,LevelDB Java 端口僅用於測試目的。

還要注意的是,對於 LevelDB Java 端口,你將需要以下依賴項:

<!-- Maven -->
<dependency>
  <groupId>org.iq80.leveldb</groupId>
  <artifactId>leveldb</artifactId>
  <version>0.9</version>
</dependency>

<!-- Gradle -->
dependencies {
  compile group: 'org.iq80.leveldb', name: 'leveldb', version: '0.9'
}

<!-- sbt -->
libraryDependencies += "org.iq80.leveldb" % "leveldb" % "0.9"
  • 警告:由於TestActorRef具有同步性,因此無法使用它來測試持久性提供的類(即PersistentActorAtLeastOnceDelivery)。這些特性需要能夠在後臺執行異步任務,以便處理與持久性相關的內部事件。當「測試基於持久性的項目」時,總是依賴於使用TestKit的異步消息傳遞。

配置

持久性模塊有幾個配置屬性,請參閱參考「配置」。

多持久性插件配置

默認情況下,持久性 Actor 將使用在reference.conf配置資源的以下部分中配置的“默認”日誌和快照存儲插件:

# Absolute path to the default journal plugin configuration entry.
akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
# Absolute path to the default snapshot store plugin configuration entry.
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"

注意,在這種情況下,Actor 只重寫persistenceId方法:

abstract class AbstractPersistentActorWithDefaultPlugins extends AbstractPersistentActor {
  @Override
  public String persistenceId() {
    return "123";
  }
}

當持久性 Actor 重寫journalPluginIdsnapshotPluginId方法時,Actor 將由這些特定的持久性插件而不是默認值提供服務:

abstract class AbstractPersistentActorWithOverridePlugins extends AbstractPersistentActor {
  @Override
  public String persistenceId() {
    return "123";
  }

  // Absolute path to the journal plugin configuration entry in the `reference.conf`
  @Override
  public String journalPluginId() {
    return "akka.persistence.chronicle.journal";
  }

  // Absolute path to the snapshot store plugin configuration entry in the `reference.conf`
  @Override
  public String snapshotPluginId() {
    return "akka.persistence.chronicle.snapshot-store";
  }
}

請注意,journalPluginIdsnapshotPluginId必須引用正確配置的reference.conf插件條目,這些插件具有標準類屬性以及特定於這些插件的設置,即:

# Configuration entry for the custom journal plugin, see `journalPluginId`.
akka.persistence.chronicle.journal {
  # Standard persistence extension property: provider FQCN.
  class = "akka.persistence.chronicle.ChronicleSyncJournal"
  # Custom setting specific for the journal `ChronicleSyncJournal`.
  folder = $${user.dir}/store/journal
}
# Configuration entry for the custom snapshot store plugin, see `snapshotPluginId`.
akka.persistence.chronicle.snapshot-store {
  # Standard persistence extension property: provider FQCN.
  class = "akka.persistence.chronicle.ChronicleSnapshotStore"
  # Custom setting specific for the snapshot store `ChronicleSnapshotStore`.
  folder = $${user.dir}/store/snapshot
}

在運行時提供持久性插件配置

默認情況下,持久性 Actor 將使用在ActorSystem創建時加載的配置來創建日誌和快照存儲插件。

當持久性 Actor 重寫journalPluginConfigsnapshotPluginConfig方法時,Actor 將使用聲明的Config對象,並對默認配置進行回退(fallback)。它允許在運行時動態配置日誌和快照存儲:

abstract class AbstractPersistentActorWithRuntimePluginConfig extends AbstractPersistentActor
    implements RuntimePluginConfig {
  // Variable that is retrieved at runtime, from an external service for instance.
  String runtimeDistinction = "foo";

  @Override
  public String persistenceId() {
    return "123";
  }

  // Absolute path to the journal plugin configuration entry in the `reference.conf`
  @Override
  public String journalPluginId() {
    return "journal-plugin-" + runtimeDistinction;
  }

  // Absolute path to the snapshot store plugin configuration entry in the `reference.conf`
  @Override
  public String snapshotPluginId() {
    return "snapshot-store-plugin-" + runtimeDistinction;
  }

  // Configuration which contains the journal plugin id defined above
  @Override
  public Config journalPluginConfig() {
    return ConfigFactory.empty()
        .withValue(
            "journal-plugin-" + runtimeDistinction,
            getContext()
                .getSystem()
                .settings()
                .config()
                .getValue(
                    "journal-plugin") // or a very different configuration coming from an external
            // service.
            );
  }

  // Configuration which contains the snapshot store plugin id defined above
  @Override
  public Config snapshotPluginConfig() {
    return ConfigFactory.empty()
        .withValue(
            "snapshot-plugin-" + runtimeDistinction,
            getContext()
                .getSystem()
                .settings()
                .config()
                .getValue(
                    "snapshot-store-plugin") // or a very different configuration coming from an
            // external service.
            );
  }
}

更多可見


英文原文鏈接Persistence.


———— ☆☆☆ —— 返回 -> Akka 中文指南 <- 目錄 —— ☆☆☆ ————

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章