Страничка курса: https://maxcom.github.io/scala-course-2018/
План задания
"Идеальная жизнь" с реляционной СУБД
Можно ли доверять значению в ячейке?
Например количеству денег на счете в банке?
Нет, правда только в журнале операций над счетом.
Значение в БД - просто cache, вычисленный из журнала.
Еще пример: типичный компонент "enterprise" системы
Этапы развития:
История может быть двух видов:
Event Sourcing - храним все события, используем их как основной источник "правды".
Событие - неизменяемый уже произошедший факт.
Что это дает:
Есть две проблемы:
Решения обоих проблем дальше
PersistentActor - актор, который сохраняет события в журнал. При рестарте он восстанавливает состояние из журнала.
build.sbt
// версии всех akka-* должны быть одинаковые
libraryDependencies +=
"com.typesafe.akka" %% "akka-persistence" % "2.5.12"
Еще нужен плагин для storage. Обычно используют Cassandra, для отладки проще LevelDB
// плагин встроен, нужна только сама LevelDB
libraryDependencies +=
"org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"
Команда - запрос актору на выполнение действия.
Команда проходит валидацию, сохраняет событие и выполняет побочный эффект.
class CounterActor extends PersistentActor {
override def persistenceId: String = "my-counter-actor"
private var value: Int = 0
override def receiveCommand: Receive = {
case Increment(n) ⇒ persist(Incremented(n)) { event ⇒
// callback
value += event.n
sender() ! Ack
}
}
override def receiveRecover: Receive = ???
}
persist асинхронный, но для нас он делает:
receiveRecover - восстанавливает актор; не делает побочных эффектов
override def receiveCommand: Receive = {
case Increment(n) ⇒ // команда
persist(Incremented(n)) { event ⇒
value += event.n
sender() ! Ack
}
}
override def receiveRecover: Receive = {
case Incremented(n) ⇒ // событие
value += n // не отправляем Ack
}
Проблема: много событий и восстановление слишком долгое.
Решение - периодически записывать снапшоты.
// case class State(value: Int)
// val SnapshotInterval = 10000
override def receiveCommand: Receive = {
case Increment(n) ⇒ // команда
persist(Incremented(n)) { event ⇒
value += event.n
sender() ! Ack
if (lastSequenceNr % SnapshotInterval == 0
&& lastSequenceNr != 0)
saveSnapshot(State(value))
}
}
Восстановление:
override def receiveRecover: Receive = {
case Incremented(n) ⇒
value += n
case SnapshotOffer(metadata, snapshot: State) ⇒
value = snapshot.value
}
восстановление по-умолчанию стартует с последнего снапшота (можно настроить разные варианты)
Что если история слишком большая и не нужна?
case SaveSnapshotSuccess(metadata) ⇒
deleteMessages(metadata.sequenceNr)
это вариант для "технических" акторов, которым просто нужно восстановление при рестарте
command-query responsibility segregation
Проблема: реалиционные БД универсальны, но они не всегда могут быстро выполнить запрос.
И на практике могут выполнить не любой запрос (например не могут полнотекстовый поиск).
В итоге появляются:
Принцим CQRS - четкое разделение на две части:
Каждая часть
Если Write Side - реляционная СУБД, то нам нужны:
Event sourcing + CQRS:
build.sbt
// версии всех akka-* должны быть одинаковые
libraryDependencies +=
"com.typesafe.akka" %% "akka-persistence-query" % "2.5.12"
Поток событий - реактивный поток
// создаем один раз на приложение
val queries: LeveldbReadJournal =
PersistenceQuery(system).
readJournalFor[LeveldbReadJournal]
(LeveldbReadJournal.Identifier)
// получаем новый поток событий
val events: Source[EventEnvelope, NotUsed] =
queries.currentEventsByPersistenceId("my-counter-actor",
fromSequenceNr = 0)
// нам интересно env.event и env.sequenceNr
Что можно делать с потоком событий (примеры)?
Read side обновляется асинхронно.
Иногда нужно прочитать результат после записи.
Можно при записи отдать sequenceNq пользователю.
После реализовать логику ожидания нужного номера в view.
Напоминаю: