创建可扩展且可维护的应用程序仍然是软件开发行业中持续存在的挑战。随着数字生态系统变得越来越复杂,用户需求迅速变化,开发人员面临着越来越大的压力,需要构建能够无缝适应和扩展的系统。

可扩展性提出了多方面的挑战。应用程序必须在不影响性能的情况下处理不断增长的用户群、不断增加的数据量和扩展的功能集。
这需要仔细的架构决策、高效的资源利用以及跨多个服务器或云实例有效分配工作负载的能力。

另一方面,可维护性关注代码库的长期健康状况。随着应用程序变得越来越复杂,保持代码简洁、易于理解和易于修改变得至关重要。
这涉及创建模块化设计、遵守编码标准以及实施稳健的测试策略。可维护的代码库可以更快地修复错误、更轻松地添加功能以及让新团队成员更顺利地入职。

可扩展性和可维护性的交叉常常导致权衡。为了可扩展性而高度优化的系统可能会牺牲代码的可读性,而为了可维护性而过度模块化的设计可能会带来性能开销。
实现正确的平衡需要深厚的领域知识、技术专长和前瞻性的软件设计方法。

技术变革的快速步伐又增加了一层复杂性。开发人员必须创建不仅满足当前需求,而且保持足够灵活性以融入未来技术和方法的系统。
这种前瞻性方法对于防止技术债务和确保应用程序的寿命至关重要。

技术变革的快速步伐又增加了一层复杂性。开发人员必须创建不仅满足当前需求,而且保持足够灵活性以融入未来技术和方法的系统。
这种前瞻性方法对于防止技术债务和确保应用程序的寿命至关重要。

领域驱动设计由 Eric Evans 首次提出,强调使软件设计与业务需求保持一致。它提供了一组模式和实践,帮助架构师和开发人员创建可以随着需求变化而发展的灵活的模块化系统。
通过将设计过程围绕核心领域和领域逻辑,DDD 有助于创建真正反映底层业务模型的软件。

领域驱动设计提供了一个框架,用于创建在功能方面可扩展且在代码组织方面可维护的系统。
它提供了管理复杂性、促进技术和非技术利益相关者之间的沟通以及创建可随着不断变化的业务需求而发展的灵活系统的策略。

领域驱动设计快速概述

领域驱动设计(DDD)是一种将项目的核心重点放在领域和领域逻辑上的软件开发方法。
由 Eric Evans 在他的开创性著作中提出,DDD 提供了一组用于创建复杂软件系统的原则和模式,这些系统密切反映了它们所服务的业务领域。

DDD 的核心是通用语言的概念。这种共享词汇由开发人员与领域专家合作精心制作,构成了模型的基础。
它确保所有利益相关者,从业务分析师到程序员,都可以使用与该领域直接相关的术语和概念进行有效沟通。这种一致性显着减少了误解,并有助于创建真正反映业务需求的软件。

有界上下文代表了 DDD 的另一个基石。这些是特定领域模型应用的明确边界。在大型系统中,不同的部分可能有不同的领域模型,每个模型都有自己的通用语言。
识别和定义这些边界有助于管理复杂性,并允许团队在系统的不同部分独立工作。

聚合是 DDD 中的关键战术模式。聚合是被视为数据更改的单个单元的域对象的集群。聚合根是聚合内的单个实体,确保聚合内更改的一致性并控制对其成员的访问。
这种模式对于定义清晰的事务边界和维护数据完整性特别有用。

领域事件是 DDD 中的另一个重要概念。这些代表了域内发生的重大事件。通过将重要的变化建模为事件,DDD 促进了系统不同部分之间的松散耦合,从而实现更灵活和可扩展的架构。

在微服务架构的背景下,DDD 被证明非常有价值。 DDD 中的有界上下文通常与各个微服务很好地结合在一起,提供了一种将复杂系统分解为可管理、可独立部署的服务的自然方法。
这种一致性有助于创建一个模块化系统,其中每个服务都有明确的职责和定义良好的接口。

DDD 的战略模式(例如上下文映射)有助于理解和定义不同有界上下文或微服务之间的关系。这对于管理分布式系统产生的复杂性并确保整个系统保持一致至关重要。

通过应用 DDD 原则,开发人员可以创建不仅符合业务需求而且本质上更易于维护和扩展的软件。对核心领域的关注确保了开发工作集中在提供最大价值的地方。
清晰的边界和明确定义的接口有助于随着时间的推移更轻松地对系统进行更改和扩展。

在以下部分中,我们将探讨如何应用这些 DDD 概念来使用双微服务架构创建类似 Twitter 的应用程序。
这个实际示例将演示 DDD 如何指导复杂系统的设计,从而产生灵活且可维护的解决方案。

定义有界上下文

将领域驱动设计 (DDD) 应用于类似 Twitter 的应用程序时,第一步是识别和定义有界上下文。有界上下文在 DDD 中至关重要,因为它们描绘了特定领域模型应用的边界。
对于我们简化的类似 Twitter 的平台,我们将重点关注两个主要的有界上下文,每个上下文对应一个微服务: UserManagementServiceMessagingService

用户管理服务有界上下文

UserManagementService 涵盖与用户帐户和配置文件相关的所有方面。该上下文负责管理用户身份、身份验证和用户特定信息。在这个有界上下文中,核心概念包括:

  • 用户 - 代表平台上个人帐户的中央实体,其中包含用户特定的详细信息,例如显示名称、个人简介和个人资料图片、用户名和密码等身份验证信息等。
  • 角色 - 定义用户在整个系统中的角色,无论他是消息生产者还是消息消费者

此上下文中的普遍语言包括“注册”、“登录”、“注销”、“身份验证”和“授权”等术语。 UserManagementService 独立运行,仅关注与用户相关的操作,而不直接关注消息传递或内容创建。

消息服务限界上下文

MessagingService 处理内容创建、分发和消费的各个方面。此上下文更加复杂,因为它管理消息(推文)的创建和订阅系统。此限界上下文中的关键概念包括:

  • 消息:代表一段内容的核心实体(类似于推文)。
  • 订阅:代表用户之间的关注关系。
  • Feed:来自订阅用户的消息的集合。
  • 生产者:创建内容(消息)的用户。
  • 消费者:消费订阅内容的用户。

这里的通用语言包括“发布消息”、“订阅”、“取消订阅”、“消费消息”等术语。此上下文处理平台的动态方面,管理用户之间的内容流。

 情境互动

虽然这些有界上下文是独立的,但它们确实是相互作用的。 MessagingService 需要引用用户,但它这样做并没有深入研究用户管理的内部复杂性。相反,它可能使用用户的简化表示,仅包含用于消息传送目的的必要信息。

Context Interactions 图 1. 有界上下文之间的上下文交互

例如,当用户发布消息时, MessagingService 不需要知道用户的密码或电子邮件地址。它只需要一个用户标识符,也许还需要一个显示名称。这种分离允许每个上下文独立发展,同时保持必要的连接。

通过实施每个服务数据库模式进一步加强了这种分离。在这种方法中,每个微服务都维护自己的专用数据库。 UserManagementService 有一个数据库,用于存储全面的用户信息,包括密码和电子邮件地址等敏感数据。另一方面, MessagingService 数据库侧重于消息内容、用户订阅及其操作所需的最小用户数据集。

为了维持服务之间必要的连接, MessagingService 保留其操作所需的用户数据的最小副本。这通常仅包括用户 ID。如果 MessagingService 需要一些与用户相关的数据,它可能会向 UserManagementService 请求该信息。这对于检查用户角色(例如要显示的用户名)可能很有用。

从技术上讲,这种方法引入了一些数据冗余,但是如果组织得当,这种冗余可能会被视为 RDBMS 中不同表之间使用的外键。

它允许每个服务在大多数时间独立运行,从而增强整体系统的弹性。它还非常符合 DDD 尊重有界上下文边界的原则,因为每个服务都可以完全控制自己的数据模型和存储。

  上下文映射

为了管理这些有界上下文之间的关系,我们使用上下文映射。在这种情况下,我们可能会使用客户-供应商关系,其中 UserManagementService (供应商)向 MessagingService (客户)提供必要的用户数据。这可以通过 API 调用或消息队列来实现,确保每个服务都拥有所需的信息,而无需紧密耦合两个上下文。

此上下文映射的实现涉及几个关键方面。首先, UserManagementService 公开了一个定义良好的 API, MessagingService 可以使用该 API 来检索基本的用户信息。此 API 旨在仅提供必要的数据,例如用户 ID 和用户角色,而不暴露敏感信息。例如,当发布新消息时, MessagingService 可能会对 UserManagementService 进行 API 调用,以验证用户的存在及其角色。

Context Mapping 图 2. 不同限界上下文之间的上下文映射

作为客户与供应商关系的一部分,我们在服务之间定义了明确的服务级别协议 (SLA)。这些 SLA 指定预期的请求和响应结构、API 调用的时间表、事件发布的频率以及服务中断的处理。

利用这些模式,我们将为模块化、可维护的系统奠定基础。每个上下文都有明确的职责和明确定义的接口,允许独立开发和扩展。
这种分离还为未来的扩展提供了灵活性,例如添加新功能或与外部系统集成。

在下面的部分中,我们将深入研究每个有界上下文,详细探索它们的域模型、聚合和服务。

用户管理服务

UserManagementService 构成了我们的消息应用程序的重要组成部分,处理与用户帐户和角色相关的所有方面。该服务体现了自己的有界上下文,仅关注与用户相关的操作。
让我们深入研究该服务的关键组件,这些组件是根据领域驱动设计原则构建的。

  领域模型

UserManagementService 的核心是用户聚合。在 DDD 中,聚合是被视为单个单元的领域对象的集群。用户聚合封装了所有与用户相关的数据和行为。

User 聚合根包含基本属性,例如 userIdusernameemailpassword 。它还包括用户身份验证、用户角色和用户授权的方法。通过将这些职责集中在用户聚合中,我们确保所有与用户相关的操作保持数据一致性并遵守业务规则。

与 User 聚合相关联的是几个值对象。这些是不可变的对象,描述用户的特征,但没有概念上的标识。示例包括电子邮件和密码。
这些值对象封装了验证逻辑,确保电子邮件地址的格式符合 RFC 2822 的要求,并且密码的长度和其他标准满足安全要求。

Profile 值对象表示特定于用户的详细信息,例如 displayName、bio 和 profilePictureUrl。通过将 Profile 建模为单独的值对象,我们可以轻松扩展与 Profile 相关的功能,而不会扰乱 User 聚合。

 存储库

UserRepository 接口定义了保存和检索用户聚合的方法。这种抽象允许我们将领域模型与底层数据存储机制分开。该存储库的实现与我们选择的数据库系统(在本例中为 MySQL)接口。

该存储库包括 findById、findByUsername 和常规 CRUD 操作 (CREATE-READ-UPDATE-DELETE) 等方法。它还提供了更具体的查询方法,例如 findByEmail,可以在用户注册过程中使用以确保电子邮件的唯一性。

数据模型也仅限于用户聚合,可能如下图所示。

UMS Data Model 图 3. 用户管理数据模型

域服务(域流)

虽然大多数业务逻辑驻留在用户聚合内,但某些操作涉及多个聚合或需要外部资源。对于这些,我们使用域服务,这些域服务可能作为单独的服务存在,也可能被定义为单个服务内的模块。
到目前为止,我们更愿意将这些操作定义为一项服务中单独的可重用模块。

UserRegistrationModule 处理用户注册过程,这可能涉及检查现有用户、验证输入和触发欢迎电子邮件。该模块在用户聚合、UserRepository 和潜在的外部服务(例如电子邮件提供商)之间进行协调。

AuthModule 管理用户登录过程,包括密码验证和身份验证令牌的生成,并处理识别用户角色的请求。
该模块与用户聚合紧密配合,但将身份验证和授权逻辑分开,以便将来更轻松地更新身份验证和授权机制。

ApplicationGatewayModule 负责与其他外部组件和服务的通信。
在其他一些来源中,它也称为控制器,但是我们尝试在此仅包含逻辑,负责终止传入流量、数据序列化、输入验证和形成响应以将其返回给请求者。
该模块就像不同外部服务和内部模块之间的通信门面,为它们提供SLA,同时保持内部模块的灵活性。

我们可能想要使用的最后一个模块是 DataAccessLayerModule 。我们已经定义了数据模型,现在我们需要确定与其通信的方式。
从代码的任何点直接调用数据库是可能的,但由于多种原因可能不是一个好主意,主要原因与数据模型和应用程序逻辑之间的高耦合和低内聚相关。
使用这个模块,我们可以分解和抽象访问数据的逻辑。我们可以简单地创建一个名为 getUserWithRoles 的方法,并隐藏其背后的实现,而不是创建特定的 SQL 查询来选择特定用户并访问其权限。在这种情况下,我们通过调用唯一的一个方法来达到高一致性,该方法将成为单点访问数据的方法。

Domain Flow 图 4. 域模块框图

基础设施问题

虽然严格来说, UserManagementService 不是域模型的一部分,但它也解决了一些基础设施问题。其中包括实施密码散列和基于令牌的身份验证等安全措施、设置数据库连接和 ORM 映射以及配置 API 端点。

该服务还实现了重要领域事件的事件发布。例如,当用户更新其个人资料时,服务会发布 UserProfileUpdated 事件。其他服务,例如我们进一步考虑的 NotificationService ,可以订阅这些事件以维护整个系统的一致性并提供不同的状态更改。

通过根据 DDD 原则构建 UserManagementService ,我们创建了一个强大的、可维护的服务,它清楚地封装了所有与用户相关的功能。这种设计可以轻松扩展用户管理功能,并为我们的消息应用程序提供坚实的基础。

 消息服务

MessagingService 构成了我们的消息应用程序的内容创建和分发系统的核心。该服务包含其自己的有界上下文,管理消息(推文)和订阅。
让我们探讨一下根据领域驱动设计原则构建的该服务的关键组件。

  领域模型

MessagingService 中的中心聚合是消息。该聚合封装了用户创建的内容以及创建时间和作者信息等元数据。消息聚合根包含 messageIDcontentauthorID (这是 userID )和 createdAt .它还包括创建、编辑(如果允许)和删除消息的方法。

另一个重要的聚合是 Subscription ,代表用户之间的关注关系。订阅聚合包含一个 producerID 和一个 subscriberID ,它们与 UserManagementService 中的相应数据相关的不超过 userID ,包含订阅状态和创建日期。该聚合负责管理内容生产者和消费者之间的关系。

此上下文中的值对象可能包括 MessageContent ,它封装消息的实际文本或媒体内容及其作者 ID,以建立和确定订阅状态。

 存储库

MessagingService 可能包含多个存储库,以防我们希望将其拆分为独立服务来管理消息和订阅。在本文中,我们认为该功能密切相关,并在 MesagingService 的单一保护伞下以及该服务的单一数据库内对其进行管理。

MessageRepository 处理消息聚合的持久性和检索。它包括常规 CRUD、 findByMessageIdfindByAuthorIdfindBySubscriberId 等方法。我们还可以实现另一种方法,允许用户在创建消息时过滤消息,这可能是另一个方便的用例,并将此方法称为 findByCreatedAt

可以看出,该数据库包括与消息和订阅相关的所有实体。它的架构可能如下图所示。

Messaging Model 图 5. 消息传递服务数据模型

域服务(域流)

在此服务中,我们可以定义与 UserManagementService - 4 中相同数量的模块,其中 2 个是相同的 - DataAccessLayerModuleApplicationGatewayModule ,因为这些模块的职责是通用的并且与服务无关。这两个模块旨在为 MessagingService 其他模块之间的数据通信提供边界模块。最终他们的目标是转换和调整传入或传出数据及其在该域内发生的内部表示。

MessagingModule 在本例中,这是整个服务的核心模块,因为它管理消息创建、检索和编辑(如果支持)。该模块接受来自不同来源的数据,包括用户输入,例如消息文本、用户数据,通过使用用户提供的用户 ID 从 UserManagementService 获取,并使用内部逻辑执行请求的操作 - 创建、检索或编辑(如果支持)消息。

SubscriptionModule 处理管理用户订阅的复杂逻辑,包括创建新订阅、处理取消订阅以及管理静音或阻止等订阅状态。

Domain Flows 图 6. 域模块框图

基础设施问题

MessagingService 实现了多个基础设施级功能来支持其操作,因为对此服务的关注点应该与 UserManagementService 不同。其中包括设置消息队列来处理大量新消息、为频繁访问的订阅实施缓存机制以及管理数据库连接以实现高效的数据检索。

该服务还可以发布诸如 MessageCreatedSubscriptionChanged 之类的域事件,以供系统其他部分或外部服务使用,以实现通知或分析等功能。

通过根据 DDD 原则构建 MessagingService,我们创建了一个强大且可扩展的系统来处理消息应用程序的核心功能。
这种设计允许有效管理消息、订阅,同时为未来的扩展和优化提供清晰的边界。

可扩展性和性能考虑因素

随着我们的应用程序越来越受欢迎,可扩展性和性能成为关键问题。本节探讨确保我们的微服务架构能够处理不断增加的负载,同时保持响应能力和可靠性的策略。
一般来说,下面的这些技术对于应用程序功能并不重要,但是当系统开始经历高负载时,它们就变得至关重要。

 水平缩放

UserManagementServiceMessagingService 都被设计为无状态,允许轻松水平缩放。随着用户流量的增加,我们可以在负载均衡器后面部署每个服务的多个实例。这种方法将负载分布到多个服务器上,从而提高整体系统容量和弹性。

  数据库扩展

随着数据量的增长,数据库性能可能成为瓶颈。为了解决这个问题,我们可以为此实施几种策略。

对于 UserManagementService ,我们建立了一个读取副本系统,因为此服务负载假定检索操作优先于写入操作。这种设置允许我们跨多个数据库实例分发读取操作,从而显着减少主数据库的负载。
通过将大量读取操作定向到这些副本,我们确保主数据库可以专注于写入操作,从而提高整体系统性能和响应能力。

MessagingService 处理大量数据,需要更强大的扩展解决方案。在这里,我们可以实现数据库分片,这是一种根据用户 ID 或消息 ID 范围跨多个数据库实例对数据进行分区的技术。
这种方法不仅分布数据本身,而且还将查询负载分散到多个服务器上。因此,与单个数据库实例相比,我们可以处理更多数量的并发操作并存储更多的数据。

除了这些结构性变化之外,我们还应该重点关注查询优化和索引。
我们的数据库管理员和开发人员协同工作,持续监控查询性能,识别常用的查询模式并确保有适当的索引来支持它们。
即使数据量和复杂性不断增长,这种持续的细化过程也有助于保持数据库效率。

 缓存策略

缓存在性能优化策略中发挥着关键作用,是我们的服务和数据库之间的关键层。在 UserManagementService 中,我们为用户配置文件数据实现了复杂的缓存机制。我们可以利用任何内存存储(例如 Redis)来实现高性能内存数据存储,我们可以在其中缓存经常访问的用户信息。
这种方法显着减少了常见操作所需的数据库查询数量,例如检索用于显示或身份验证目的的用户详细信息。

MessagingService 采用更复杂的多层缓存策略来处理社交媒体源的大容量、实时性。
我们可以将内存缓存与 Caffeine 结合使用来存储最近和经常访问的提要条目,从而提供对这些热门数据的近乎即时的访问。对于较旧或访问频率较低的提要数据,我们利用 Redis 作为二级缓存。
这种分层方法使我们能够平衡超快速访问最新内容的需求与有效存储和检索大量历史数据的能力。

随着我们的系统水平扩展,保持多个服务实例之间的缓存一致性变得至关重要。为了解决这个问题,我们在集群模式下配置 Redis,确保我们的缓存层既是分布式的又是一致的。
这种设置使我们能够与应用程序服务一起扩展缓存基础设施,随着用户数量的增长保持性能。

最终一致性和 CAP 定理注意事项

在设计分布式系统时,我们必须努力解决 CAP 定理所描述的基本权衡问题,该定理指出,在发生网络分区时,分布式系统必须在一致性和可用性之间进行选择。
鉴于我们应用程序的性质,信息流的即时性是一个关键特征,因此在许多场景中,我们选择优先考虑可用性和分区容错性,而不是严格一致性。

CAP 图 7. CAP 定理问题

这一决定体现在我们对最终一致性的拥护中。例如,当用户在 UserManagementService 中更新其个人资料信息时,我们会立即在该服务中确认并应用此更改。但是,我们承认此更新在 MessagingService 或整个系统的缓存数据中反映出来之前可能会有短暂的延迟。在这段短暂的时间内,系统的不同部分对用户数据的看法可能略有不同。

虽然这种方法可能会出现暂时不一致的情况,但它使我们的系统即使在面临网络问题或高负载的情况下也能保持高可用性和响应能力。
我们通过仔细的系统设计来减轻这些不一致的影响,例如在数据更新中包含时间戳以帮助解决冲突,以及设置适当的用户对数据传播速度的期望。

这种最终一致性模型也扩展到我们系统的其他领域,例如关注者计数或消息分发。
通过放宽对即时、系统范围一致性的要求,我们可以更有效地处理大量操作,从而实现整体上更具可扩展性和响应能力的系统。
然而,我们也小心地确定应用程序中需要强一致性的领域,例如在金融交易或安全关键操作中,并相应地设计这些组件。

  结论

使用领域驱动设计 (DDD) 和微服务架构创建类似 Twitter 的应用程序的过程为构建复杂、可扩展的系统提供了宝贵的见解。通过对 UserManagementServiceMessagingService 的探索,我们演示了 DDD 原则如何指导健壮、可维护且灵活的应用程序架构的开发。

通过专注于明确定义的有界上下文,我们创建了可独立部署和可扩展的服务,但其整体功能具有凝聚力。
聚合、存储库和领域服务的使用使我们能够将复杂的业务逻辑封装在每个服务中,从而促进代码更易于理解并且与底层业务领域更紧密地结合。

我们的数据管理方法,包括实施每个服务数据库模式和复杂的缓存策略,展示了现代分布式系统如何处理大量数据,同时保持性能和响应能力。
采用事件驱动架构进行服务间通信凸显了松散耦合在创建具有弹性和适应变化的系统方面的力量。

在整个过程中,我们必须做出重要的架构决策,平衡数据一致性、可扩展性和系统复杂性等问题。
我们选择在应用程序的某些领域采用最终一致性,这证明了设计分布式系统时所需的细致入微的思维,同时考虑到 CAP 定理描述的权衡。

虽然我们的实现侧重于核心功能,省略了通知和评论等功能,但我们设计的架构为未来的扩展提供了坚实的基础。
随着应用程序的增长,我们应用的原则和模式可以扩展以适应新的功能和服务。

值得注意的是,旅程并不会随着实施而结束。持续监控、性能优化和迭代改进对于维护和发展这样的系统至关重要。
随着用户行为的变化和新技术的出现,我们类似 Twitter 的应用程序必须做好相应调整和扩展的准备。

领域驱动设计和微服务架构的结合提供了一种构建复杂、可扩展应用程序的强大方法。
通过关注清晰的领域边界,在适当的情况下采用最终一致性,并利用现代数据管理和服务间通信技术,我们可以创建不仅能够处理当前需求,而且能够根据未来需求不断发展的系统。