在我的最近项目中,我旨在使用事件驱动架构重写一个混乱的代码基础。然而,我发现真正实现解耦比我想象的要难得多。我打算在我的博客上分享我的经历和学到的教训。
基于事件驱动架构的承诺我从一个服务开始,这个服务在一个文件中有超过2000行的代码。这个服务类负责更新多个组件中的多个实体,这些实体内部和外部都有复杂的条件和逻辑。一个组件中的变化可能会以不可预测的方式影响其他组件。当缺乏足够的测试代码来覆盖服务类的各种情况时,这会带来更多麻烦。
这里就是事件驱动模式的概念。这个模式的核心思想是明确地分离关注点,从而不必担心意外的变化。每个用例只需专注于更新一个实体并发布相应的事件。事件监听器监听感兴趣的事件,然后调用相应的用例来更新相关的实体。目标是创建一个松耦合的系统,使各个组件可以独立发展。
瑞典翻译成中文应直接反映标题意思,这里是符合语境的直译: 瑞典原文是英文,所以无需提及从瑞典语翻译,直接给出合适的中文翻译即可: 事情开始出问题了 双向交流面临的难题事件驱动的方法最初看起来很有前景,但很快就开始出问题了。事件驱动架构的一个主要限制是它不适用于双向通信。事件的发布和订阅本质上是单向的,即从A到B。与直接调用模式不同,其中A向B发送请求,B回应A,事件驱动系统没有内置机制让B能够回传消息给A。
这就会出现问题,当A需要根据B的结果采取行动时。一个常见的例子是订单服务(A)和支付服务(B)。当订单被提交时,A会向B发送OrderPlaced
事件。如果B在处理支付过程中遇到问题(例如,用户的信用卡被拒绝),B需要告知A出现问题,这样A可以根据情况采取适当的措施,比如通知用户更换另一张信用卡。
在事件驱动系统中,A向B发送OrderPlaced
事件并且B无法轻易地向A反馈,会让用户感到困惑。用户可能无法立即收到关于支付问题的反馈。
要解决这个问题,可以考虑给 B 单独设置一个端点,比如通过 webhook 向 A 传达支付结果。然而,这会增加系统的复杂性。如果 B 是外部服务,通过新的端点同步结果并不总是可行。
A和B之间的互动变得更乱了,A向B发送事件,B需要找到一种办法回复A。这破坏了事件驱动系统最初承诺的简洁和优雅。
交易一致性与最终一致性的挑战以事务性的方式来思考与以最终一致性的方式来思考是完全不同的。在最终一致性的系统里,服务A假设,在向服务B发布事件后,服务B最终会完成任务。实际上,这比听上去要复杂得多。
例如,比如说服务A(订单服务)和服务B(CRM,客户关系管理服务):
注:句子末尾已加上句号,使句子更加完整。但由于“比如说”和“服务”之间添加句号会导致句子结构混乱,故未在两者之间添加句号。
- 服务A负责订单处理,并在订单完成后向用户发送通知。
- 服务B负责管理会员奖励,这些奖励会与商品一起寄送给常客。
- 要求是服务A需要在订单完成的通知中提及已发出的会员奖励。
在事件驱动的系统中,服务A发送一个orderCompleted
事件给服务B,服务B在接收到这个事件后开始其任务。当服务A需要为通知获取会员奖励信息时,它必须向服务B询问与尚未发送的已完成订单相关的奖励信息。而在事务性场景中,当通知发生在完成订单并已发放奖励之后,服务A需要找到已经发送出去的奖励。
在思维上从最终的一致性转换到事务的一致性是很具挑战性的。我们得时刻记得这些实体之间的关系——它们是否是按顺序处理的,还是通过事件来处理的。
管理一系列事件的困难在事件驱动的架构中,事件订阅者可以确信事件发布者已经完成了其任务。当服务A向服务B发布事件时,服务B就可以确定引发该事件的服务A的任务已经完成。
管理事件链带来了一套新的挑战。在上图所示场景中,当服务D启动其事件监听器时,它能够确定与事件D和E相关的流程已经结束。通过传递性原则,它还可以假设触发事件A和B的用例场景也已完成。
然而,服务 D 无法确定与事件 C 及其相应监听器相关的流程在它开始执行时是否已完成。如果服务 D 需要作为整个事件链的最终环节,例如在它发送邮件通知用户服务 A、B 和 C 所有流程已完成的情况下,这就会成为一个问题。
一种解决方案是为事件分配优先级,确保优先级最低的事件最后被发布。虽然这种方法有助于管理顺序,但同时也,在事件之间创建了隐含的依赖关系。在向系统添加新事件时,需要考虑一些因素。
- 在这个事件链中,这个事件应该放在哪里?
- 事件链中哪些事件已经被分配了优先级?
要回答这些问题,这往往需要追踪整个事件链。这比在单一用例中排列服务顺序要难得多。
最后,到最后,
我决定不再尝试将一切都解耦并通过事件发布/订阅来管理。相反,我将大部分流程保持在一个单一的使用场景中,并将那些没有陷入上述问题的流程移到事件和事件监听器中。理解事件驱动模式的优缺点可以帮助我们做出更好的选择,因此在此总结了一张表格作为本文的重点。