2012-02-29 42 views
54

让我们以一个简单的“注册帐号”的例子,这里是流量:CQRS事件采购:验证用户名的唯一性

  • 用户访问网站
  • 点击“注册”按钮,填写表格,点击“保存”按钮
  • MVC控制器:验证用户名的唯一性由ReadModel
  • RegisterCommand阅读:再次验证用户名的唯一性(这里是问题)

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

如果我们不使用事件采购,我们可以查询领域模型,所以这没有问题。但是如果我们使用事件采购,我们无法查询域模型,所以我们如何验证RegisterCommand中的用户名唯一性?

说明:用户类具有Id属性,UserName不是User类的关键属性。使用事件采购时,我们只能通过Id获取域对象。

BTW:在需求,如果输入的用户名已被使用,该网站应显示错误消息“对不起,该用户名XXX不可用”的访问者。显示一条消息,例如说,“我们正在创建您的帐户,请等待,我们会在稍后通过电子邮件将注册结果发送给您”,这是不能接受的。

任何想法?非常感谢!

[更新]

更复杂的例子:

要求:

下订单时,系统应检查客户的订货历史,如果他是一个有价值的客户(如果客户在过去一年每月至少订购10份订单,​​他很有价值),我们将订单打九折。

实现:

我们创造PlaceOrderCommand,并在命令中,我们需要查询订货历史,看看如果客户是有价值的。但我们怎么做到这一点?我们不应该在命令中使用ReadModel!作为Mikael said,我们可以在帐户注册示例中使用补偿命令,但是如果我们也在此排序示例中使用补偿命令,则它会太复杂,并且代码可能太难以维护。

回答

34

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

不过,如果你觉得你必须处理它由于某种原因,或者如果你只是觉得你想知道如何掌握这种情况下,这里是一个办法:

你不应该访问从读模式命令处理程序或使用事件源时的域。但是,您可以执行的操作是使用一个域服务,该服务将监听您再次访问读取模型的UserRegistered事件,并检查用户名是否仍然不重复。当然,您需要在这里使用UserGuid,并且您的读取模型可能已用您刚刚创建的用户更新过。如果发现重复,则可以发送补偿命令,例如更改用户名并通知用户该用户名已被占用。

这是解决问题的方法之一。

你或许可以看到,这是不可能做到这一点的同步请求 - 响应方式。为了解决这个问题,我们使用SignalR来更新UI,只要有一些我们想要推送给客户端的东西(如果它们仍然连接,那就是)。我们所做的是让Web客户端订阅包含对客户立即查看有用的信息的事件。

更新

对于更复杂的情况:

我想说的顺序放置不是很复杂的,因为你可以使用读取模型,找出客户端是否有价值在发送命令之前。事实上,当你加载订单时,你可能会询问,因为你可能想向客户展示在下订单之前他们会得到10%的折扣。只需在PlaceOrderCommand上添加折扣,这也许是折扣的原因,这样您就可以追踪为什么会降低利润。

但话又说回来,如果你真的需要计算折扣后的顺序是出于某种原因的地方,再次使用会听OrderPlacedEvent和“补偿”域名服务在这种情况下,命令可能会是一个DiscountOrderCommand或一些东西。该命令会影响Order Aggregate根,并且信息可能会传播到您的读取模型。

对于重复的用户名的情况:

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

在域名服务方面,我要说的是,你还必须使用其他手段来通知用户,例如发送一样,因为你可以不知道,如果用户仍处于连接状态可能是有用的电子邮件的可能性。也许这个通知功能可能是由Web客户端订阅的相同事件启动的。

当谈到SignalR,我使用SignalR枢纽的方便用户连接时加载一定的形式。我使用SignalR Group功能,它允许我创建一个名为我在命令中发送的Guid值的组。这可能是你的情况userGuid。然后我有Eventhandler订阅可能对客户端有用的事件,并且当事件到达时,我可以调用SignalR Group中所有客户端上的javascript函数(在这种情况下,只有一个客户端在您的账户中创建重复的用户名案件)。我知道这听起来很复杂,但事实并非如此。我把它全部在一个下午建立起来。 SignalR Github页面上有很多文档和例子。

+0

当我发现用户名重复时,我应该在补偿命令中做些什么?发布SignalR事件以通知客户端用户名不可用? (我没有使用过SignalR,我想可能会有某种“事件?”) – 2012-02-29 09:21:32

+0

很好的答案,谢谢!但是我对“域名服务”感到困惑,你是不是指“事件处理程序”?我认为它与DDD中的“域服务”不一样吗? – 2012-02-29 13:00:14

+1

我认为我们称之为DDD中的应用服务,但我可能弄错了,而域服务在DDDD/CQRS社区中是一个争论的话题。除了你可能不需要状态机或状态机之外,它们与他们所说的Saga类似,你只需要一些能够反应和反馈事件,执行数据查找和调度命令的东西,我称之为域服务。订阅事件和发送命令,这在聚合根节点之间进行通信时非常有用 – 2012-02-29 14:48:35

18

我想你还没有拥有的心态转变为eventual consistency和采购活动的性质。我有同样的问题。具体而言,我拒绝接受您应该信任来自客户的命令,使用您的示例,如果没有域验证折扣应该继续,请说“以10%的折扣下订单”。有一件事对我来说真的很重要,它是something that Udi himself said to me(查看接受的答案的评论)。

基本上,我才明白,没有理由不信任客户端;读取方面的所有内容都是从域模型生成的,所以没有理由不接受这些命令。无论在什么方面,客户都有资格获得折扣,这一点已经被域名放在那里。

顺便说一下,在要求中,如果输入的用户名已被使用,网站应该向访问者显示错误信息“对不起,用户名XXX不可用”。显示一条消息,例如说,“我们正在创建您的帐户,请等待,我们会在稍后通过电子邮件将注册结果发送给您”,这是不能接受的。

如果你打算采用事件采购&最终一致性,则需要接受有时它会不会有可能提交命令后立即显示错误消息。使用唯一的用户名示例,发生这种情况的可能性非常小(因为您在发送命令之前检查了读取方)它不值得担心太多,但是需要为此方案发送后续通知,或者可能会询问他们在下次登录时使用不同的用户名。关于这些情况的好处是,它让你考虑到商业价值&真正重要的是什么。

更新:2015年10月

只是想补充一点,其实不然,在面向公众的网站而言 - 这表明邮件已经被占用实际上是对安全性的最佳实践。相反,注册似乎已成功通知用户已发送验证电子邮件,但在用户名存在的情况下,电子邮件应通知他们并提示他们登录或重置密码。虽然这只适用于使用电子邮件地址作为用户名,我认为这是明智的。

+3

优秀的输入。这是在系统可以之前必须改变的想法(我并不打算在那里听起来像尤达)。 – 2012-02-29 21:06:12

+4

+1在这里真正*迂腐...... ES和EC是两个完全不同的东西,使用一个不应该暗示使用其他(尽管在大多数情况下它是非常有意义的)。使用ES没有最终一致的模型是完全有效的,反之亦然。 – James 2014-05-01 14:33:20

+0

“基本上我意识到没有理由不相信客户” - 是的,我认为这是一个公平的评论。但是,如何处理可能产生命令的外部访问呢?显然,我们不希望允许带有自动应用折扣的PlaceOrderCommand;折扣的应用是领域逻辑,而不是我们可以“信任”某人告诉我们应用的东西。 – 2014-05-17 07:35:28

11

没有什么错与创建一些立即读一致性模型(例如不通过分布式网络),它们会在相同的事务中的命令进行更新。

看了模型是最终通过分布式网络一致的帮助重读取系统读取模型的支持比例。但没有什么可说的,你不可能有一个特定领域的读取模型,它们立即一致。

立即一致的读取模型仅用于在发出命令(真正的命令服务)之前检查和接收数据,您不应该使用它直接显示读取数据给用户(即从GET网络请求或类似)。为此,最终使用consitent,可扩展的读取模型。

4

我想对于这样的情况下,我们可以使用类似“咨询锁用过期”的机制。

样品执行:

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

即使您可以使用sql数据库,将用户名作为某个锁定表的主键;然后计划的作业可以处理到期。

3

像许多其他实施基于事件源的系统一样,我们遇到了唯一性问题。

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

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

类似的东西也可以用于订单折扣问题。

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

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

0

你有没有考虑过使用“工作”缓存作为RSVP的排序?这很难解释,因为它在一个循环中有效,但基本上,当一个新的用户名被“声称”(即,该命令是为了创建它而发出的)时,您将用户名放入缓存并且过期很短足够长的时间以解释通过队列的另一个请求并将其非规格化到读取模型中)。如果它是一个服务实例,那么在内存中可能会工作,否则集中Redis或其他东西。

然后,当下一个用户填写表单(假设有一个前端)时,您可以异步检查读取的模型是否有用户名的可用性,并提醒用户是否已经使用。提交命令时,您检查缓存(不是读取模型),以便在接受命令之前验证请求(在返回202之前);如果名称在缓存中,请不要接受该命令,如果不是,则将其添加到缓存;如果添加它失败(重复键,因为其他进程击败了它),然后假定名称被采取 - 然后适当地响应客户端。在这两件事之间,我认为没有太多的碰撞机会。

如果没有前端,那么您可以跳过异步查找,或者至少让您的API提供端点来查找它。您真的不应允许客户端直接与命令模型直接对话,并将API放在它的前面,这样您就可以让API充当命令和读取主机之间的中介。