CQRS Event Sourcing: проверка уникальности UserName

Возьмем простой пример "Регистрация учетной записи", вот поток:

  • Веб-сайт посетителя
  • Нажмите кнопку "Зарегистрироваться" и заполните форму, нажмите кнопку "Сохранить"
  • MVC-контроллер: проверьте уникальность уникального имени пользователя, прочитав ReadModel
  • RegisterCommand: снова подтвердить уникальность имени пользователя (вот вопрос)

Конечно, мы можем проверить уникальность UserName, прочитав ReadModel в MVC-контроллере, чтобы повысить производительность и пользовательский интерфейс. Тем не менее, нам все равно нужно снова проверить уникальность в RegisterCommand, и, очевидно, мы НЕ должны получать доступ к ReadModel в командах.

Если мы не используем Event Sourcing, мы можем запросить модель домена, чтобы не было проблем. Но если мы используем Event Sourcing, мы не можем запросить модель домена, так как мы можем проверить уникальность UserName в RegisterCommand?

Примечание. Класс пользователя имеет свойство Id, а UserName не является ключевым свойством класса User. Мы можем получить объект домена только с помощью идентификатора при использовании источника событий.

BTW: В требовании, если введенное UserName уже принято, веб-сайт должен показать сообщение об ошибке "К сожалению, имя пользователя XXX недоступно" посетителю. Неприемлемо показывать сообщение, скажем: "Мы создаем вашу учетную запись, пожалуйста, подождите, мы отправим вам результат регистрации по электронной почте позже", посетителю.

Есть идеи? Большое спасибо!

[ОБНОВИТЬ]

Более сложный пример:

Требование:

При размещении заказа система должна проверять историю заказа клиента, если он является ценным клиентом (если клиент разместил по меньшей мере 10 заказов в месяц в прошлом году, он ценен), мы делаем 10% скидку на заказ.

Реализация:

Мы создаем PlaceOrderCommand, и в команде нам нужно запросить историю заказов, чтобы узнать, является ли клиент ценным. Но как мы можем это сделать? Мы не должны получать доступ к ReadModel в команде! Как сказал Микаэль, мы можем использовать компенсирующие команды в примере регистрации учетной записи, но если мы также будем использовать это в этом примере заказа, это будет слишком сложно, и код может быть слишком сложным в обслуживании.

Ответ 1

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

Однако, если вы чувствуете, что по какой-то причине вы должны обращаться с ним, или если вы просто чувствуете, что хотите знать, как справиться с таким делом, вот один из способов:

Вы не должны обращаться к модели чтения из обработчика команд или домена при использовании источников событий. Однако вы можете использовать службу домена, которая будет прослушивать событие UserRegistered, в котором вы снова получите доступ к модели чтения, и проверьте, не является ли имя пользователя еще не дублируемым. Конечно, вам нужно использовать UserGuid здесь, а также ваша прочитанная модель могла быть обновлена пользователем, которого вы только что создали. Если найден дубликат, у вас есть возможность отправить компенсирующие команды, такие как изменение имени пользователя и уведомление пользователя о том, что имя пользователя было принято.

Это один из подходов к проблеме.

Как вы, вероятно, видите, это невозможно сделать синхронным образом запроса-ответа. Чтобы решить эту проблему, мы используем SignalR для обновления пользовательского интерфейса, когда есть что-то, что мы хотим нажать на клиента (если они все еще подключены, то есть). Мы делаем то, что мы позволяем веб-клиенту подписываться на события, которые содержат информацию, которая полезна для клиента, чтобы сразу увидеть.

Обновить

Для более сложного случая:

Я бы сказал, что размещение заказа менее сложное, так как вы можете использовать прочитанную модель, чтобы узнать, ценен ли клиент, прежде чем отправлять команду. Собственно, вы можете запросить, когда вы загружаете форму заказа, так как вы, вероятно, хотите показать клиенту, что они получат 10% скидку, прежде чем разместить заказ. Просто добавьте скидку на PlaceOrderCommand и, возможно, повод для скидки, чтобы вы могли отслеживать, почему вы сокращаете прибыль.

Но опять же, если вам действительно нужно рассчитать скидку после того, как заказ по каким-то причинам был местом, снова используйте службу домена, которая будет слушать OrderPlacedEvent а команда "компенсация" в этом случае, вероятно, будет DiscountOrderCommand или что-то еще. Эта команда повлияет на корень агрегата заказа, и информация может быть распространена на ваши прочитанные модели.

Для дублированного имени пользователя:

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

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

Когда дело доходит до SignalR, я использую концентратор SignalR, к которому подключаются пользователи, когда они загружают определенную форму. Я использую функциональность SignalR Group, которая позволяет мне создать группу, которую я называю значением Guid, которое я посылаю в команде. Это может быть userGuid в вашем случае. Затем у меня есть Eventhandler, который подписывается на события, которые могут быть полезны для клиента, и когда приходит событие, я могу вызвать функцию javascript для всех клиентов в SignalR Group (которая в этом случае будет только одним клиентом, создающим дублирующее имя пользователя в вашем дело). Я знаю, что это звучит сложно, но на самом деле это не так. У меня было все это после обеда. На странице SignalR Github есть большие документы и примеры.

Ответ 2

Я думаю, что вам еще предстоит изменить менталитет к возможной последовательности и природе источников событий. У меня такая же проблема. В частности, я отказался признать, что вы должны доверять командам от клиента, которые, используя ваш пример, скажут "Поместите этот заказ с 10% скидкой" без домена, подтверждающего, что скидка должна идти вперед. Одна вещь, которая действительно поразила меня, - это то, что сам Уди сказал мне (проверьте комментарии принятого ответа).

В основном я понял, что нет причин не доверять клиенту; все на стороне чтения было создано из модели домена, поэтому нет причин не принимать команды. Независимо от того, что читает сторона, которая говорит, что клиент имеет право на скидку, был помещен туда доменом.

BTW: В требовании, если введенное UserName уже принято, веб-сайт должен показать сообщение об ошибке "К сожалению, имя пользователя XXX недоступно" посетителю. Неприемлемо показывать сообщение, скажем: "Мы создаем вашу учетную запись, пожалуйста, подождите, мы отправим вам результат регистрации по электронной почте позже", посетителю.

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

ОБНОВЛЕНИЕ: октябрь 2015 г.

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

Ответ 3

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

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

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

Ответ 4

Как и многие другие при внедрении системы, основанной на событиях, мы столкнулись с проблемой уникальности.

Сначала я был сторонником предоставления клиенту доступа к стороне запроса перед отправкой команды, чтобы узнать, уникально ли имя пользователя или нет. Но потом я пришел к выводу, что наличие back-end, которое имеет нулевую проверку на уникальность, является плохой идеей. Зачем вообще что-либо принуждать, когда можно отправить команду, которая повредила бы систему? Внутренняя часть должна проверять все, что она вводит, иначе вы открыты для непоследовательных данных.

Мы сделали, чтобы создать index на стороне команды. Например, в простом случае имени пользователя, которое должно быть уникальным, просто создайте UserIndex с полем имени пользователя. Теперь команда может проверить, нет ли имени пользователя в системе или нет. После выполнения команды безопасно хранить новое имя пользователя в индексе.

Что-то подобное может также работать на проблему скидок Order.

Преимущества в том, что ваша команда back-end правильно проверяет все входные данные, поэтому не могут быть сохранены несогласованные данные.

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

Ответ 5

Я думаю, что для таких случаев мы можем использовать такой механизм, как "консультативный замок с истечением срока действия".

Пример выполнения:

  • Проверка имени пользователя существует или нет в окончательно согласованной модели чтения
  • Если не существует; используя хранилище или кеш-память, подобную ключевому слову redis-couchbase; попробуйте указать имя пользователя как ключевое поле с некоторым истечением срока действия.
  • В случае успеха; затем поднимите userRegisteredEvent.
  • Если либо имя пользователя существует в модели чтения, либо в кэше, сообщите посетителю, что имя пользователя приняло.

Даже вы можете использовать базу данных sql; вставьте имя пользователя в качестве первичного ключа какой-либо таблицы блокировок; а затем запланированное задание может работать с истечением срока действия.

Ответ 6

По поводу уникальности я реализовал следующее:

  • Первая команда, такая как "StartUserRegistration". UserAggregate будет создан независимо от того, является ли пользователь уникальным или нет, но со статусом RegistrationRequested.

  • При "UserRegistrationStarted" асинхронное сообщение будет отправлено службе без сохранения состояния "UsernamesRegistry". будет что-то вроде "RegisterName".

  • Служба будет пытаться обновить (без запросов, "не говори") таблицу, которая будет содержать уникальное ограничение.

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

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

Подведение итогов:

  • Этот подход не требует запросов.

  • Регистрация пользователя будет всегда создаваться без проверки.

  • Процесс подтверждения будет включать два асинхронных сообщения и одну вставку БД. Таблица не является частью модели чтения, а является службой.

  • Наконец, одна асинхронная команда, чтобы подтвердить, что пользователь действителен.

  • На этом этапе денормализатор может реагировать на событие UserRegistrationConfirmed и создавать модель чтения для пользователя.

Ответ 7

Рассматривали ли вы использование "рабочего" кеша как своего рода RSVP? Трудно объяснить, потому что он работает в течение некоторого цикла, но в основном, когда новое имя пользователя "заявлено" (то есть команда была выпущена для его создания), вы поместите имя пользователя в кеш с коротким сроком действия ( достаточно долго, чтобы учесть другой запрос, проходящий через очередь и денормализованный в считываемую модель). Если это один экземпляр службы, то в памяти, вероятно, будет работать, иначе централизовать его с Redis или что-то еще.

Затем, когда следующий пользователь заполняет форму (при наличии переднего конца), вы асинхронно проверяете модель чтения на наличие имени пользователя и предупреждаете пользователя, если он уже сделан. Когда команда отправляется, вы проверяете кеш (а не модель чтения), чтобы проверить запрос перед принятием команды (перед возвратом 202); если имя находится в кеше, не принимайте команду, если нет, то вы добавляете ее в кеш; если его сбой не сработает (дублируйте ключ, потому что какой-то другой процесс вас побьет), тогда предположим, что это имя принято, а затем соответствующим образом реагировать на клиента. Между двумя вещами, я не думаю, что будет много возможностей для столкновения.

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

Ответ 8

Мне кажется, что, возможно, совокупность здесь не так.

В общих чертах, если вам нужно гарантировать, что значение Z, принадлежащее Y, уникально в пределах множества X, тогда используйте X в качестве агрегата. В конце концов, X - это то место, где инвариант действительно существует (в X может быть только один Z).

Другими словами, ваш инвариант заключается в том, что имя пользователя может появляться только один раз в пределах области действия всех пользователей вашего приложения (или может быть другой областью действия, например в рамках организации и т.д.). Если у вас есть совокупность "ApplicationUsers" и отправка команда "RegisterUser", то вы должны иметь то, что вам нужно, чтобы убедиться, что команда действительна до сохранения события "UserRegistered". (И, конечно, затем вы можете использовать это событие для создания проекций, которые вам нужны для выполнения таких задач, как аутентификация пользователя без необходимости загрузки всей совокупности "ApplicationUsers".