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

14

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

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

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

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

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

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

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

Ответы:

14

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

VoiceOfUnreason
источник
Все это звучит разумно. Когда я играл с фиктивной реализацией 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, если поток очень большой, для перестройки новой схемы агрегата потребуется много времени ... Сейчас я думаю о снимке, который представляет собой состояние прямой проекции, которая может быть запущена до выпуска нового агрегата. схема
Нарвалекс