复杂的方法(您确实需要一个商家帐户)是基于API的。它通常分两步执行。首先,支付服务验证您的客户是否可以支付所需的金额,并向您发送一个令牌。其次,您可以在固定的时间内使用令牌,通过将令牌发送回支付服务来完成支付。
Contoso假定其业务客户没有商户帐户,必须使用简单的方法。这样做的一个后果是,在客户完成付款时,座位预订可能会过期。如果发生这种情况,系统会尝试在客户付款后重新获得座位。如果无法重新获得座位,系统会将此问题通知业务客户,业务客户必须手动解决此情况。
备注:该系统允许一点额外的时间,显示在倒计时时钟上,来完成支付过程。在这个特定的场景中,如果没有用户(在本例中是业务所有者,他必须发起退款或覆盖座位配额)的手动干预,系统无法使自己完全一致,这说明了与最终一致性和命令验证相关的以下更普遍的观点。
接受最终一致性的一个关键好处是消除了使用分布式事务的需求,由于大型系统中必须持有的锁的数量和持续时间,分布式事务对可伸缩性和性能有显著的负面影响。在这个特定的场景中,您可以采取以下两种方式来避免在没有座位的情况下接受付款的潜在问题:
把系统更改成在付款前重新检查座位是否有空位。但这是不现实的,因为与支付系统的集成是在没有商户帐户的情况下工作的。
保留座位直到付款完毕。这也很困难,因为你不知道付款过程需要多长时间。您必须预留(锁定)座位一段不确定的时间,等待注册人完成付款。
团队选择允许这样一种可能性,即注册者可以付费购买座位,却发现座位已不再可用。在实际中不太可能发生超时,除非注册者要付费的座位很多。这种方法对系统的影响最小,因为它不需要对任何座位进行长期预订(锁定)。
Markus(软件开发人员)发言:为了进一步减少发生这种情况的机会,团队决定将释放预留座位的缓冲时间从5分钟增加到14分钟。选择5分钟的原始值是为了考虑服务器之间任何可能的时钟倾斜使得在UI中的15分钟倒计时器过期之前不会释放预订。
在更通常的情况下,你可以重申上述两个选项:
在执行命令之前验证命令,以确保命令成功。
锁定所有资源,直到命令完成。
如果命令只影响单个聚合,并且不需要引用聚合定义的一致性边界之外的任何内容,那么就没有问题,因为验证命令所需的所有信息都在聚合中。目前的情况并非如此。如果您能在付款之前验证座位是否仍然可用,那么这个信息将需要检查当前汇总之外的信息。
如果选择验证命令,您需要查看聚合之外的数据,例如,通过查询读模型或查看缓存,系统的可伸缩性将受到负面影响。另外,如果您正在查询一个读模型,请记住读模型最终是一致的。在当前场景中,您需要查询最终一致的读模型来检查座位的可用性。
如果您决定在命令完成之前锁定所有相关资源,请注意这将对系统的可伸缩性造成的影响。
从业务角度处理这样的问题要比在系统上设置大型架构约束好得多。
-- Greg Young
有关这个问题的详细讨论,请参阅Q/A Greg Young's Blog。
事件源基础设施的初始实现是非常基本的:团队打算在不久的将来用产品质量的事件存储来替换它。本节描述了初始的、基本的实现,并列出了改进它的各种方法。
这个基本事件源解决方案的核心要素是:
每当聚合实例的状态发生更改时,实例将引发一个事件,该事件将完整地描述状态更改。
系统将这些事件保存在事件存储中。
聚合可以通过重播其过去的事件流来重建状态。
其他聚合和流程管理器(可能在不同的限界上下文中)可以订阅这些事件。
当聚合状态发生更改时引发事件订单(Order)聚合中的以下两个方法是OrderCommandHandler类在接收订单命令时调用的方法的示例。这两种方法都不会更新订单(Order)聚合的状态。相反,它们引发一个事件,该事件将由订单(Order)聚合处理。在MarkAsReserved方法中,有一些最小的逻辑来确定要引发哪两个事件。
public void MarkAsReserved(DateTime expirationDate, IEnumerable<SeatQuantity> reservedSeats) { if (this.isConfirmed) throw new InvalidOperationException("Cannot modify a confirmed order."); var reserved = reservedSeats.ToList(); // Is there an order item which didn't get an exact reservation? if (this.seats.Any(item => !reserved.Any(seat => seat.SeatType == item.SeatType && seat.Quantity == item.Quantity))) { this.Update(new OrderPartiallyReserved { ReservationExpiration = expirationDate, Seats = reserved.ToArray() }); } else { this.Update(new OrderReservationCompleted { ReservationExpiration = expirationDate, Seats = reserved.ToArray() }); } } public void ConfirmPayment() { this.Update(new OrderPaymentConfirmed()); }