CQRS事件采购:验证UserName唯一性

Mou*_*Lin 69 domain-driven-design unique cqrs event-sourcing

我们来看一个简单的"帐户注册"示例,这里是流程:

  • 用户访问网站
  • 点击"注册"按钮并填写表格,点击"保存"按钮
  • MVC控制器:通过从ReadModel读取来验证UserName的唯一性
  • RegisterCommand:再次验证UserName唯一性(这是问题)

当然,我们可以通过读取MVC控制器中的ReadModel来验证UserName的唯一性,以提高性能和用户体验.但是,我们仍然需要在RegisterCommand中再次验证唯一性,显然,我们不应该在命令中访问ReadModel.

如果我们不使用Event Sourcing,我们可以查询域模型,这样就没问题了.但是如果我们使用Event Sourcing,我们无法查询域模型,那么我们如何验证RegisterCommand中的UserName唯一性?

注意: User类具有Id属性,UserName不是User类的键属性.在使用事件源时,我们只能通过Id获取域对象.

顺便说一句:在要求中,如果已经输入了UserName,则网站应向访问者显示错误消息"抱歉,用户名XXX不可用".显示一条消息是不可接受的,例如"我们正在创建您的帐户,请稍后,我们会通过电子邮件将注册结果发送给您"给访问者.

有任何想法吗?非常感谢!

[UPDATE]

一个更复杂的例子:

需求:

下订单时,系统应该检查客户的订购历史,如果他是一个有价值的客户(如果客户在去年每月至少订购10个订单,他是有价值的),我们会在订单上减价10%.

执行:

我们创建PlaceOrderCommand,在命令中,我们需要查询订购历史记录以查看客户端是否有价值.但是我们怎么能这样做呢?我们不应该在命令中访问ReadModel!正如Mikael 所说,我们可以在帐户注册示例中使用补偿命令,但如果我们在此订购示例中也使用补偿命令,那么它将太复杂,并且代码可能难以维护.

Mik*_*erg 36

如果在发送命令之前使用读取模型验证用户名,我们正在谈论一个几百毫秒的竞争条件窗口,其中可能发生真正的竞争条件,这在我的系统中未被处理.与处理它的成本相比,它不太可能发生.

但是,如果你觉得你必须出于某种原因处理它,或者如果你只是觉得你想知道如何掌握这种情况,这里有一种方法:

使用事件源时,不应从命令处理程序或域访问读取模型.但是,您可以使用的域服务将侦听再次访问读取模型的UserRegistered事件,并检查用户名是否仍然不重复.当然,您需要在此处使用UserGuid,并且您的读取模型可能已经与您刚刚创建的用户进行了更新.如果找到重复,您有机会发送补偿命令,例如更改用户名并通知用户用户名已被删除.

这是解决问题的一种方法.

您可能已经看到,无法以同步请求 - 响应方式执行此操作.为了解决这个问题,我们正在使用SignalR在我们想要推送到客户端时更新UI(如果它们仍然连接,那就是).我们所做的是让Web客户端订阅包含对客户端立即查看有用的信息的事件.

更新

对于更复杂的情况:

我会说订单放置不太复杂,因为在发送命令之前,您可以使用读取模型来确定客户端是否有价值.实际上,您可以在加载订单时查询,因为您可能希望向客户显示在下订单之前他们将获得10%的折扣.只需为折扣添加折扣PlaceOrderCommand,也许是折扣的原因,这样您就可以追踪削减利润的原因.

但话说回来,如果你真的需要在订单出于某种原因之后计算折扣,再次使用一个可以收听的域服务,OrderPlacedEvent在这种情况下"补偿"命令可能是一个DiscountOrderCommand或者什么.该命令将影响Order Aggregate root,并且信息可以传播到您的读取模型.

对于重复的用户名案例:

您可以ChangeUsernameCommand从域服务发送一个补偿命令.甚至更具体的东西,这将描述用户名更改的原因,这也可能导致创建Web客户端可以订阅的事件,以便您可以让用户看到用户名是重复的.

在域服务上下文中,我会说您也可以使用其他方式通知用户,例如发送可能有用的电子邮件,因为您无法知道用户是否仍然连接.也许该通知功能可以由Web客户端订阅的相同事件启动.

说到SignalR,我使用了一个SignalR Hub,用户在加载某个表单时会连接到它.我使用SignalR Group功能,它允许我创建一个组,我命名我在命令中发送的Guid的值.在您的情况下,这可能是userGuid.然后我有Eventhandler订阅可能对客户端有用的事件,当事件到达时,我可以在SignalR组中的所有客户端上调用javascript函数(在这种情况下,只有一个客户端在您的客户端创建重复的用户名)案件).我知道这听起来很复杂,但事实并非如此.我把这一切都安排在一个下午.SignalR Github页面上有很棒的文档和示例.


Dav*_*ers 23

我认为您尚未将思维模式转变为最终的一致性和事件采购的本质.我有同样的问题.具体而言,我拒绝接受您应该信任来自客户的命令,使用您的示例,在没有域确认折扣应该继续的情况下说"以10%折扣放置此订单".Udi本人对我说的一件事就是让我回家的一件事(查看接受答案的评论).

基本上我意识到没有理由不相信客户; 读取端的所有内容都是从域模型生成的,因此没有理由不接受命令.无论读取方面是什么,表示客户有资格享受折扣,都是由域名提供的.

顺便说一句:在要求中,如果已经输入了UserName,则网站应向访问者显示错误消息"抱歉,用户名XXX不可用".显示一条消息是不可接受的,例如"我们正在创建您的帐户,请稍后,我们会通过电子邮件将注册结果发送给您"给访问者.

如果您要采用事件源和最终一致性,则需要接受有时在提交命令后无法立即显示错误消息.使用唯一的用户名示例,这种情况发生的可能性非常小(假设您在发送命令之前检查读取方面)它不值得担心太多,但是需要为此方案发送后续通知,或者可能要求他们下次登录时会使用不同的用户名.这些场景的好处在于它可以让您思考业务价值以及真正重要的事情.

更新:2015年10月

只是想补充一点,实际上,面向公众的网站 - 表明已经采取的电子邮件实际上违反了安全最佳实践.相反,注册似乎已成功通知用户已发送验证电子邮件,但在用户名存在的情况下,电子邮件应通知他们并提示他们登录或重置其密码.虽然这仅在使用电子邮件地址作为用户名时才有效,我认为这是可行的.

  • +1只是在这里真的*迂腐...... ES和EC是两个完全不同的东西,使用一个不应该暗示使用另一个(尽管,在大多数情况下它是完全合理的).在没有最终一致模型的情况下使用ES是完全有效的,反之亦然. (5认同)
  • 让你的回答接近核心,意味着围绕"不变量","商业规则","高度封装"的所有理论都是绝对无稽之谈.不信任UI的原因太多了.毕竟UI不是强制性的部分......如果没有UI怎么办? (5认同)
  • 出色的输入.这是必须在系统之前改变的头脑(我不打算听起来像Yoda). (3认同)
  • @StephenDrew - 此上下文中的客户端仅表示生成命令的任何代码单元.您可能(也许应该)在命令总线之前有一个层.如果您正在创建外部Web服务,则下订单的mvc控制器将首先执行查询,然后提交命令.这里的客户是你的控制器. (3认同)

Gaz*_*dge 12

创建一些立即一致的读取模型(例如,不通过分布式网络)在与命令相同的事务中更新是没有错的.

读取模型最终在分布式网络上保持一致有助于支持重读取系统的读取模型的缩放.但没有什么可以说你不能拥有一个立即一致的领域特定的阅读模型.

立即一致的读取模型仅用于在发出命令之前检查和接收数据(实际上它是对命令的服务),您不应该使用它直接向用户显示读取数据(即来自GET Web请求或类似命令) ).最终使用有意义的,可扩展的读取模型.


Jon*_*gat 7

像许多其他人一样,在实现基于事件源的系统时,我们遇到了唯一性问题.

起初,我是一个支持者,让客户端在发送命令之前访问查询端,以便找出用户名是否唯一.但后来我发现有一个对唯一性没有验证的后端是一个坏主意.当有可能发布破坏系统的命令时,为什么要强制执行任何操作?后端应验证其所有输入,否则您将打开不一致的数据.

我们所做的是index在命令端创建一个.例如,在需要唯一的用户名的简单情况下,只需创建一个带有用户名字段的UserIndex.现在,命令端可以检查用户名是否已经在系统中.执行该命令后,可以安全地将新用户名存储在索引中.

这样的东西也适用于订单折扣问题.

好处是您的命令后端可以正确验证所有输入,因此不会存储任何不一致的数据.

缺点可能是您需要对每个唯一性约束进行额外查询,并且您要强制执行额外的复杂性.


Saf*_*soy 6

我认为对于这种情况,我们可以使用像"过期咨询锁"这样的机制.

样品执行:

  • 在最终一致的读取模型中检查用户名是否存在
  • 如果不存在; 通过使用像keyvalue存储或缓存的redis-couchbase; 尝试将用户名作为关键字段推送一些过期.
  • 如果成功; 然后引发userRegisteredEvent.
  • 如果读取模型或缓存存储中存在任一用户名,请通知访问者用户名已经使用.

即使你可以使用sql数据库; 插入用户名作为某些锁定表的主键; 然后预定的工作可以处理到期.


小智 5

关于唯一性,我实现了以下内容:

  • 第一个命令,例如“ StartUserRegistration”。无论用户是否唯一,都将创建UserAggregate,并且状态为RegistrationRequested。

  • 在“ UserRegistrationStarted”上,异步消息将发送到无状态服务“ UsernamesRegistry”。类似于“ RegisterName”。

  • 服务将尝试更新(无查询,“不告诉”)表,该表将包含唯一约束。

  • 如果成功,服务将通过另一条消息(异步)回复,并带有某种授权“ UsernameRegistration”,表明该用户名已成功注册。您可以包括一些requestId来跟踪并发能力(不太可能)。

  • 上面消息的发布者现在已经授权该名称是自己注册的,因此现在可以安全地将UserRegistration聚合标记为成功。否则,标记为已丢弃。

包起来:

  • 这种方法不涉及任何查询。

  • 用户注册将始终在没有验证的情况下创建。

  • 确认过程将涉及两个异步消息和一个数据库插入。该表不是读取模型的一部分,而是服务的一部分。

  • 最后,一个异步命令来确认用户有效。

  • 在这一点上,反规范化器可以对UserRegistrationConfirmed事件做出反应,并为用户创建读取模型。

  • 我做类似的事情。在我的事件源系统中,我有一个 UserName 聚合。它的 AggregateID 是我想注册的用户名。我发出一个命令来注册它。如果它已经注册,我们会收到一个事件。如果它可用,那么它会立即注册,我们会收到一个事件。我尽量避免使用“服务”,因为他们有时会觉得域中存在建模缺陷。通过使用户名成为第一类聚合,我们对域中的约束进行建模。 (2认同)