Comments 35
Я не совсем понял, что вы хотите сделать:
при переигрывание событий, чтоб заполнить StateDB, доставать только определенные события
или
пользовательский запрос направить на базу, где хранятся сами события и от StateDB отказаться
При этой модели, state db в классическом понимании нет, как нету и «единой» модели данных.
Так понятней, или все еще плохо объяснил?
2.Вы пишете его в WriteDb и получаете допустим некий ключ, по которому можно быстро получить это событие
3. Пишите этот ключ в ReadDb
4. Пользователь делает запрос, разбираете, выясняете какие ключи вам нужны, достаете по ним данные из WriteDb
В итоге:
1. пользователь так же будет получать не актуальную информацию(хотя не настолько как в традиционном варианте), потому что пока вы считываете ключи, WriteDb может измениться, а новые ключи еще не успели добавить в ReadDb
2. изменений может быть очень много и вместо одного запроса, который бы вернул статистику изменения курса, вам нужно отправить много запросов(ведь вы предлагаете доставать отдельные события по ключу)
3. если запросов на чтение много, то БД на запись так же будет тормозить
по итогам:
1. более актуальная информация чем в традиционном варианте — уже лучше.
2. как я упоминал в статье, конечно есть случаи когда массовые выборки будут медленными, и так или иначе агрегировать и/или кешировать данные надо. идеального мира нет :)
3. не обязательно, это зависит от того как она устроена. append only дает определенные преимущества.
если разбивать файлы в которых лежат необходимые ключи (например, consistent hashing или на исторические «отрезки») и хранить на независимых устройствах (в терминах дисков ли, или сетевых устройств) то можно обеспечить сценарий в котором чтение из writedb практически независимо от записи. пока что этого ф-ционала нет, но это интересная тема!
1. делаете запрос в ElasticSearch
2. ElasticSearch возвращает ИД из ReadDb
3. Достаете данные из ReadDb
Такой подход легко масштабируется
А почему вы решили, что для хранения состояния нужна обязательно реляционная бд? Фаулер пишет буквально следующее:
Application states can be stored either in memory or on disk. Since an application state is purely derivable from the event log, you can cache it anywhere you like. A system in use during a working day could be started at the beginning of the day from an overnight snapshot and hold the current application state in memory.
То есть можно вообще не создавать read model и кешировать в памяти доменную модель из которой и брать нужные вещи, In-memory cache можно восстанавливать по событиям при старте приложения.
Разделение на Read модель и доменную модель описывается паттерном CQRS, который хоть и хорошо сочетается с EventSourcing но все же отдельный паттерн, не требующий ни EventSourcing ни даже отдельных бд для чтения и записи.
Я не писал, что нужна обязательно реляционная база данных, выше в комментариях я написал «часто это реляционная база данных». Доменную модель и правда можно хранить где угодно. Если доменная модель в памяти, это не значит что read side нету, просто она в памяти.
Можно прекрасно делать то что вы описываете («кешировать в памяти доменную модель из которой и брать нужные вещи, In-memory cache можно восстанавливать по событиям при старте приложения») однако это не решает ту задачу которая была передо мной — избежать планирования доменной модели («предугадывание будущего»), и дорогостоящих «переигрываний» событий.
Прямо скажем, идея "давайте использовать EventStore как read-model" лежит на поверхности, и как раз в варианте "давайте просто читать последнее релевантное событие" (особенно если ES так организован, что позволяет это сделать сравнительно быстро). Проблемы этого подхода начинаются в тот момент, когда вам надо-таки прочитать всю историю агрегата (например, если вам надо для пользователя достать все теги, на которые он подписан, а каждый тег вы храните отдельным событием).
Мои усилия на тему чтения истории агрегата сейчас таковы: 1) оптимизация поиска (индексы используют CQengine, память/диск)
Индексы синхронные?
организация события в таком виде при котором легко запрашивать без over-fetching.
… то есть вы подстроили события (которые write-model) под то, как удобно читать (то есть read-model)?
2. Только в некотором смысле — в том плане что я сохраняю данные в событиях в таком виде в котором использование индексов для того чтобы найти эти события было настолько тривиальным насколько это возможно. Случаи, когда таки приходится делать агрегацию событий для оптимизации конечно происходят, но я стараюсь это свести к минимуму.
Явно выраженных гарантий по поводу индексов еще нет (но скорее всего будут), пока это факт реализации и некоторых тестов — индексы появляются по факту коммита транзакции (scope транзакций — команда), перед тем как контроль «вернется» к отправителю команды, то есть по сути идут вместе с транзакцией.
То есть вы пожертвовали скоростью записи ради индексов?
Вы понимаете, что ваши индексы — это и есть ваша доменная модель?
Индекс — это и есть попытка угадать, какие данные вам понадобятся.
С моей личной практической точки зрения, это тот «низкий» порог «угадывания» который меня устроил. Мне не нужно угадывать какие будут структуры моделей, как нужно со временем менять модели и мигрировать данные, мне нужно только время от времени добавлять индексы к полям, причем совсем необязательно перед тем как события записаны — индексы можно добавить в любой момент.
… и каждый индекс будет просаживать производительность записи EventStore.
… а если вы их сделаете асинхронными, вы получите eventual consistency без возможности управления ей (или вы сразу закладываетесь на то, что данные, которые вы получили, неактуальны?)
Просто если вы согласны на eventually consistent, то его надо закладывать в проект заранее, а не "мы пока не знаем, какие индексы будут".
Лично я вообще воспринимаю работу с eventual consistency с другой стороны — это поведение команды на запись: она должна знать, относительно каких данных она была консистентна (а, значит, либо запрос на чтение должен отдавать идентификатор версии, либо мы тащим с собой все данные для проверки консистентности), и проверять к каким данным она применяется (естественно, уже в рамках транзакции), а дальше уже применять ту или иную стратегию сведения.
Для "честного" ES это в реальности делается очень просто — каждый запрос на чтение возвращает номер события, относительно которого данные прочитаны (и актуальны), каждая команда на запись проверяет, что в агрегате нет новых событий после указанного (а тривиальная стратегия сведения сводится к ошибке "данные изменились, накатите заново").
Ленивый event sourcing или как жить сегодняшним днем