在CQRS中实现基于集合的约束

时间:2010-12-04 21:12:58

标签: azure cqrs azure-table-storage dddd

我仍然在努力解决与CQRS风格架构相关的基本(和解决)问题:

我们如何实施依赖一组聚合根源的业务规则?

以一个预订申请为例。它可以让您预订音乐会的门票,电影的座位或餐厅的桌子。在所有情况下,只有有限数量的“商品”待售。

让我们想象一下这个活动或地点非常受欢迎。当销售对新活动或时段开放时,预订开始很快到达 - 可能每秒很多。

在查询方面,我们可以进行大规模扩展,并将预留放在队列中,由自治组件异步处理。首先,当我们从队列中取消预订命令时,我们会接受它们,但在某个时间我们必须开始拒绝其余的

我们如何知道何时达到限制?

对于每个预约命令,我们必须查询某种商店以确定我们是否可以容纳请求。这意味着我们需要知道当时已收到多少预订。

但是,如果域存储是非关系数据存储,例如, Windows Azure表存储,我们不能很好地执行SELECT COUNT(*) FROM ...

一种选择是保持单独的聚合根,只需跟踪当前计数,如下所示:

  • AR:预订(谁?多少?)
  • AR:事件/时间段/日期(总计数)

第二个聚合根将是第一个聚合根的非规范化聚合,但是当底层数据存储不支持事务时,很可能这些在大容量场景中会不同步(这就是我们我们试图首先解决这个问题。)

一种可能的解决方案是序列化处理预约命令,以便一次只处理一个,但这违背了我们的可扩展性(和冗余)目标。

这种情况让我想起标准的“缺货”情景,但区别在于我们不能很好地将预订放在后面。一旦活动售罄,它就会售罄,所以我看不出会有什么补偿行动。

我们如何处理这种情况?

4 个答案:

答案 0 :(得分:3)

在考虑了一段时间后,我终于意识到基础问题与CQRS的关系不如与不同REST服务的非交易性质相关。

真的可以归结为这个问题:如果需要更新多个资源,如果第二次写入操作失败,如何确保一致性?

让我们假设我们想要按顺序写入资源A和资源B的更新。

  1. 资源A已成功更新
  2. 尝试更新资源B失败
  3. 面对异常时,第一次写操作不能轻易回滚,那么我们能做什么呢?捕获和抑制异常以对资源A执行补偿操作不是可行的选择。首先,它实现起来很复杂,但其次不安全:如果由于网络连接失败而发生第一个异常会发生什么?在这种情况下,我们也无法针对资源A编写补偿行动。

    关键在于明确的幂等性。虽然Windows Azure队列不保证完全一次语义,但它们确实保证至少一次语义。这意味着,面对间歇性异常,该消息稍后将重播

    在上一个场景中,这就是:

    1. 尝试更新资源A.但是,检测到重放,因此A的状态不受影响。但是,'write'操作成功。
    2. 资源B已成功更新。
    3. 当所有写入操作都是幂等的时,消息重放可以实现最终一致性

答案 1 :(得分:2)

有趣的问题,在这个问题上,你正在确定CQRS的一个难点。

亚马逊处理此问题的方法是,如果请求的项目已售罄,则业务方案可以应对错误状态。错误状态只是通过电子邮件通知客户当前没有库存的物品和估计的运输日期。

然而 - 这并不能完全回答你的问题。

考虑到销售门票的情况,我会确保告诉客户他们提出的请求是预订请求。预订请求将尽快得到处理,并且他们将在稍后通过邮件恢复最终答案。通过改变这一点,一些客户可能会收到拒绝其请求的电子邮件。

现在。我们可以减少痛苦吗?当然。通过在我们的分布式缓存中插入一个密钥,其中包含库存中的项目百分比或数量,并在销售某个项目时递减此计数器。通过这种方式,我们可以在给出预订请求之前警告用户,假设只剩下初始数量项目的10%,客户可能无法获得相关项目。如果计数器为零,我们将拒绝接受任何更多的预订请求。

我的观点是:

1)让用户知道这是他们正在发出的请求,这可能会被拒绝 2)告知用户获得相关项目的成功率很低

对你的问题不完全准确的答案,但这是我在处理CQRS时如何处理这样的情况。

答案 2 :(得分:1)

eTag支持乐观并发,您可以使用它来代替事务锁定来更新文档并安全地处理潜在的竞争条件。有关详细信息,请参阅此处的评论http://msdn.microsoft.com/en-us/library/dd179427.aspx

故事可能会是这样的: 用户A创建一个事件E,最大票数为2,eTag为123.由于需求量很大,3个用户几乎同时尝试购买票证。 用户B创建预订请求B. 用户C创建预订请求C. 用户D创建预订请求D.

系统S接收预约请求B,用eTag 123读取事件并将事件更改为剩余1张票,S提交包括与原始eTag匹配的eTag 123的更新,以便更新成功。 eTag现在为456.预订请求已获批准,用户通知成功。

另一个系统S2在系统S正在处理请求B的同时接收预约请求C,因此它还读取事件,eTag 123将事件更改为1个剩余票据并尝试更新文档。但是这次eTag 123不匹配,因此更新失败并出现异常。系统S2尝试通过重新读取现在具有eTag 456且计数为1的文档来重试该操作,因此将其递减为0并使用eTag 456重新提交。

不幸的是,对于用户C用户,System S在用户B之后立即开始处理用户D的请求并且还使用eTag 456读取文档但是因为系统S比系统S2快,所以它能够在系统S2之前用eTag 456更新事件。用户D也成功保留了他的机票。 eTag现在是789

因此系统S2再次失败,再给它一次尝试但是这次当它用eTag 789读取事件时,它发现没有可用的票据,因此拒绝用户C的预订请求。

如何通知用户他们的预订请求是否成功取决于您。您可以每隔几秒钟轮询服务器并等待更新预订状态。

答案 3 :(得分:1)

让我们看看商业角度(我处理类似的事情 - 在免费老虎机上预约)......

你的分析中第一件让我感到震惊的事情就是没有可预订的票/座位/桌子的概念。这些是预订的资源。

如果是交易,您可以使用某种形式的唯一性来确保同一票/座位/表不会发生双重预订(http://seabites.wordpress.com/2010/11/11/consistent-indexes-constraints的更多信息)。此方案需要同步(但仍然是并发)命令处理。

如果不是交易,您可以追溯监控事件流并补偿命令。您甚至可以为最终用户提供等待预订确认的体验,直到系统确定 - 即在事件流分析之后 - 该命令已完成并且已经或未被补偿(归结为“是否已进行预订?是还是不是?”)。换句话说,补偿可以成为确认周期的一部分。

让我们再退一步......

当涉及账单时(例如在线门票销售),我认为这整个场景无论如何都会变成一个传奇(预订机票+票据票)。即使没有账单,您也会有一个传奇(保留表+确认预订),以使体验可信。因此,即使您只是放大了预订机票/桌子/座位的一个方面(即它仍然可用),“长期运行”的交易在我支付或直到我确认之前都没有完成。无论如何都会发生赔偿,当我以任何理由中止交易时再次取消罚单。有趣的部分现在变成了企业想要处理的事情:如果我们给他/她相同的票,也许其他一些客户会完成交易。在这种情况下,当双重预订机票/座位/桌子时,退款可能会变得更有趣 - 甚至可以为下一次/类似活动提供折扣以弥补不便。答案在于商业模式,而不是技术模型。