2.2 微服务的设计
2.1节介绍了领域驱动设计的基本概念与主要过程,重点讲解了在战略设计中扮演重要角色的限界上下文。在微服务架构中,限界上下文是沟通领域驱动设计与微服务之间的桥梁。要掌握如何设计微服务,就需要先了解限界上下文对边界的定义。
2.2.1 限界上下文的边界
在划分限界上下文时,限界上下文之间是否为进程边界隔离这一决策直接影响架构设计。之所以将“进程”作为限界上下文边界划分的标志,是因为进程内与进程间在如下方面存在迥然不同的处理方式:
● 通信;
● 消息的序列化;
● 资源管理;
● 事务与一致性处理;
● 部署。
除此之外,通信边界的不同还影响了系统对各个组件(服务)的重用方式与共享方式。
1.进程内的通信边界
若限界上下文之间为进程内的通信方式,则意味着在运行时它们的代码模型都运行在同一个进程中,可以通过实例化的方式重用领域模型或其他层次的对象。即使都属于进程内通信,限界上下文的代码模型(Code Model)仍然存在两种级别的设计方式。以Java为例,归纳如下。
● 命名空间级别:通过命名空间进行界定,所有的限界上下文其实都处于同一个模块(module)中,编译后生成一个JAR包。
● 模块级别:在命名空间上是逻辑分离的,不同限界上下文属于同一个项目的不同模块,编译后生成各自的JAR包。
这两种级别的代码模型仅存在编译期的差异,后者的解耦会更加彻底,倘若限界上下文的划分足够合理,也能提高它们对变化的应对能力。例如,当限界上下文A的业务场景发生变更时,我们可以只修改和重编译限界上下文A对应的JAR包,其余JAR包并不会受到影响。由于它们都运行在同一个Java虚拟机中,意味着当变化发生时,整个系统需要重新启动和运行。
即使处于同一个进程的边界,我们仍需重视代码模型的边界划分,因为这种边界隔离有助于整个系统代码结构变得更加清晰。限界上下文之间若采用进程内通信,则彼此之间的协作会更容易、更高效。然而,正所谓越容易重用,就越容易产生耦合。编写代码时,我们需要谨守这条无形的逻辑边界,时刻注意不要逾界,并确定限界上下文各自对外公开的接口,避免它们之间产生过多的依赖。此时,防腐层(ACL)就成了抵御外部限界上下文变化的最佳场所。一旦系统架构需要将限界上下文调整为进程间的通信边界,这种“各自为政”的设计与实现能够更好地适应这种演进。
采用进程内通信的系统架构属于单体(Monolithic)架构,所有限界上下文部署在同一个进程中,因此不能针对某一个限界上下文进行水平伸缩。当我们需要对限界上下文的实现进行替换或升级时,也会影响整个系统。即使我们守住了代码模型的边界,但耦合仍然存在,导致各个限界上下文的开发互相影响,团队之间的协调成本也随之而增加。
2.进程间的通信边界
倘若限界上下文之间的通信是跨进程的,则意味着限界上下文以进程为边界。此时,一个限界上下文就不能直接调用另一个限界上下文的方法,而是要通过分布式的通信方式。
当我们将一个限界上下文限定在一个独立的进程边界内时,并不足以决定领域驱动架构的设计质量。我们还需要将这个边界的外延扩大,考虑限界上下文需要访问的外部资源。这样就产生了两种不同风格的架构:
● 数据库共享架构;
● 零共享架构。
数据库共享架构
数据库共享架构其实是一种折中的手段。在考虑限界上下文划分时,分开考虑代码模型与数据库模型,就可能出现代码的运行是进程分离的,数据库却共享彼此的数据,即多个限界上下文共享同一个数据库。由于没有分库,在数据库层面就可以更好地保证事务的ACID。这或许是该方案最有说服力的证据,但也可以视为是对“一致性”约束的妥协。
数据库共享的问题在于数据库的变化方向与业务的变化方向并不一致。这种不一致性体现在两方面。
● 耦合:虽然限界上下文的代码模型是解耦的,但在数据库层面依然存在强耦合关系。
● 水平伸缩:部署在应用服务器的应用服务可以根据限界上下文的边界单独进行水平伸缩,但在数据库层面却无法做到。
根据Netflix团队提出的微服务架构最佳实践,其中一个最重要特征就是“每个微服务的数据单独存储”。但是服务的分离并不绝对代表数据应该分离。数据库的样式(Schema)与领域模型未必存在一对一的映射关系。在对数据进行分库设计时,如果仅站在业务边界的角度去思考,可能会因为分库的粒度太小,导致不必要的跨库关联。因此,我们可以将“数据库共享”模式视为一种过渡方案。如果没有想清楚微服务的边界,就不要在一开始设计微服务时直接将数据彻底分开,而应采用演进式的设计。
为了便于在演进设计中将分表重构为分库,从一开始要注意避免在分属两个限界上下文的表之间建立外键约束关系。某些关系型数据库可能通过这种约束关系提供级联更新与删除的功能,这种功能反过来会影响代码的实现。一旦因为分库而去掉表之间的外键约束关系,需要修改的代码太多,则会导致演进的成本太高,甚至可能因为某种疏漏带来隐藏的Bug。
零共享架构
当我们将两个限界上下文共享的外部资源彻底斩断后,就成为了零共享架构。在如图2-11所示的舆情分析系统中,危机分析与用户管理之间不存在任何资源之间的共享。
图2-11
这是一种限界上下文彻底独立的架构风格,它保证了边界内的服务、基础设施乃至于存储资源、中间件等其他外部资源的完整性与独立性,最终形成自治的微服务。这种架构的表现形式为:每个限界上下文都有自己的代码库、数据存储及开发团队,每个限界上下文选择的技术栈和语言平台也可以不同,限界上下文之间仅通过限定的通信协议和数据格式进行通信。
2.2.2 限界上下文即微服务
领域驱动设计中的限界上下文与微服务并非充分必要条件。限界上下文不一定就是微服务,而微服务的设计与实现也未必需要通过限界上下文来界定。但是,领域驱动设计确乎可以作为微服务设计的重要补充,是识别微服务的行之有效的设计手段。一旦我们通过业务边界、工作边界和应用边界识别了限界上下文,就可以将其作为微服务的候选。显然,如果限界上下文之间采用了进程间通信,就可以认为一个限界上下文就是一个微服务。
我们可以参考Martin Fowler给出的微服务架构,如图2-12所示。
图2-12
图2-12综合展现了分层架构、六边形架构、限界上下文与上下文映射在微服务架构中的一种融合。例如Data Mappers就属于基础设施层,Domain与Repositories属于分层架构中的领域层,而Service Layer则属于应用层。图中的Gateways与HTTP Client实现了与外部服务之间的通信,相当于六边形架构中的端口与适配器,同时又运用了上下文映射的防腐层模式。Resource暴露对外提供的服务接口,同样属于六边形架构的端口与适配器,并运用了上下文映射的开放主机服务模式。图中的网络边界(Network Boundary)相当于六边形架构的外部边界,实则就是一个微服务的物理边界。但微服务不止于此,它的逻辑边界还包括了一个外部数据库,这是基于微服务的设计原则——“每个微服务的数据单独存储”,因此需要将物理边界外的数据库放在微服务的内部。这样的设计是符合零共享架构特征的,保证了每个微服务对资源的独占,以及它的独立扩展能力。
现在,我们可以将限界上下文、六边形架构与微服务三者结合起来:
● 一个限界上下文就是一个六边形,限界上下文之间的通信通过六边形的端口进行。
● 一个微服务就是一个限界上下文,微服务之间的协作就是限界上下文之间的协作。
图2-13将这三者各自的设计原则与思想融合在了一起。
图2-13
在确定了限界上下文的物理边界之后,我们就可以建立限界上下文、六边形架构与微服务的“三位一体”关系。
● 限界上下文即微服务:我们可以利用领域驱动设计对限界上下文的定义,以及前述识别限界上下文的方法来设计微服务。
● 微服务即限界上下文:运用微服务设计原则,可以进一步甄别限界上下文的边界是否合理,对限界上下文进行进一步的演化。
● 微服务即六边形:深刻体会微服务的“零共享架构”,并通过六边形架构来表达微服务。
● 限界上下文即六边形:运用上下文映射来进一步探索六边形架构的端口与适配器角色。
● 六边形即限界上下文:通过六边形架构的端口确定限界上下文之间的集成关系。
2.2.3 识别限界上下文
不少领域驱动设计的专家都非常重视限界上下文。Mike在文章DDD:The Bounded Context Explained中写道:“限界上下文是领域驱动设计中最难解释的原则,但或许也是最重要的原则。可以说,没有限界上下文,就不能做领域驱动设计。在了解聚合根(Aggregate Root)、聚合(Aggregate)、实体(Entity)等概念之前,需要先了解限界上下文。”当我们将限界上下文与微服务结合起来时,限界上下文的重要性就更加凸显了。
那么,有没有什么方法可以快速准确地帮助我们去识别限界上下文呢?目前业界较为流行的做法是采用Alberto Brandolini提出的事件风暴方法(Event Storming),该方法以事件作为核心来帮助我们分析领域模型,进而驱动出我们想要获得的限界上下文。而我则结合自己的项目实践经验,引入用例场景分析来帮助团队剖析业务场景,驱动业务架构的设计。殊途同归,无论采用什么样的方法,其宗旨都是强调团队与领域专家的合作,通过深入的交流与协作,并采取工作坊的形式共同梳理和识别业务场景。在设计方面,则秉承了“高内聚低耦合”的设计原则,通过梳理业务相关性与功能相关性最终确定限界上下文的边界。
1.事件风暴
个人认为,相比较传统领域分析方法,事件风暴的革命意义在于它建立了以“领域事件”为核心的建模思路,这相当于改变了我们观察业务领域的世界观。当我们在理解业务需求时,我们看到的常常是功能、流程,并通过从需求描述中梳理领域概念,进而借助这些概念去识别那些参与到业务场景中互为协作的领域对象,这往往让我们忽略了一个在任何领域中都必须存在的概念,即“事件”。这些事件是每次用户操作、业务活动留下来的不可磨灭的足迹,它牵涉状态的迁移,业务事实的发生,忠实地记录了每次执行命令后可能产生的结果。倘若这些事件还直接影响该领域的运营和管理时,则可以将它们认为是“关键事件”。
正如Martin Fowler对领域事件的定义:“重要的事件肯定会在系统其他地方引起反应,因此理解为什么会有这些反应同样重要。”在识别和理解事件时,正是要从这样的因果关系着手,考虑为什么要产生这一事件,以及为什么要响应这一事件,进而思考如何响应这个事件,驱动着设计者的“心流”不断思考下去,就像搅动了一场激荡湍急的风暴一般。这或许是Alberto Brandolini将其命名为事件风暴的缘由吧。
在事件风暴中,往往使用橙色标签来代表一个“关键事件”。由于事件代表的是一个已经发生的事实(Fact),所以往往用动词的过去时态来表达,例如OrderConfirmed事件。
在识别“事件”时,团队应与业务人员一起通过梳理业务流程,在统一语言的指导下共同寻找这些可能直接影响业务价值与运营目的的“关键事件”。在一个业务场景中,一系列“关键事件”连接起来,会形成明显的基于一条时间线的状态迁移过程,如图2-14所示。
图2-14
这种状态迁移过程体现了业务的因果关系。这种因果关系是一种不断传递的过程,导致事件发生的因,在事件风暴中被称为命令(Command),相当于事件的发布者,在事件风暴中使用蓝色标签来表示。一旦事件发生,作为该命令的结果又可能引起别的业务反应,事件的订阅者关心这一结果,然后触发新的命令,变成了下一个流程的起因。命令往往由动宾短语组成,例如Place Order、Send Invitation等。
注意,在识别事件时,要注意区分触发事件的四种情形。
● 由用户活动触发:例如用户将商品加入购物车。
● 外部系统:支付系统返回交易凭证。
● 时间消逝导致:订单的支付时间超时。
● 另一个领域事件的结果:支付命令产生支付完成事件(PaymentProcessed),该事件导致订单完成事件(OrderCompleted)。
事件由命令触发,那么谁又是命令的发起者呢?答案是参与者(Actor)。参与者的引入将对事件的分析与业务场景结合起来,驱动参与事件风暴的所有成员要对业务达成一致(形成统一语言),并从用户体验(User Experience)的角度去分析每个业务场景。这时作为参与者对业务的参与,就不再是发起一个业务流程,执行一个业务动作,而是做出决策(Decision)。在事件风暴中,决策就是命令,但“决策”更具有拟人化的意义,正如在现实生活中,当一个管理者要做出决策时,需要如下两方面数据的支撑。
● 信息:必须基于足够充分的信息才能做出正确的决策,提供这些信息的对象就称为读模型(Read Model),在事件风暴中用绿色标签表示。
● 策略:一旦做出决策就会触发一个业务流程,流程的执行暗含了业务规则,该规则被命名为策略(Policy),在事件风暴中用紫色标签表示。
描述策略时,往往可以使用“一旦(Whenever)”这个关键字来引导对策略规则的描述。策略引发的决策可以是自动的,也可以是参与者人为触发的。Alberto Brandolini给出了描述策略的实例,例如:
● whenever the exposure passes the given threshold, we need to notify the risk manager,一旦关注的值超出给定的阈值,我们就需要通知风险管理者。
● whenever a user logs in from an new device, we send him an SMS warning,一旦用户从一个新设备中登录,我们就应该给用户发送一条短信警告。
在运用事件风暴时,我们可以通过用户体验(例如用户旅程等UX方法)剖析业务场景,从参与者到命令再到事件,又可以以表达状态迁移的事件为核心,将策略与读模型组合在一起帮助我们推导出命令对象。Alberto Brandolini通过图2-15整体描述了事件风暴的驱动过程。
图2-15
一旦我们识别了事件和对应的命令,我们就可以根据这些对象的生命周期与职责内聚性识别出聚合(Aggregate)与聚合根。聚合在事件风暴中使用黄色标签来表示。聚合是命令的真正发起者,这是相对于前面提到的参与者而言的。在问题域中,由参与者(用户、系统或其他特殊组件,如定时器)发起命令来“开启”一个业务流程。但在解决方案域,我们是从职责的角度去看待命令的,这就需要在领域模型中去寻找履行该职责的对象,即聚合。例如,在电商系统的业务流程中,问题域表达的是“买家购买了商品”,对应的解决方案域则是“购物车添加了购物项”,因此分析获得ShoppingCart这个聚合对象。
一旦获得了这些内聚的聚合,就可以根据各自的相关性对聚合进行分组,从而获得限界上下文。在获得限界上下文的过程中,可以从业务、团队合作与技术实现等诸多方面进行判定。由于限界上下文属于解决方案域的内容,在初步获得限界上下文之后,团队就可以考虑这些限界上下文的技术实现。尤其是在微服务架构下,需要针对微服务特征来确定限界上下文的粒度与边界是否合理。此时,我们可以引入上下文映射,通过识别限界上下文之间的协作关系进一步确认它的合理性。
2.用例场景分析
用例场景分析的核心思想是通过“场景”来展现领域逻辑的。领域专家或业务分析师从领域中提炼出“场景”,就好像是从抽象的三维球体中切割出具体可见的一片一样。然后以这一片场景为舞台,上演各种角色之间的悲欢离合。每个角色的行为皆在业务流程的指引下展开活动,并受到业务规则的约束。当我们在描述场景时,就好像在讲故事,又好似在拍电影。
在场景分析时,表现问题域的是用户角色(Who)、业务价值(Why)与业务功能(What),为问题域划定解决方案域,就是要从这些场景中确定业务边界(Where),这个边界就是我们要识别的限界上下文。恰好,Ivar Jacobson提出的“用例”正好满足了场景分析的这几个关键要素。通过用例,可以帮助我们思考参与系统活动的角色,即用例中所谓的“参与者(Actor)”,然后通过参与者的角度去思考为其提供“价值”的业务功能。
UML引入用例图来表示用例,通过可视化的方式表示参与者与用例之间的交互,用例与用例之间的关系及系统的边界。组成一个用例图的要素如下。
● 参与者(Actor):代表了6W模型的Who。
● 用例(Use Case):代表了6W模型的What。
● 用例关系:包括使用、包含、扩展、泛化、特化等关系,其中使用(use)关系代表了Why。
● 边界(Boundary):代表了6W模型的Where。
与事件风暴不同,使用用例场景分析将围绕“用例”展开领域分析。一个用例就是一个具有业务价值的业务功能。用例场景分析的步骤如下:
● 确定业务流程,通过业务流程识别参与者(Actor);
● 根据每个参与者识别属于该参与者的用例,遵循一个参与者一张用例图的原则,保证用例图的直观与清晰;
● 对识别出来的用例根据语义相关性和功能相关性进行分类,确定用例的主题边界,并对每个主题进行命名。
一个典型的用例图如图2-16所示。
用例的识别是从参与者开始的,只有那些为参与者提供了业务价值的业务行为才是我们要识别的主用例。在图2-16中,只有place order用例与buyer参与者之间才存在使用(use)关系。这是因为只有“下订单”用例对于买家而言才具有业务价值,也是买家“参与”该业务场景的主要目的。因此,我们可以将该用例视为体现这个领域场景的主用例,其他用例则是与该主用例产生协作关系的子用例。
图2-16
当我们通过参与者来识别用例时,为了保证用例场景分析思路的清晰与正确,并且避免缺失一些重要的主用例,我的一个实践是让一个参与者对应一个用例图。为了团队成员更好地互动与交流,绘制用例图不必要严格遵循UML用例图的形式,借鉴事件风暴,可以让黄色标签代表参与者,蓝色标签代表一个用例,毕竟从某种程度上讲,所谓“用例”其实与事件风暴中的“命令”并没有太大的区别。例如通过交流,团队可以获得图2-17所示的另一种格式的用例图。
图2-17
这样的用例图是领域专家与开发团队进行沟通的一种可视化手段,简单形象,还可以避免从一开始就陷入技术细节中——用例的关注点就是领域。绘制用例图时,切忌闭门造车,最好让团队一起协作。用例表达的领域概念必须精准!在为每个用例进行命名时,我们都应该采纳统一语言中的概念,然后以言简意赅的动宾短语描述用例,并提供英文表达。很多时候,在团队内部已经形成了中文概念的固有印象,一旦翻译成英文,就可能呈现百花齐放的面貌,这就破坏了“统一语言”。为保证用例描述的精准性,可以考虑引入“局外人”对用例提问。局外人不了解业务,任何领域概念对他而言可能都是陌生的。通过不断对用例表达的概念进行提问,团队成员就会在不断地阐释中形成更加清晰的术语定义,对领域行为的认识也会更加精确。
除了参与者与用例之间的使用关系,用例之间主要的协作关系为:
● 包含(include);
● 扩展(extend)。
如何理解包含与扩展之间的区别?大体而言,“包含”关系意味着子用例是主用例中不可缺少的一个执行步骤,如果缺少了该子用例,则主用例可能会变得不完整。“扩展”子用例是对主用例的一种补充或强化,即使没有该扩展用例,对主用例也不会产生直接影响,主用例自身仍然是完整的。倘若熟悉面向对象设计与分析方法,则可以将“包含”关系类比为对象之间的组合关系,如汽车与轮胎,是一种must have,而“扩展”关系就是对象之间的聚合关系,如汽车与车载音响,是一种nice to have。当然,在绘制用例图时,倘若实在无法分辨某个用例究竟是包含还是扩展,那就“跟着感觉走”吧,这种设计决策并非生死攸关的重大决定,即使辨别错误,几乎也不会影响最后的设计。
在用例场景分析过程中,识别包含与扩展关系仍然是值得的,因为它们代表了用例之间的功能相关性。无论包含还是扩展,这些子用例都是为主用例服务的,体现了用例规格描述的流程。
在识别了主要(所有)参与者的全部用例之后,我们就可以根据用例的语义相关性和功能相关性对这些用例进行分组,从而确定用例的主题边界(Subject Boundary)。确定用例相关性就是分析何谓内聚的职责,是根据关系的亲密程度来判断的。例如,在上面给出的用例图中,remove shopping cart items、notify buyer与validate inventory与place order用例的关系,远不如validate order等用例与place order之间的关系紧密。因此,我们将这些用例与order分开,分别放到shopping cart、notification与inventory中。
识别出来的主题边界可以认为是候选的限界上下文。接下来对限界上下文粒度与边界的甄别,与事件风暴的过程是完全一致的,仍然需要从业务、团队合作与技术实现等多个层次对限界上下文开展进一步的梳理,以保证限界上下文的合理性。
2.2.4 微服务之间的协作
当我们将限界上下文视为一个微服务时,确定微服务之间的协作关系将作为设计过程中重要的决策。我们可以引入领域驱动设计的上下文映射(Context Map),以及六边形架构的端口和适配器,共同来帮助我们梳理微服务之间的协作关系。
在辨别微服务之间的协作关系时,首先需要确定它们彼此之间是否存在关系,然后确定是何种关系,最后基于变化导致的影响来确定该引入何种上下文映射模式。倘若发现微服务的协作关系有不合理之处,则需要反思之前我们识别出来的限界上下文(即微服务)是否合理。确定微服务之间的关系不能想当然,需得全面考虑参与两个微服务协作的业务场景,然后在场景中识别二者之间产生依赖的原因,确定依赖的方向,进而确定集成点。
如果微服务之间存在协作关系,必然是某种原因导致的这种协作关系。从依赖的角度看,这种协作关系是因为一方需要“知道”另一方的知识。这种知识包括:
● 领域行为——需要判断导致行为之间的耦合原因是什么?如果是上下游关系,则要确定下游是否就是上游服务的真正调用者。
● 领域模型——需要重用别人的领域模型,还是自己重新定义一个模型。
● 数据——是否需要限界上下文对应的数据库提供支撑业务行为的操作数据。
仍然以电商系统为例,假设我们初步获得了如下六个限界上下文:
● Product Context;
● Basket Context;
● Order Context;
● Inventory Context;
● Payment Context;
● Notification Context。
结合购买流程,电商系统还需要用到第三方物流系统对商品进行配送。这个物流系统可以认为是电商系统的外部系统(External Service)。如果这六个限界上下文之间采用跨进程通信,那么实际上就是六个微服务,它们应该单独部署在不同节点之上。现在,我们需要站在微服务的角度对其进行思考。需要考虑的内容包括:
● 每个微服务是如何独立部署和运行的?如果我们从运维角度去思考微服务,就可以直观地理解所谓的“零共享架构”到底是什么含义。如果我们在规划系统的部署视图时,发现微服务之间在某些资源存在共用或纠缠不清的情况,就说明微服务的边界存在不合理之处,也就是之前识别限界上下文存在不妥。
● 微服务之间是如何协作的?这个问题牵涉通信机制的决策、同步或异步协作的选择,以及上游与下游服务的确定。我们可以结合上下文映射与六边形架构来思考这些问题。上下文映射帮助我们确定这种协作模式,并在确定了上下游关系后,通过六边形架构来定义端口。
现在我们可以将六边形架构与限界上下文结合起来,即通过端口确定限界上下文之间的协作关系绘制上下文映射。如果采用客户方——供应商开发模式,则各个限界上下文六边形的端口就是上游(Upstream,简称U)与下游(Downstream,简称D),如图2-18所示。
图2-18
由于这些限界上下文都是独立部署的微服务,因此,它们的上游端口应实现为OHS模式,图中以U表示;下游端口应实现为ACL模式,图中以D表示。
每个微服务都是一个独立的应用,我们可以针对每个微服务规划自己的分层架构,进而确定微服务内的领域建模方式。微服务之间可以通过命令、查询或事件进行协作。如果采用命令与查询方式,则提供命令或查询功能的为上游服务,发出执行请求的为下游服务。在图2-18中,以菱形端口代表“命令”,矩形端口代表“查询”。这样就能直观地通过上下文映射及六边形的端口清晰地表达微服务的定义,以及服务之间的协作方式。例如,Product Context同时作为Basket Context与Order Context的上游限界上下文,其查询端口提供的是商品查询服务。Basket Context作为Order Context的上游限界上下文,其命令端口提供了清除购物篮的命令服务。
如果微服务的协作采用事件机制,则上下文映射中的上下游语义就会发生变化,原来作为“命令”或“查询”提供者的上游,成为“事件”机制下的订阅者。以购物篮为例,“清除购物篮”命令服务被定义在Basket Context中。当提交订单成功后,Order Context就会发起对该服务的调用。倘若将“提交订单”视为一个内部命令(Command),在订单被提交成功后,就会触发OrderConfirmed事件,此时,Order Context反而成为该事件的发布者,Basket Context则会订阅该事件,一旦侦听到该事件触发,就会在Basket Context内部执行“清除购物篮”命令。显然,“清除购物篮”不再作为服务发布,而是在事件的handler中作为内部功能被调用。
采用“事件”协作机制会改变我们习惯的顺序式服务调用形式,整个调用链会随着事件的发布而产生跳转,尤其是暴露在六边形端口的“关键事件”,更是会产生跨六边形(即限界上下文)的协作。仍以电商系统的购买流程为例,我们只考虑正常流程。在Basket Context中,一旦购物篮中的商品准备就绪,买家就会请求下订单,此时开始了事件流:
① Basket Context发布OrderRequested事件。Order Context订阅该事件,然后执行提交订单的流程。
② Order Context验证订单,并发布InventoryRequested事件,要求验证订单中购买商品的数量是否满足库存要求。
③ Inventory Context订阅此事件并对商品库存进行检查。倘若检查通过,则发布AvailabilityValidated事件。
④ Order Context侦听到AvailabilityValidated事件后,验证通过,发布OrderValidated事件从而发起支付流程。
⑤ Payment Context响应OrderValidated事件,在支付成功后发布PaymentProcessed事件。
⑥ Order Context订阅PaymentProcessed事件,确认支付完成进而发布OrderConfirmed事件。
⑦ Basket Context、Notification Context与Shipment Context上下文都将订阅该事件。Basket Context会清除购物篮,Notification Context会发起对买家和卖家的通知,而Shipment Context会发起配送流程,在交付商品给买家后,发布ShipmentDelivered事件并被Order Context订阅。
整个协作过程如图2-19所示,图中的序号对应事件流的编号。
与订单流程相关的事件包括:
● OrderRequested;
● InventoryRequested;
● AvailabilityValidated;
● OrderValidated;
● PaymentProcessed;
● OrderConfirmed;
● ShipmentDelivered。
图2-19
正如前面给出的事件驱动架构所示,事件的发布者负责触发输出事件(Outgoing Event),事件的订阅者负责处理输入事件(Incoming Event),它们作为六边形的事件适配器被定义在基础设施层。事件适配器的抽象则被定义在应用层。假设电商系统选择Kafka作为事件传递的通道,我们就可以为不同的事件类别定义不同的主题(Topic)。此时,Kafka相当于连接微服务之间进行协作的事件总线(Event Bus)。