Akka持久性查询事件流和CQRS

时间:2016-07-07 13:28:42

标签: scala akka cqrs event-sourcing akka-persistence

我正在尝试在ES-CQRS架构中实现读取端。假设我有一个像这样的执着演员:

object UserWrite {

  sealed trait UserEvent
  sealed trait State
  case object Uninitialized extends State
  case class User(username: String, password: String) extends State
  case class AddUser(user: User)
  case class UserAdded(user: User) extends UserEvent
  case class UserEvents(userEvents: Source[(Long, UserEvent), NotUsed])
  case class UsersStream(fromSeqNo: Long)
  case object GetCurrentUser

  def props = Props(new UserWrite)
}

class UserWrite extends PersistentActor {

  import UserWrite._

  private var currentUser: State = Uninitialized

  override def persistenceId: String = "user-write"

  override def receiveRecover: Receive = {
    case UserAdded(user) => currentUser = user
  }

  override def receiveCommand: Receive = {
    case AddUser(user: User) => persist(UserAdded(user)) {
      case UserAdded(`user`) => currentUser = user
    }
    case UsersStream(fromSeqNo: Long) => publishUserEvents(fromSeqNo)
    case GetCurrentUser => sender() ! currentUser
  }

  def publishUserEvents(fromSeqNo: Long) = {
    val readJournal = PersistenceQuery(context.system).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier)
    val userEvents = readJournal
      .eventsByPersistenceId("user-write", fromSeqNo, Long.MaxValue)
      .map { case EventEnvelope(_, _, seqNo, event: UserEvent) => seqNo -> event }
    sender() ! UserEvents(userEvents)
  }
}

据我了解,每次事件持续存在时,我们都可以通过Akka Persistence Query发布。现在,我不确定订阅这些事件的正确方法是什么,所以我可以将它保存在我的读取数据库中?其中一个想法是最初从我的阅读方演员发送UsersStream消息到UserWrite演员,并在该演员中发送“汇”事件。

修改

根据@cmbaxter的建议,我以这种方式实现了读取方:

object UserRead {

  case object GetUsers
  case class GetUserByUsername(username: String)
  case class LastProcessedEventOffset(seqNo: Long)
  case object StreamCompleted

  def props = Props(new UserRead)
}

class UserRead extends PersistentActor {
  import UserRead._

  var inMemoryUsers = Set.empty[User]
  var offset        = 0L

  override val persistenceId: String = "user-read"

  override def receiveRecover: Receive = {
    // Recovery from snapshot will always give us last sequence number
    case SnapshotOffer(_, LastProcessedEventOffset(seqNo)) => offset = seqNo
    case RecoveryCompleted                                 => recoveryCompleted()
  }

  // After recovery is being completed, events will be projected to UserRead actor
  def recoveryCompleted(): Unit = {
    implicit val materializer = ActorMaterializer()
    PersistenceQuery(context.system)
      .readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier)
      .eventsByPersistenceId("user-write", offset + 1, Long.MaxValue)
      .map {
        case EventEnvelope(_, _, seqNo, event: UserEvent) => seqNo -> event
      }
      .runWith(Sink.actorRef(self, StreamCompleted))
  }

  override def receiveCommand: Receive = {
    case GetUsers                    => sender() ! inMemoryUsers
    case GetUserByUsername(username) => sender() ! inMemoryUsers.find(_.username == username)
    // Match projected event and update offset
    case (seqNo: Long, UserAdded(user)) =>
      saveSnapshot(LastProcessedEventOffset(seqNo))
      inMemoryUsers += user
  }
}

有一些问题:事件流似乎很慢。即UserRead actor可以在保存新添加的用户之前使用一组用户进行回答。

编辑2

我增加了cassandra查询日志的刷新间隔,更少解决了慢事件流的问题。看来Cassandra事件日志是默认情况下,每3秒轮询一次。在我application.conf我添加了:

cassandra-query-journal {
  refresh-interval = 20ms
}

编辑3

实际上,不要减少刷新间隔。这将增加内存使用量,但这并不危险,也不是一点。通常,CQRS的概念是写入和读取侧是异步的。因此,在您写入数据后,将永远无法立即进行读取。处理用户界面?我只是打开流并在读取端确认后通过服务器发送的事件推送数据。

2 个答案:

答案 0 :(得分:5)

有一些方法可以做到这一点。例如,在我的应用程序中,我的查询端有一个actor,它有一个持续查找更改的PersistenceQuery,但是你也可以拥有一个具有相同查询的线程。事情是保持流打开,以便能够在发生时立即读取持久事件

val readJournal =
PersistenceQuery(system).readJournalFor[CassandraReadJournal](
  CassandraReadJournal.Identifier)

// issue query to journal
val source: Source[EventEnvelope, NotUsed] =
  readJournal.eventsByPersistenceId(s"MyActorId", 0, Long.MaxValue)

// materialize stream, consuming events
implicit val mat = ActorMaterializer()
source.map(_.event).runForeach{
  case userEvent: UserEvent => {
    doSomething(userEvent)
  }
}

而不是这个,你可以有一个提升PersistenceQuery并存储新事件的计时器,但我认为打开一个流是最好的方式

答案 1 :(得分:3)

虽然仅使用PersistenceQuery的解决方案获得批准,但它包含以下问题:

  1. 这是部分的,只有读取EventEnvelopes的方法。
  2. 它无法与状态快照一起使用,因此,CQRS阅读器部分应该结束 所有坚持不懈的事件一直存在。
  3. 第一种解决方案更好,但存在以下问题:

    1. 太复杂了。这会导致用户不必要地处理序列号。
    2. 代码处理状态(查询/更新)以及Actors实现。
    3. 存在更简单的一个:

      import akka.NotUsed
      import akka.actor.{Actor, ActorLogging}
      import akka.persistence.query.{EventEnvelope, PersistenceQuery}
      import akka.persistence.query.javadsl.{EventsByPersistenceIdQuery, ReadJournal}
      import akka.persistence._
      import akka.stream.ActorMaterializer
      import akka.stream.javadsl.Source
      
      /**
        * Created by alexv on 4/26/2017.
        */
      class CQRSTest {
      
        // User Command, will be transformed to User Event
        sealed trait UserCommand
        // User Event
        // let's assume some conversion from Command to event here
        case class PersistedEvent(command: UserCommand) extends Serializable
        // User State, for simplicity assumed that all State will be snapshotted
        sealed trait State extends Serializable{
          def clear(): Unit
          def updateState(event: PersistedEvent): Unit
          def validateCommand(command:UserCommand): Boolean
          def applyShapshot(newState: State): Unit
          def getShapshot() : State
        }
        case class SaveSnapshot()
      
        /**
          * Common code for Both reader and writer
          * @param state - State
          */
        abstract class CQRSCore(state: State) extends PersistentActor with ActorLogging {
          override def persistenceId: String = "CQRSPersistenceId"
      
          override def preStart(): Unit = {
            // Since the state is external and not depends to Actor's failure or restarts it should be cleared.
            state.clear()
          }
      
          override def receiveRecover: Receive = {
            case event : PersistedEvent => state.updateState(event)
            case SnapshotOffer(_, snapshot: State) => state.applyShapshot(snapshot)
            case RecoveryCompleted  => onRecoveryCompleted(super.lastSequenceNr)
          }
      
          abstract def onRecoveryCompleted(lastSequenceNr:Long)
        }
      
        class CQRSWriter(state: State) extends CQRSCore(state){
          override def preStart(): Unit = {
            super.preStart()
            log.info("CQRSWriter Started")
          }
      
          override  def onRecoveryCompleted(lastSequenceNr: Long): Unit = {
            log.info("Recovery completed")
          }
      
          override def receiveCommand: Receive = {
            case command: UserCommand =>
              if(state.validateCommand(command)) {
                // Persist events and call state.updateState with each persisted event
                persistAll(List(PersistedEvent(command)))(state.updateState)
              }
              else {
                log.error("Validation Failed for Command: {}", command)
              }
            case SaveSnapshot => saveSnapshot(state.getShapshot())
            case SaveSnapshotSuccess(metadata) => log.debug("Saved snapshot successfully: {}", metadata)
            case SaveSnapshotFailure(metadata, reason) => log.error("Failed to Save snapshot: {} . Reason: {}", metadata, reason)
          }
        }
      
        class CQRSReader(state: State) extends CQRSCore(state){
          override def preStart(): Unit = {
            super.preStart()
            log.info("CQRSReader Started")
          }
      
          override  def onRecoveryCompleted(lastSequenceNr: Long): Unit = {
            log.info("Recovery completed, Starting QueryStream")
      
            // ReadJournal type not specified here, so may be used with Cassandra or In-memory Journal (for Tests)
            val readJournal = PersistenceQuery(context.system).readJournalFor(
              context.system.settings.config.getString("akka.persistence.query.my-read-journal"))
              .asInstanceOf[ReadJournal
              with EventsByPersistenceIdQuery]
            val source: Source[EventEnvelope, NotUsed] = readJournal.eventsByPersistenceId(
              OrgPersistentActor.orgPersistenceId, lastSequenceNr + 1, Long.MaxValue)
            source.runForeach({ envelope => state.updateState(envelope.event.asInstanceOf[PersistedEvent]) },ActorMaterializer())
      
          }
      
          // Nothing received since it is Reader only
          override def receiveCommand: Receive = Actor.emptyBehavior
        }
      }