8

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

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

以预订应用程序为例。它可以让您预订音乐会的门票、电影的座位或餐厅的餐桌。在所有情况下,只有有限数量的“物品”可供出售。

让我们假设某个事件或地点非常受欢迎。当新活动或时间段的销售开始时,预订开始很快到达 - 可能每秒很多。

在查询方面,我们可以大规模扩展,并将预订放在队列中,由自治组件异步处理。起初,当我们从队列中拉出保留命令时,我们将接受它们,但在某个时间我们将不得不开始拒绝其余的命令。

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

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

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

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

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

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

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

这样的场景让我想起了标准的“缺货”场景,但不同的是我们不能很好地将预订放在延期交货上。一旦一个活动售罄,它就已经售罄,所以我看不出有什么补偿措施。

我们如何处理这样的场景?

4

4 回答 4

4

在考虑了一段时间后,我终于明白,根本问题与 CQRS 的关系比与不同 REST 服务的非事务性质的关系要小。

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

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

  1. 资源 A 已成功更新
  2. 更新资源 B 的尝试失败

第一次写操作在遇到异常时不能轻易回滚,那怎么办呢?捕获和抑制异常以对资源 A 执行补偿操作不是一个可行的选择。首先它实现起来很复杂,其次它不安全:如果第一个异常是由于网络连接失败而发生的,会发生什么?在那种情况下,我们也不能针对资源 A 编写补偿操作。

关键在于显式幂等性。虽然 Windows Azure 队列不保证完全一次语义,但它们确实保证至少一次语义。这意味着在遇到间歇性异常时,稍后会重播消息。

在前面的场景中,接下来会发生以下情况:

  1. 尝试更新资源 A。但是,检测到重放,因此 A 的状态不受影响。但是,“写入”操作成功。
  2. 资源 B 已成功更新。

当所有写操作都是幂等的时,可以通过消息重放来实现最终的一致性

于 2010-12-23T14:18:07.090 回答
2

有趣的问题,通过这个问题,您可以解决 CQRS 的痛点之一。

亚马逊处理此问题的方式是让业务场景在请求的商品售罄时处理错误状态。错误状态只是通过电子邮件通知客户请求的物品当前没有库存以及预计的发货日期。

但是 - 这并不能完全回答您的问题。

考虑到销售门票的场景,我会确保告诉客户他们提出的请求是预订请求。预订请求将尽快得到处理,他们稍后将在邮件中恢复最终答案。如果不这样做,一些客户可能会收到一封拒绝其请求的电子邮件。

现在。我们能让这种拒绝不那么痛苦吗?当然。通过在我们的分布式缓存中插入一个带有库存商品百分比或数量的键,并在商品售出时递减该计数器。这样,我们可以在给出预订请求之前警告用户,假设如果只剩下初始数量的 10%,那么客户可能无法获得有问题的项目。如果柜台为零,我们将简单地拒绝接受任何更多的预订请求。

我的观点是:

1) 让用户知道这是他们正在提出的请求并且这可能会被拒绝 2) 通知用户成功获得相关项目的机会很低

不完全是您问题的准确答案,但这就是我在处理 CQRS 时处理这种情况的方式。

于 2010-12-04T22:37:39.087 回答
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 用户,系统 S 在用户 B 之后立即开始处理用户 D 的请求,并且还使用 eTag 456 读取文档,但是由于系统 S 比系统 S2 更快,它能够在系统 S2 之前使用 eTag 456 更新事件,因此用户 D 也成功预订了他的票。eTag 现在是 789

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

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

于 2010-12-04T23:14:20.637 回答
1

让我们从业务角度来看(我处理类似的事情 - 在免费时段预约)......

在您的分析中,让我印象深刻的第一件事是没有可预订的票/座位/桌子的概念。这些是被预订的资源。

在交易的情况下,您可以使用某种形式的唯一性来确保不会发生同一张票/座位/桌子的重复预订(更多信息请访问http://seabites.wordpress.com/2010/11/11/一致的索引约束)。这种情况需要同步(但仍然是并发的)命令处理。

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

让我们再退一步……

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

于 2010-12-06T17:30:22.480 回答