Источник событий CQRS: проверка уникальности имени пользователя



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

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

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

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

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

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

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

[обновление]

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

требования:

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

реализация:

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

183   7  

7 ответов:

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

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

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

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

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

обновление

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

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

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

для дубликата имени пользователя case:

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

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

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

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

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

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

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

обновление: октябрь 2015

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

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

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

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

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

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

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

что-то вроде этого также может работать для скидки заказа проблема.

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

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

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

образец выполнения:

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

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

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

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

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

об уникальности, я реализовал следующим образом:

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

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

  • сервис будет пытаться обновить (нет запросы, "tell don't ask") таблица, которая будет включать уникальное ограничение.

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

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

подводя итоги:

  • этот подход не предполагает запросов.

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

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

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

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

    Ничего не найдено.

Добавить ответ:
Отменить.