Повторное увлажнение агрегатов из проекции «моментальных снимков», а не из хранилища событий


14

Поэтому я некоторое время заигрываю с Event Sourcing и CQRS, хотя у меня никогда не было возможности применять шаблоны в реальном проекте.

Я понимаю преимущества разделения ваших проблем с чтением и записью, и я ценю то, как Event Sourcing облегчает проецирование изменений состояния в базы данных «Read Model», которые отличаются от вашего Event Store.

То, что я не очень ясно, это то, почему вы когда-либо регидратировать свои агрегаты из самого магазина событий.

Если проецировать изменения в «читаемые» базы данных так просто, почему бы не всегда проектировать изменения в «записывающей» базе данных, схема которой идеально соответствует вашей доменной модели? Это будет эффективно база данных снимков.

Я предполагаю, что это должно быть довольно распространенным явлением в приложениях ES + CQRS.

Если это так, является ли хранилище событий полезным только при перестройке вашей базы данных «записи» в результате изменений схемы? Или я что-то упустил?


Нет ничего плохого в асинхронной записи в хранилище состояний модели записи и использовании его исключительно для загрузки сущностей. Точно такие же проблемы согласованности присутствуют, независимо от того, делаете ли вы это или нет. Ключ к устранению этих проблем согласованности заключается в том, чтобы по-разному моделировать ваши объекты. В Event Sourcing нет ничего волшебного, что решает эти проблемы согласованности. Магия заключается в моделировании, а не в заботе. Существуют конкретные приложения, которые требуют согласованности на этом уровне, которые имеют очень спорные сущности, независимо от того, как вы их моделируете, и требуют особого внимания в любом случае.
— Эндрю Ларссон

Пока вы можете гарантировать доставку событий. Для этого вашему приложению просто необходимо синхронно опубликовать событие в долговременной шине событий. После публикации работа приложения завершена. Затем шина доставит его различным обработчикам событий: один для обновления хранилища событий, один для обновления хранилища состояний и любые другие, необходимые для обновления хранилищ чтения. Причина, по которой вы используете Event Sourcing, заключается в том, что вы больше не заботитесь о немедленной согласованности. Прими это.
— Эндрю Ларссон

Нет причин, по которым вы должны постоянно загружать свои объекты из хранилища событий. Это не его цель. Его цель - предоставить сырой, постоянный регистр всего, что произошло в системе. Хранилища сущностей и денормализованные модели чтения предназначены для загрузки и чтения.
— Эндрю Ларссон

Ответы:


14

То, что я не очень ясно, это то, почему вы когда-либо регидратировать свои агрегаты из самого магазина событий.

Потому что «события» - это книга рекордов.

Если проецировать изменения в «читаемые» базы данных так просто, почему бы не всегда проектировать изменения в «записывающей» базе данных, схема которой идеально соответствует вашей доменной модели? Это будет эффективно база данных снимков.

Да; Иногда полезно оптимизировать производительность, чтобы использовать кэшированную копию агрегатного состояния вместо того, чтобы каждый раз восстанавливать это состояние с нуля. Помните: первое правило оптимизации производительности - «Не надо». Это добавляет дополнительную сложность к решению, и вы бы предпочли избегать этого, пока у вас не будет убедительной мотивации бизнеса.

Если это так, является ли хранилище событий полезным только при перестройке вашей базы данных «записи» в результате изменений схемы? Или я что-то упустил?

Вам не хватает чего-то большего.

Во-первых, если вы рассматриваете решение, основанное на событиях, то это потому, что вы ожидаете, что будет полезно сохранить историю того, что произошло, то есть вы хотите внести неразрушающие изменения .

Вот почему мы вообще пишем в магазин событий.

В частности, это означает, что каждое изменение должно быть записано в хранилище событий.

Конкурирующие авторы могут потенциально либо уничтожать записи друг друга, либо приводить систему в непреднамеренное состояние, если они не знают о правках друг друга. Поэтому обычный подход, когда вам нужна согласованность, состоит в том, чтобы адресовать ваши записи в определенную позицию в журнале (аналог условного PUT в HTTP API). Неудачная запись говорит автору, что его текущее понимание журнала не синхронизировано, и что они должны восстановиться.

Возврат к известной хорошей позиции с последующим воспроизведением любых дополнительных событий, поскольку эта точка является обычной стратегией восстановления. Эта известная хорошая позиция может быть копией того, что находится в локальном кэше, или представлением в вашем хранилище снимков.

На счастливом пути вы можете сохранить снимок агрегата в памяти; вам нужно обратиться к внешнему хранилищу только тогда, когда нет локальной копии.

Кроме того, вам не нужно быть полностью захваченным, если у вас есть доступ к книге рекордов.

Поэтому обычный подход ( при использовании репозитория моментальных снимков) - поддерживать его асинхронно . Таким образом, если вам нужно восстановить, вы можете сделать это без перезагрузки и повторного воспроизведения всей истории агрегата.

Во многих случаях эта сложность не представляет интереса, потому что мелкозернистые агрегаты с ограниченным временем жизни обычно не собирают достаточно событий, чтобы выгоды превышали затраты на поддержание кэша моментальных снимков.

Но когда это подходящий инструмент для решения проблемы, загрузка устаревшего представления агрегата в модель записи с последующим обновлением его дополнительными событиями является вполне разумной вещью.


Все это звучит разумно. Когда я играл с фиктивной реализацией ES некоторое время назад, я использовал EventStore, чтобы гарантировать согласованность, но я также синхронно записывал то, что вы называете «хранилищем снимков». Это означало, что текущее состояние всегда было готово к чтению без повторного воспроизведения событий. Я подозревал, что это не будет хорошо масштабироваться, но так как это было просто упражнение, я не возражал.
— MetaFight

6

Поскольку вы не указываете, для чего будет предназначена база данных «запись», я буду предполагать, что вы имеете в виду следующее: при регистрации нового обновления в агрегате вместо перестройки агрегата из хранилища событий вы вытащите его из базы данных «запись», подтвердите изменение и отправьте событие.

Если это именно то, что вы имеете в виду, то эта стратегия создаст условие для несогласованности: если новое обновление произойдет до того, как у последнего будет шанс внести его в базу данных «запись», новое обновление будет проверено на устаревшие данные, таким образом, потенциально выдает «невозможное» (то есть «запрещенное») событие и портит состояние системы.

Например, рассмотрим постоянный пример бронирования мест в театре. Чтобы избежать двойного бронирования, вам нужно убедиться, что забронированное место еще не занято - это то, что вы называете «проверка». Для этого вы сохраняете список уже забронированных мест в базе данных «запись». Затем, когда приходит запрос на бронирование, вы проверяете, есть ли запрошенное место в списке, и, если нет, запускаете «забронированное» событие, в противном случае отвечаете сообщением об ошибке. Затем вы запускаете процесс проецирования, где вы слушаете «забронированные» события и добавляете забронированные места в список в базе данных «запись».

Обычно система будет функционировать так:

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Projection process catches the event, adds seat #1 to the "already booked" list.
5. Another request to book seat #1.
6. Check in the list: the list contains seat #1
7. Respond with an error message.

Однако что делать, если запросы поступают слишком быстро, а шаг 5 происходит до шага 4?

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Another request to book seat #1.
5. Check in the list: the list is still empty.
6. Issue another "booked seat #1" event.

Теперь у вас есть два события для бронирования одного и того же места. Состояние системы повреждено.

Чтобы этого не происходило, вы никогда не должны проверять обновления по проекции. Чтобы проверить обновление, вы перестраиваете агрегат из хранилища событий, а затем проверяете обновление по нему. После этого вы запускаете событие, но используете временную метку, чтобы убедиться, что с момента последнего чтения из магазина не было создано никаких новых событий. Если это не удается, просто повторите попытку.

Восстановление агрегатов из хранилища событий может привести к снижению производительности. Чтобы смягчить это, вы можете хранить совокупные снимки прямо в потоке событий, помеченные идентификатором события, из которого был создан снимок. Таким образом, вы можете перестроить агрегат, загрузив самый последний моментальный снимок и воспроизведя только те события, которые произошли после него, вместо того, чтобы всегда воспроизводить весь поток событий с начала времени.


Спасибо за ваш ответ (и извините, что так долго отвечал). То, что вы говорите о проверке по базе данных записи, не обязательно верно. Как я упоминал в другом комментарии, в примерной реализации ES, с которой я играл, я удостоверился, что обновляю свою базу данных записи синхронно (и сохраняю concurrencyId / timestamp). Это позволило мне обнаружить нарушения оптимистичного параллелизма без необходимости готовиться из EventStore. Конечно, синхронные записи сами по себе не защищают от повреждения данных, но я также делал однопоточные (однопоточные) записи.
— MetaFight

Итак, у меня были проблемы с согласованностью. Хотя я предположил, что это за счет масштабируемости.
— MetaFight

Синхронная запись в базу данных записи все еще несет в себе опасность повреждения: что произойдет, если ваша запись в хранилище событий будет успешной, но ваша запись в базу данных записи завершится неудачно?
— Федор Сойкин

1
Если проекция чтения не удалась, она будет просто повторяться, пока не выполнится. Если он аварийно завершится, он проснется и продолжит с того места, где он разбился, другими словами, также повторите попытку. Внешний наблюдаемый эффект ничем не отличается от того, что он работает немного медленнее. Если проекция продолжает терпеть неудачу и постоянно терпит неудачу, это будет означать, что в ней есть ошибка, и это нужно будет исправить. После исправления он возобновит работу с последнего хорошего состояния. Если в результате ошибки вся база данных чтения будет повреждена, я просто перестрою базу данных с нуля, используя историю событий.
— Федор Сойкин

1
Данные никогда не теряются, вот в чем суть. Данные на некоторое время могут застрять в неудобной (для чтения) форме, но они никогда не теряются.
— Федор Сойкин

3

Основная причина - производительность. Вы можете сохранить снимок для каждого коммита (commit = события, которые генерируются одной командой, обычно только одним событием), но это дорого. Вдоль снимка необходимо также сохранить коммит, иначе это не будет Event Sourcing. И все это должно быть сделано атомарно, все или ничего. Ваш вопрос действителен, только если используются отдельные базы данных / таблицы / коллекции (в противном случае это был бы именно источник событий), поэтому вы вынуждены использовать транзакции , чтобы гарантировать согласованность. Транзакции не масштабируются. Поток событий только для добавления (хранилище событий) является основой масштабируемости.

Вторая причина - совокупная инкапсуляция. Вы должны защитить это. Это означает, что Агрегат должен иметь возможность изменять свое внутреннее представительство в любое время. Если вы храните его и сильно зависите от него, то вам будет очень трудно работать с версиями, что наверняка произойдет. В ситуации, когда вы используете снимок только в качестве оптимизации, при изменении схемы вы просто игнорируете эти снимки ( просто ? Я действительно так не думаю; удачи в определении того, что схема Aggregate изменяет - включая все вложенные объекты и объекты-значения) в эффективный способ и управление этим).


Когда моя Aggregate-схема изменится, не будет ли простым воспроизведением моих событий генерировать обновленную базу данных «записи»?
— MetaFight

Проблема заключается в обнаружении этого изменения. Агрегат может быть очень большим со многими файлами / классами.
— Константин Гальбену

Я не понимаю Изменение произойдет с выпуском программного обеспечения. Релиз, вероятно, будет поставляться со сценарием базы данных для регенерации базы данных «запись».
— MetaFight

Многое нужно сделать для скрипта миграции. Пока он работает, приложение должно быть выключено.
— Константин Гальбену

@MetaFight, если поток очень большой, для перестройки новой схемы агрегата потребуется много времени ... Сейчас я думаю о снимке, который представляет собой состояние прямой проекции, которая может быть запущена до выпуска нового агрегата. схема
— Нарвалекс
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.