如何设计一款大型Laravel应用程序的架构(上)?

2020 年 6 月 02 日

如何设计一款大型Laravel应用程序的架构(上)?

本文最初发布于 stitcher 博客,经原作者授权由 InfoQ 中文站编译并分享。

首先来看场景概况。这是我们曾经做过的一个比较大的项目。一旦项目完成,它将为数以十万计的用户提供服务,处理大量财务交易,并且需要即时创建独立的租户专属安装。该项目的一个关键需求是需要轻松地报告和追踪产品订购流程的历史记录,这一流程也是业务的核心。

除了这个面向前端的客户流程外,还会有一个复杂的管理面板来管理产品。这里几乎完全不需要报告或追踪管理活动的历史记录。主要目标是提供一个易于使用的产品管理系统。

希望你能理解我故意使用模糊术语的做法,因为这显然不是一个开源项目。不过,我认为“产品管理”和“订单”的概念足以让你了解我们所做的是怎样的设计决策了。

我们首先来讨论这个系统的一种设计方法,这种方法出自我之前写过的《超越CRUD 的Laravel 》文章系列。

在这样的系统中可能会有两个域组:Product 和Order,以及两个同时使用这些域的应用程序: AdminApplicationCustomerApplication

简化版本如下所示:

在之前的项目中,我们成功应用了这一架构,所以这次也可以直接使用它了事。但它也有一些缺点,特别是对于这个新项目而言,缺陷很突出:我们必须牢记,报告和历史记录跟踪是订购流程的关键之处。我们希望能在代码中体现这一点,而不是简单地加个副效果就行。

例如:我们可以使用活动日志包来跟踪订单过程中出现的“历史消息”。我们还能在订单和历史记录表上编写自定义查询以生成报告。

但是,这些解决方案只能成为核心业务的简单的副效果,否则就没法正常工作。可是我们的情况下并没有这个条件。因此,Freek 和我的任务就是为这个项目设计一个方案,让报告和历史记录跟踪成为应用程序中易于维护和易于使用的核心部分。

很自然地,我们将目光投向了事件溯源,这是一种可以满足上述要求的优秀而灵活的解决方案。但没有什么是免费的午餐:事件溯源需要编写很多额外代码才能完成其他方法下很简单的事情。在有些地方,你过去只需要简单的 CRUD 操作处理数据库中的数据即可,可现在你不得不操心事件调度,还要用 projector 和 reactor 处理事件,同时还要一直关注版本控制。

很明显,事件溯源系统能解决许多问题;但即使在一些没有任何收益的地方,它也会带来很多开销。

这就是我的意思:如果我们决定事件溯源 Orders 模块(它依赖于 Products 模块中的数据),那么我们还需要事件溯源后者,否则的话我们可能会撞上无效状态。如果 Products 没有事件溯源,并且已被删除,则我们将无法重建 Orders 状态,因为它缺少信息。

因此,要么我们事件溯源一切内容,要么就设法解决这个矛盾。

需要事件溯源一切内容吗?

过去,在一些业余项目中使用事件溯源的经历让我们痛苦地意识到,我们不应该低估它所增加的复杂性。此外,Greg Young 表示事件溯源整个系统往往不是一个好主意——他对人们关于事件溯源的误解有完整的论述,值得一看!

很显然,我们不想事件溯源整个应用程序。这样做根本没有任何意义。唯一的选择是找到一种将有状态系统与事件溯源系统结合在一起的方法,但可惜在这个主题上我们发现没什么资源可用。

不管怎样,我们还是进行了一些费工费力的研究,并设法找到了问题的答案。但这个答案不是来自事件溯源社区,而是来自一种完善的DDD 实践:限界上下文。

如果我们希望Products 模块成为一个独立的,有状态的系统,则必须清晰地尊重Products 和Orders 之间的界限。我们不能把这两个模块视为一个单体应用程序,而必须将它们视为两个单独的上下文——也就是单独的服务,两者之间通信时必须确保Order 上下文永远不会进入无效状态。

如果构建的Order 上下文不直接依赖于Product 上下文,那么这个Product 上下文是怎么构建的就无关紧要了。

在与Freek 讨论时,我提到:将Products 视为可通过一个REST API 访问的独立服务。当API 下线或改动了自己的数据结构时,我们如何保证事件溯源应用程序仍然可以正常工作呢。

显然,我们实际上并不会构建在服务之间通信的API,因为它们将位于同一服务器上的同一代码库中。但在设计系统时有这样的思考还是很好的。

边界是下面这个样子,其中每个服务都有自己的内部设计。

如果你读过了我的《超越CRUD 的Laravel》系列文章,那么肯定已经熟悉了Product 上下文的工作机制。那边就没什么新东西要讲了。不过Order 上下文可以多讲一些背景信息。

事件溯源一部分内容

下面我们看一下事件溯源的部分内容。我假设你之所以会阅读这篇文章,至少是因为你对事件溯源感兴趣,所以我不会详细解释所有内容。

OrderAggregateRoot将跟踪在这一上下文中发生的所有事件,并将成为与应用程序对话的入口点。它还将调度事件,事件被存储并传播到所有 reactor 和 projector。

Reactor 将处理副效果,而这些副效果将永远不会重放,并且 projector 将进行投影。在我们的项目中,这些都是简单的 Laravel 模型。尽管只能从 projector 内部写入这些模型,但可以从其他任何上下文中读取它们。

我们在这里做出的一个设计决策是不拆分读写模型,因为现在我们使用了口头和书面约定,要求这些模型只能通过它们的 projector 写入。这种投影模型的一个例子就是一个 Order。

要记住的一条最重要规则是,只能从 Order 存储的事件中重建 Order 上下文的整个状态。

那么我们如何从其他上下文中提取数据呢?当与 Product 相关的上下文中发生某些事情时,如何通知 Order 上下文?可以肯定的是:与 Products 有关的所有信息将需要作为事件存储在 Order 上下文中;因为在这一上下文中,事件是唯一的真实来源。

为了做到这一点,我们引入第三种事件监听器。我们已经有了 projectors 和 reactors;现在我们添加订阅者(subscribers)的概念。允许这些订阅者侦听来自其他上下文的事件,并在其当前上下文中进行相应的处理。看起来,它们几乎总是将外部事件转换为内部存储事件。

从事件存储在 Order 上下文中的那一刻起,我们就可以放心地忘记对 Product 上下文的任何依赖。

有些读者可能认为我们会通过在这两个上下文之间复制事件来复制数据。当然,我们将基于 Product 的created时间存储特定于 Orders 的事件,因此的确会复制一些数据。但是,这样做带来的好处比你想象的更多。

首先:Product 上下文不需要知道其他哪些上下文将使用它的数据。它不必考虑事件版本控制,因为它的事件永远都不会被存储。这样我们就可以在处理 Product 上下文时将它视为正常的有状态应用程序,无需加入事件溯源,也就避免其复杂性。

第二:会被事件溯源的不只是 Order 上下文,而且所有这些上下文都能单独侦听 Product 上下文中触发的相关事件。

第三:我们不必存储原始 Product 事件的完整副本,因为每个上下文都可以挑选和存储与自己用例相关的数据。

数据迁移问题呢?

一个新问题出现了。

假设这套系统已经投入生产一年之久,我们决定添加一个新的事件溯源上下文;它还需要有关 Product 上下文的信息。由于上面列出的原因,原始的 Product 事件没有被存储下来——那么我们如何为新的上下文建立初始状态?

答案是这样的:在部署时,我们必须读取所有产品数据,并根据现有产品将相关事件发送到新添加的上下文中。这种一次性迁移的麻烦是额外的成本,但它让我们可以自由地处理 Product 上下文,而不必担心外部环境。对于这个项目,这是值得付出的代价。

最终整合

最后,通过使用只读模型,我们就能使用从所有上下文收集的应用程序数据。到目前为止,我们的约定依旧要求这些模型是只读的;而将来可能会改变这个约定。

从应用程序到 Product 上下文,就像普通的有状态应用程序一样通信即可。应用程序和事件溯源的上下文(例如 Orders)之间是通过其聚合根通信的。

下面是最终成品的概述。这张图中还缺少一些箭头,但是上下文和应用程序之间与它们内部的相关流程画的应该足够清楚了。

解决我们问题的关键来自 DDD 的限界上下文思想。它们描述了我们代码库中的严格界限,我们不能随意跨越这些界限。当然这增加了一层复杂性,但它还让我们能够自由地以期望的方式构建每个上下文,而不必操心支持其他上下文的问题。

最后一个难题是仅依靠事件作为上下文之间的交流手段。它又增加了一层复杂性,但同时也是一种解耦和增加灵活性的方式。

第二部分则是讲述我们如何在一个 Laravel 项目中编写具体的代码。

英文原文:

Combining event sourcing and stateful systems

2020 年 6 月 02 日 14:27 748
用户头像

发布了 444 篇内容, 共 164.6 次阅读, 收获喜欢 906 次。

关注

评论

发布
暂无评论
发现更多内容

第九周总结

andy

架构师训练营第九章作业

叮叮董董

尚未到来的远程工作

ThoughtWorks洞见

敏捷 敏捷开发 软件开发 远程办公 thoughtworks

架构师训练营第九章总结

叮叮董董

Elasticsearch从入门到放弃:瞎说Mapping

Jackey

elasticsearch

《RabbitMQ》如何保证消息的可靠性

Java旅途

面经手册 · 第2篇《数据结构,HashCode为什么使用31作为乘数?》

小傅哥

数据结构 java hashcode 小傅哥 面试官

秒杀系统的挑战和应对方案

2流程序员

一周信创舆情观察(7.27~8.2)

统小信uos

架构师训练营第9周

大丁💸💵💴💶🚀🐟

微服务架构下的核心话题 (一):微服务架构下各类项目的顺势崛起

xcbeyond

架构 微服务

Atlassian 重磅推出12个新功能为您打造全新 DevOps 体验!

Atlassian速递

项目管理 DevOps Atlassian Jira

第9周总结+作业

林毋梦

计算机网络基础(十四)---传输层-UDP协议详解

书旅

计算机网络 网络 协议栈 通信协议

SpreadJS 纯前端表格控件应用案例:SPDQD 质量数据云

Geek_Willie

SpreadJS 案例

架构师训练营 - 第九周 - 作业

韩挺

并发-草稿

superman

Ubuntu启动盘无法格式化

kraken0

NOSQL or NEWSQL

大唐小生

sql nosql

当面试官问我ArrayList和LinkedList哪个更占空间时,我这么答让他眼前一亮

鄙人薛某

Java 面试 集合 面试题 java基础

秒杀系统设计初稿

jason

HomeWork

天之彼方

我还在生产玩 JDK7,JDK 15 却要来了!|新特性尝鲜

楼下小黑哥

Java jdk

JVM学习总结

jason

关于微信电子发票生态,这三种服务商最有机会

诸葛小猿

电子发票 发票

树莓派上安装docker记录

田振宇

LeetCode题解:189. 旋转数组,3次翻转,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

【第九周作业】

Aldaron

Docker-compose实战

北漂码农有话说

JVM系列之:从汇编角度分析Volatile

程序那些事

Java JVM JIT 汇编

训练一个数据不够多的数据集是什么体验?

华为云开发者社区

数据 数据集 华为云 标签 modelarts

云原生来袭,企业上云如何平滑迁移增效避险?

云原生来袭,企业上云如何平滑迁移增效避险?

如何设计一款大型Laravel应用程序的架构(上)?-InfoQ