如果您的应用程序与我们所见的大多数启动微服务重构的应用程序类似,那么基本可以确定它正在使用单一的大型 关系型数据库。此外,该数据库有接近一半的几率是 Oracle 数据库——其余所有关系型数据库(DB2、SQL Server、Informix,甚至是 MySQL 或 Postgres 等开源数据库)共同瓜分剩余份额。事实上,摆脱(通常成本高昂的)企业级关系型数据库,正是推广微服务重构时常提及的优势之一。
目前,为许多微服务选择其他类型数据库——无论是 NewSQL 还是 NoSQL ——都存在充分理由。然而,一次性完全抛弃现有关系型数据库通常是一种鲁莽的决定。相反,您或许可以考虑采用更渐进的方式来更改数据库,正如我们倡导对现有 Java 代码采用渐进式重构方法一样。
但正如我们倡导的渐进式编码方法,数据库重构采用渐进方式的最大难点在于从何处着手。一旦决定采用渐进方法,您必须做出的首个决策是:应该使用一个大型数据库,还是多个小型数据库。初听之下,这似乎毫无意义——您当然不会想要一个大型数据库,那不就是单体架构中的模式吗!但请容我们首先阐明具体含义。
本质上,起点在于区分数据库服务器和数据库架构。对于熟悉 Oracle 或 Db2 等企业级数据库的用户而言,这已是第二天性,因为企业通常会搭建一个大型 Oracle 服务器(或 RAC——一个由多台较小服务器组成的大型服务器),供多个团队在其上托管各自独立的数据库(每个数据库以独立架构表示)。这样做的原因在于许可通常按 CPU 计算,而公司希望最大化资金利用率。将多个团队集中到一个大型服务器正是实现此目的的一种方式。而对于更熟悉 MySQL 或 PostgreSQL 等开源数据库的用户,这种情况较不常见,因为区分必要性较低。
这一点很重要,因为当我们讨论为微服务构建数据库时,关键是要减少或消除数据库层面的耦合——但这实际上是指数据库架构层面的耦合。当两个不同的微服务使用相同信息——即同一架构内的相同表时,问题就会出现。通过查看图 1,您就能理解我们的意思。
与单体应用程序(又称“大泥球”架构,其中所有组件相互连接)对应的数据库模式,是采用一个将所有表相互连接的大型架构。一旦形成这种结构,分离这些表所需的工作量将十分巨大。
然而,在向微服务过渡时,您必须认识到:在服务器级别共享硬件和许可所引发的问题要少得多。实际上,在进行微服务初步重构时,将新的、更清晰分离的架构保留在同一企业服务器中存在诸多优势——因为企业通常已建立完善的数据库备份恢复流程和数据库服务器更新机制,各个团队可以直接利用这些资源。
从某种意义上说,企业通过提供硬件、软件及其管理所实现的企业级数据库服务,正是 数据库即服务 (DBaaS) 的雏形。这种方式特别适合从功能区域更清晰分离单体应用部分开始的方案,即以“模块化单体”方法起步,如图 2 所示。
本示例(旨在展示进行中的重构工作)展现了如何通过分离对应三个新架构(A、B 和 C)的表来拆分数据库,这些架构与重构应用程序中的特定模块相对应。当它们被如此分离后,就能清晰地拆分为独立的微服务。不过,D 和 E 仍在重构过程中——它们仍共享一个具有互联数据表的单一架构。
最终,即使在数据库服务器级别的连接也可能会成为问题。例如,如果您选择了企业级数据库,就会受限于其提供的可用功能。即使在关系模型中,也并非所有架构都需要所有这些功能,或者它们可能需要通过不同服务器才能更好支持的功能(例如,更好的分片能力常被列为使用 NewSQL 数据库的理由)。同样地,升级由多个微服务共享的数据库服务器可能会导致多个服务同时中断。
但关键在于,该决定可以推迟——不必在项目启动时就立即做出。当团队开始初步重构时,在重构项目至少初始阶段将它们保留在同一数据库服务器中,是一种让团队在获取代码和数据库重构必要经验的同时实现渐进式变更的方式。
如上一节所提示,随着重构过程的推进,您可能需要更仔细地考虑数据库选项——并非所有数据集都完全适合关系模型。确定管理这些数据集的最佳方法通常归结为“您在数据库中实际存储的是什么?”这一问题。
我们多年来帮助企业构建、维护并时常“折磨”对象关系映射框架,但问题的现实是,在若干情况下,所存储的数据完全无法良好映射到关系数据模型。每当这种情况发生时,我们发现自己要么不得不“扭曲”关系模型以适应需求,要么更常见的是,在程序中费尽周折以强制代码匹配关系数据存储。
既然我们已经进入多语言持久化选择的时代,就可以重新审视其中一些决策并做出更优选择。特别是,我们将考察四种明显不适合关系模型的不同案例,然后思考一个关系模型实为最佳选择且将数据置于其他形式反而不当的案例。
多次审视企业系统的持久化代码后,我们常惊讶地发现:实际存储在关系数据库中的竟是序列化 Java 对象的二进制形式。这些数据被存放在“二进制大对象” (Blob) 列中,通常是开发团队面对将 Java 对象映射到关系表和列的复杂性时采取的无奈之举。Blob 存储存在明显缺陷:无法按列进行查询;访问速度通常较慢;对 Java 对象结构变化极其敏感——若对象结构发生重大变更,旧数据可能无法读取。
如果您的应用程序(或更可能是某个子模块)正在关系数据库中使用 Blob 存储,这已充分表明改用 Memcached 或 Redis 等键-值存储可能是更优选择。另一方面,您或许需要退一步重新审视存储内容的本质。若存储的实为结构化 Java 对象(可能具有深层结构,但非原生二进制格式),那么选用 Cloudant 或 MongoDB 等文档存储或许更为合适。更重要的是,只需稍加规划文档存储方式(例如上述数据库均支持 JSON 文档存储,且 JSON 解析器既普及又易定制),就能比采用存储机制更不透明的 Blob 方案更轻松地应对“模式漂移”问题。
多年前,当 Martin Fowler 撰写《企业应用程序架构模式》时,我们曾频繁通信,并就其中诸多模式开展了多次热烈的评审会议。有一个模式始终显得格格不入,即 Active Record 模式。其特殊之处在于,作为 Java 社区出身的我们从未接触过这种模式(尽管 Martin 向我们保证它在 Microsoft .NET 编程社区中十分常见)。但最让我们印象深刻的是——尤其是当我们开始看到一些使用 iBatis 等开源技术实现的 Java 版本时——这种模式似乎最适合处理扁平化对象。
如果您要映射到数据库的对象完全扁平化——与其他对象没有关联关系(除可能存在有限的嵌套对象外)——那么您很可能未能充分利用关系模型的全部能力。事实上,您存储的更像是一种文档。在我们遇到的多数案例中,开发团队实际上是在存储纸质文档的电子版本,无论是客户满意度调查、问题工单等。这种情况下,像 Cloudant 或 MongoDB 这样的文档数据库可能是最合适的选择。将代码拆分成基于该类数据库的独立服务,不仅能显著简化代码结构,相比在大型企业数据库中实现相同功能,其维护性通常也会更好。
我们在 ORM 系统中观察到的另一种常见模式是“表中参考数据加载至内存缓存”的组合方式。参考数据指那些不常(或从不)更新但被频繁读取的信息。典型例子包括美国州名列表或加拿大省份列表;其他示例还有医疗代码或标准零件清单。这类数据常被用于填充 GUI 中的下拉列表。
常见的模式是每次使用时先从数据表(通常是扁平的两列或至多三列表)中读取列表。然而,随后您会发现每次执行此操作的性能代价令人难以承受,因此应用程序改为在启动时将其读入类似 EhCache 的内存缓存中。
每当遇到此类问题时,就意味着需要重构为更简单、更快速的缓存机制。同样,这种情况使用 Memcached 或 Redis 是完全合理的。如果参考数据独立于数据库的其他结构(通常如此,或至多呈松散耦合),那么将数据及其服务从系统其余部分分离出来会很有帮助。
在某个客户系统中,我们当时正在进行复杂的金融建模,需要执行非常复杂的查询(涉及六重或七重连接)才能创建程序要操作的对象。更新操作更为复杂,我们必须结合多个不同层级的乐观锁检查,才能确定哪些数据发生了变更,以及数据库中的内容是否仍与我们创建和操作的结构一致。
回顾来看,我们当时处理的数据显然更适合用图结构进行建模。类似这样的情况(当时我们建模的是资金分层,每层由不同类型的股权和债务构成,每种资产以不同货币计价且到期时间各异,每个估值还涉及不同的计算规则)几乎必然需要一种能轻松实现核心需求的数据结构——即在图结构中上下遍历,并随意移动图的局部组件。
此时采用 Apache Tinkerpop 或 Neo4J 这类解决方案会是理想选择。通过直接将解决方案建模为图结构,我们本可避免编写大量复杂的 Java 和 SQL 代码,同时很可能显著提升运行时性能。
尽管在许多场景下,NoSQL 数据库确实是特定数据结构的合理选择,但关系模型的灵活性和强大功能往往难以被超越。关系数据库的卓越之处在于,您能高效地将同一数据按不同目的“灵活地切片和组合”成多种形态。诸如数据库视图等技术,支持您为同一数据创建多重映射——这在需要对复杂数据模型执行一系列关联查询时尤为实用。
若需深入了解 NoSQL 与 SQL 的对比,请参阅“SQL 与 NoSQL:差异何在?”
如我们在前文所述,若微服务的“下界”是由一组实体组合而成的聚合及其相关操作服务构成,那么采用 SQL 来实现代表系列关联查询的视图通常是最简便直接的方式。
我们最初在客户案例中观察到这一点,该案例也催生了 前文中简单的 Account/LineItem 示例。当时一家银行竭力尝试在面向文档的 NoSQL 数据库中实现类似简单模型,最终却受限于 CAP 定理而未能成功。然而,该团队当初选择该数据模型完全是基于错误考量。他们出于对架构一致性的错误执念,为所有不同类型的微服务统一选用了面向文档的数据库。
在此案例中,他们需要 ACID 模式的全部特性,但无需分片功能;其现有系统基于关系模型运行多年且性能表现始终处于完全可接受的水平,同时他们并未预期会出现爆发式增长。然而,尽管系统核心需要 ACID 事务且无需分区,但这并不适用于其系统中所有不同的组成部分。SQL 数据库在某些方面表现卓越,ACID 事务正是其强项之一。在微服务系统中,将 ACID 事务围绕其操作的最小数据集进行分组通常是正确的做法。
但 SQL 未必意味着传统 SQL 数据库。它既可以指传统数据库(在许多微服务架构中确实有其适用场景),也至少体现在另外两类对微服务实施团队颇具价值的数据库中。其一是“小型 SQL”,即 MySQL和Postgres 等开源数据库的领域。对于许多实施微服务的团队而言,选择这类数据库出于多种充分理由:
使用这类“小型 SQL”数据库的主要局限在于,它们通常不具备企业级数据库所能支持的同等规模的扩展能力(尤其在分片方面)。不过,一批新兴数据库供应商和开源项目很好地解决了这一问题,它们融合了小型 SQL 数据库的最佳特性与 NoSQL 数据库的可扩展性。这类常被称为“NewSQL”的数据库包括 CockroachDB、Apache Trafodion 和 Clustrix。
当团队需要完整的 ACID 事务支持、全面兼容关系模型的 SQL 引擎,同时还需具备横向扩展与分片能力时,NewSQL 数据库无疑是理想之选。但这种选择伴随一定代价——这些数据库的配置与管理通常比传统的“小型 SQL”方案更为复杂。精通这些数据库的技术人员也更难寻觅。尽管如此,在评估选项时仍需要对其予以慎重考量。
我们快速探讨了多种可能伪装成编码问题的数据库建模问题。如果您发现自己存在一个或多个此类特定问题,那么最好从现有企业数据存储中分离出来,并采用不同类型的数据存储重新规划。无论如何,我们的建议是:采取渐进式的方式逐步推进。请注意,并非所有应用程序都需要所有类型的数据模型,在开始大规模实施之前,您需要逐步培养相关技能并充分了解所选数据库的运维能力。
最后,必须认识到整个微服务方法的基础理念是:构建每个微服务的团队有权自主决定最适合其需求的数据存储。我们遇到的最大问题(正如许多案例所暗示)是试图让单一的数据表示和存储方法适用于所有场景。
我多次目睹团队在根本不应使用 NoSQL 数据库的场景下,却不得不艰难应对 CAP 定理的现实约束。同样地,试图从 NoSQL 数据库进行复杂报表提取也往往令人挫败不已。另一方面,我们所示的案例也表明关系模型并非万能。其他数据模型同样具有其适用场景。我们可以给出的最佳建议是:确保您的团队拥有必要的自主权,为每个微服务选择最合适的数据模型。
IBM Garage 专为加速前进、智慧工作与颠覆式创新而构建。
Red Hat OpenShift on IBM Cloud 是一个完全托管的 OpenShift 容器平台 (OCP)。
使用开发运维软件和工具,在多种设备和环境中构建、部署和管理云原生应用程序。
利用 IBM 的云咨询服务发掘新功能并提升业务敏捷性。了解如何通过混合云战略和专家合作共同制定解决方案、加快数字化转型并优化性能。