Источник событий CQRS: проверка уникальности имени пользователя - PullRequest
62 голосов
/ 29 февраля 2012

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

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

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

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

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

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

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

[UPDATE]

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

Требования:

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

Реализация:

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

Ответы [ 8 ]

36 голосов
/ 29 февраля 2012

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

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

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

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

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

Обновление

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

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

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

Для случая с повторяющимся именем пользователя:

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

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

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

23 голосов
/ 29 февраля 2012

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

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

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

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

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

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

11 голосов
/ 12 мая 2015

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

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

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

5 голосов
/ 24 декабря 2015

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

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

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

Нечто подобное может также помочь при проблеме со скидкой.

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

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

5 голосов
/ 14 июля 2015

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

Пример исполнения:

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

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

3 голосов
/ 30 августа 2018

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

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

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

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

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

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

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

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

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

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

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

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

1 голос
/ 11 апреля 2019

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

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

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

1 голос
/ 30 июля 2017

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

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

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

...